Files
pyssh-client/app/dialogs/tunnel_dialog.py
snoopy 9d346d76ed Tunnels SSH, envoi multi-onglets, et reparation de main_window.py
Tunnels (redirection de ports), la specialite de Bitvise : un bouton dans la
barre d'un onglet SSH ouvre un gestionnaire de redirections.
- locale (-L) : un port de votre poste est relaye, via la connexion SSH, vers
  une machine joignable depuis le serveur (typiquement un service qui n'ecoute
  que sur la boucle locale du serveur)
- distante (-R) : l'inverse
Chaque tunnel tourne dans son propre thread, un relais bloque ne genant ni
l'interface ni les autres tunnels. Ils sont fermes avec la session.

Envoi simultane, façon MobaXterm : le bouton "Multi" diffuse la frappe vers
tous les onglets terminal. La diffusion ecrit directement dans les connexions
et non via les widgets terminal, sinon chaque onglet rediffuserait a son tour.

Reparation : un de mes scripts de correction avait reecrit main_window.py en
retraduisant deux fois les fins de ligne, produisant des sequences \r\r\n ;
le fichier avait ete committe ainsi (2538 lignes au lieu de 856, une ligne vide
inseree partout). Le contenu etait intact, seule la mise en forme etait touchee.
Le fichier est reconstitue par normalisation des fins de ligne, et un controle
d'integrite a ete ajoute a la verification de non-regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 11:52:25 +02:00

115 lines
4.5 KiB
Python

"""Gestion des tunnels SSH (redirections de ports) d'une session ouverte."""
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QComboBox, QSpinBox, QLineEdit, QLabel, QMessageBox, QAbstractItemView,
)
from app.tunnels import (
DIRECTION_LOCAL, DIRECTION_REMOTE, DIRECTION_LABELS, TunnelManager, describe,
)
class TunnelDialog(QDialog):
def __init__(self, manager: TunnelManager, parent=None):
super().__init__(parent)
self.manager = manager
self.setWindowTitle("Tunnels SSH (redirection de ports)")
self.setMinimumSize(660, 400)
layout = QVBoxLayout(self)
info = QLabel(
"Une redirection <b>locale</b> ouvre un port sur votre poste et l'envoie, a travers "
"la connexion SSH, vers une machine joignable depuis le serveur — pratique pour "
"atteindre un service qui n'ecoute que sur la boucle locale du serveur.<br>"
"Une redirection <b>distante</b> fait l'inverse : un port ouvert sur le serveur est "
"relaye vers une machine joignable depuis votre poste."
)
info.setWordWrap(True)
layout.addWidget(info)
self.tree = QTreeWidget()
self.tree.setHeaderLabels(["Sens", "Redirection", "Etat"])
self.tree.setRootIsDecorated(False)
self.tree.setSelectionMode(QAbstractItemView.SingleSelection)
self.tree.setColumnWidth(0, 110)
self.tree.setColumnWidth(1, 380)
layout.addWidget(self.tree)
form = QFormLayout()
self.direction = QComboBox()
for key, label in DIRECTION_LABELS.items():
self.direction.addItem(label, key)
self.listen_port = QSpinBox(); self.listen_port.setRange(1, 65535); self.listen_port.setValue(8080)
self.dest_host = QLineEdit("127.0.0.1")
self.dest_port = QSpinBox(); self.dest_port.setRange(1, 65535); self.dest_port.setValue(80)
form.addRow("Sens :", self.direction)
form.addRow("Port a ouvrir :", self.listen_port)
form.addRow("Machine de destination :", self.dest_host)
form.addRow("Port de destination :", self.dest_port)
layout.addLayout(form)
buttons = QHBoxLayout()
self.add_btn = QPushButton("Ouvrir le tunnel")
self.add_btn.setObjectName("primary")
self.remove_btn = QPushButton("Fermer le tunnel selectionne")
buttons.addWidget(self.add_btn)
buttons.addWidget(self.remove_btn)
buttons.addStretch()
close_btn = QPushButton("Fermer")
close_btn.clicked.connect(self.accept)
buttons.addWidget(close_btn)
layout.addLayout(buttons)
self.add_btn.clicked.connect(self.add_tunnel)
self.remove_btn.clicked.connect(self.remove_tunnel)
self.refresh()
def refresh(self) -> None:
self.tree.clear()
for tunnel in self.manager.tunnels:
etat = f"Erreur : {tunnel.error}" if tunnel.error else "Actif"
item = QTreeWidgetItem([
DIRECTION_LABELS.get(tunnel.direction, tunnel.direction),
describe(tunnel.config),
etat,
])
item.setData(0, Qt.UserRole, id(tunnel))
self.tree.addTopLevelItem(item)
self.remove_btn.setEnabled(bool(self.manager.tunnels))
def add_tunnel(self) -> None:
if not self.dest_host.text().strip():
QMessageBox.warning(self, "Destination manquante", "Indiquez la machine de destination.")
return
config = {
"direction": self.direction.currentData(),
"listen_port": self.listen_port.value(),
"dest_host": self.dest_host.text().strip(),
"dest_port": self.dest_port.value(),
}
tunnel = self.manager.start(config)
if tunnel.error:
QMessageBox.critical(
self, "Tunnel impossible",
f"{describe(config)}\n\n{tunnel.error}\n\n"
"Le port est peut-etre deja utilise sur le poste.",
)
self.manager.stop(tunnel)
self.refresh()
def remove_tunnel(self) -> None:
item = self.tree.currentItem()
if not item:
return
target = item.data(0, Qt.UserRole)
for tunnel in list(self.manager.tunnels):
if id(tunnel) == target:
self.manager.stop(tunnel)
break
self.refresh()