Reunit en un seul outil les fonctions utiles de MobaXterm, Termius et Bitvise, sans compte ni service tiers, avec toutes les donnees en local. - Terminaux multi-onglets : sessions SSH et terminaux Windows locaux (PowerShell / cmd) via la pseudo-console native ConPTY - Gestionnaire de fichiers SFTP facon WinSCP : double panneau, colonnes triables, glisser-deposer, renommage, permissions, file de transferts. Ouvrable sur une connexion en cours sans creer de favori - Gestion des cles SSH : generation Ed25519/RSA, import, export - Gestion du fichier known_hosts, avec remplacement guide d'une cle d'hote modifiee (cas frequent des serveurs en DHCP) - Trois themes (clair, bleu nuit, noir), commutables a chaud - Authentification automatique reprenant le comportement de la commande ssh (agent SSH et cles ~/.ssh), mot de passe demande seulement en secours Aucun mot de passe n'est stocke sur le disque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
6.6 KiB
Python
152 lines
6.6 KiB
Python
"""Boite de dialogue de creation/edition d'une session SSH."""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
|
|
QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QWidget, QVBoxLayout,
|
|
QCheckBox, QMessageBox,
|
|
)
|
|
|
|
from app.session_manager import Session, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT
|
|
from app.key_manager import KeyManager
|
|
|
|
|
|
class SessionDialog(QDialog):
|
|
"""Formulaire de session. En mode `connect_label`, il sert de connexion rapide :
|
|
on saisit les infos et on se connecte directement, l'enregistrement etant optionnel."""
|
|
|
|
def __init__(self, key_manager: KeyManager, session: Session | None = None,
|
|
parent=None, connect_label: str = "", title: str = ""):
|
|
super().__init__(parent)
|
|
self.key_manager = key_manager
|
|
self.session = session or Session()
|
|
self.quick_mode = bool(connect_label)
|
|
self.setWindowTitle(title or ("Nouvelle session" if session is None else "Modifier la session"))
|
|
self.setMinimumWidth(420)
|
|
|
|
layout = QVBoxLayout(self)
|
|
form = QFormLayout()
|
|
|
|
self.name_edit = QLineEdit(self.session.name)
|
|
self.group_edit = QLineEdit(self.session.group)
|
|
self.group_edit.setPlaceholderText("ex: Production/Web")
|
|
self.host_edit = QLineEdit(self.session.host)
|
|
self.port_spin = QSpinBox()
|
|
self.port_spin.setRange(1, 65535)
|
|
self.port_spin.setValue(self.session.port or 22)
|
|
self.user_edit = QLineEdit(self.session.username)
|
|
|
|
self.auth_combo = QComboBox()
|
|
self.auth_combo.addItem("Automatique (comme la commande ssh)", AUTH_AUTO)
|
|
self.auth_combo.addItem("Cle SSH choisie", AUTH_KEY)
|
|
self.auth_combo.addItem("Mot de passe (saisi a la connexion)", AUTH_PASSWORD)
|
|
self.auth_combo.addItem("Agent SSH", AUTH_AGENT)
|
|
self.auth_combo.setToolTip(
|
|
"Automatique : utilise l'agent SSH et vos cles ~/.ssh comme le fait la commande ssh,\n"
|
|
"et demande un mot de passe seulement si aucune cle ne convient."
|
|
)
|
|
idx = self.auth_combo.findData(self.session.auth_type)
|
|
self.auth_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
|
|
self.key_combo = QComboBox()
|
|
self._reload_keys()
|
|
self.browse_key_btn = QPushButton("Parcourir...")
|
|
|
|
key_row = QWidget()
|
|
key_layout = QHBoxLayout(key_row)
|
|
key_layout.setContentsMargins(0, 0, 0, 0)
|
|
key_layout.addWidget(self.key_combo, 1)
|
|
key_layout.addWidget(self.browse_key_btn)
|
|
|
|
self.notes_edit = QTextEdit(self.session.notes)
|
|
self.notes_edit.setFixedHeight(60)
|
|
|
|
form.addRow("Nom :", self.name_edit)
|
|
form.addRow("Dossier :", self.group_edit)
|
|
form.addRow("Hote :", self.host_edit)
|
|
form.addRow("Port :", self.port_spin)
|
|
form.addRow("Utilisateur :", self.user_edit)
|
|
form.addRow("Authentification :", self.auth_combo)
|
|
form.addRow("Cle privee :", key_row)
|
|
form.addRow("Notes :", self.notes_edit)
|
|
|
|
layout.addLayout(form)
|
|
|
|
self.save_check = QCheckBox("Enregistrer dans mes sessions (optionnel)")
|
|
if self.quick_mode:
|
|
# decoche par defaut : les serveurs de test changent d'IP (DHCP), les
|
|
# enregistrer systematiquement encombre la liste pour rien
|
|
self.save_check.setChecked(False)
|
|
layout.addWidget(self.save_check)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
if self.quick_mode:
|
|
buttons.button(QDialogButtonBox.Ok).setText(connect_label)
|
|
buttons.accepted.connect(self._on_accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
self.auth_combo.currentIndexChanged.connect(self._update_key_row)
|
|
self.browse_key_btn.clicked.connect(self._browse_key)
|
|
self._update_key_row()
|
|
|
|
if self.session.key_path:
|
|
existing = self.key_combo.findData(self.session.key_path)
|
|
if existing >= 0:
|
|
self.key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.key_combo.addItem(self.session.key_path, self.session.key_path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
def _reload_keys(self) -> None:
|
|
self.key_combo.clear()
|
|
for entry in self.key_manager.keys:
|
|
self.key_combo.addItem(f"{entry.name} ({entry.key_type})", entry.private_path)
|
|
|
|
def _update_key_row(self) -> None:
|
|
is_key_auth = self.auth_combo.currentData() == AUTH_KEY
|
|
self.key_combo.setEnabled(is_key_auth)
|
|
self.browse_key_btn.setEnabled(is_key_auth)
|
|
|
|
def _browse_key(self) -> None:
|
|
path, _ = QFileDialog.getOpenFileName(self, "Choisir une cle privee")
|
|
if path:
|
|
self.key_combo.addItem(path, path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
def should_save(self) -> bool:
|
|
"""En connexion rapide, indique si la session doit aussi etre enregistree."""
|
|
return self.save_check.isChecked() if self.quick_mode else True
|
|
|
|
def _on_accept(self) -> None:
|
|
if not self.host_edit.text().strip():
|
|
QMessageBox.warning(self, "Hote manquant", "Indiquez l'adresse du serveur (nom d'hote ou IP).")
|
|
self.host_edit.setFocus()
|
|
return
|
|
if not self.user_edit.text().strip():
|
|
QMessageBox.warning(self, "Utilisateur manquant", "Indiquez le nom d'utilisateur.")
|
|
self.user_edit.setFocus()
|
|
return
|
|
if self.auth_combo.currentData() == AUTH_KEY and not self.key_combo.currentData():
|
|
QMessageBox.warning(
|
|
self, "Cle manquante",
|
|
"Aucune cle privee n'est selectionnee.\n\n"
|
|
"Choisissez une cle (bouton « Parcourir... »), ou passez l'authentification "
|
|
"sur « Automatique » — qui utilisera l'agent SSH et vos cles ~/.ssh comme le "
|
|
"fait la commande ssh.",
|
|
)
|
|
self.auth_combo.setFocus()
|
|
return
|
|
self.session.name = self.name_edit.text().strip() or self.host_edit.text().strip()
|
|
self.session.group = self.group_edit.text().strip()
|
|
self.session.host = self.host_edit.text().strip()
|
|
self.session.port = self.port_spin.value()
|
|
self.session.username = self.user_edit.text().strip()
|
|
self.session.auth_type = self.auth_combo.currentData()
|
|
self.session.key_path = self.key_combo.currentData() or ""
|
|
self.session.notes = self.notes_edit.toPlainText()
|
|
self.accept()
|
|
|
|
def result_session(self) -> Session:
|
|
return self.session
|