Client SSH/SFTP personnel pour Windows
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>
This commit is contained in:
151
app/dialogs/session_dialog.py
Normal file
151
app/dialogs/session_dialog.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user