v1.2.0 + v1.3.0: Localization, About dialog, TOTP/2FA, stability improvements

v1.2.0:
- GUI localization (EN/RU/ZH) with language switcher and persistent selection
- About dialog (ⓘ) with app info, features, quick start guide
- core/i18n.py — internationalization module with t() function
- All GUI components translated via t() keys

v1.3.0:
- TOTP/2FA tab — Google Authenticator compatible codes with live 30s countdown,
  one-click copy, per-server secret management
- core/totp.py — TOTP module (pyotp, RFC 6238)
- core/logger.py — rotating file logger (5MB, 3 backups)
- Stronger Fernet encryption key with automatic migration from old key
- Thread-safe server store with locks, atomic writes, auto-restore on corruption
- Parallel status checks via ThreadPoolExecutor (up to 10 concurrent)
- SSH client: explicit channel cleanup, Unix key permissions
- Server dialog: port validation (1-65535), TOTP secret field
- Language change preserves active tab and server selection
- pyotp dependency added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-23 11:07:51 -05:00
parent f86d6a7214
commit bf39fd7b67
26 changed files with 2029 additions and 246 deletions

View File

@@ -1,10 +1,11 @@
"""
SSH client wrapper — refactored from ssh.py.
Handles connect, exec, sftp, key management via paramiko.
SSH client wrapper — connect, exec, sftp, key management via paramiko.
"""
import os
import platform
import paramiko
from core.logger import log
class SSHClientWrapper:
@@ -32,7 +33,13 @@ class SSHClientWrapper:
client.connect(**kwargs)
self._client = client
return client
except Exception:
except paramiko.AuthenticationException:
log.debug(f"Key auth failed for {self.server.get('alias', '?')}, trying password")
del kwargs["key_filename"]
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
except Exception as e:
log.debug(f"Key connect failed: {e}")
del kwargs["key_filename"]
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -60,6 +67,7 @@ class SSHClientWrapper:
def exec_command(self, command: str, use_sudo: bool = True) -> tuple[str, str, int]:
"""Execute command. Auto-sudo if user != root and use_sudo=True."""
client = self.connect()
stdin = stdout = stderr = None
try:
user = self.server.get("user", "root")
need_sudo = use_sudo and user != "root"
@@ -87,6 +95,13 @@ class SSHClientWrapper:
return out, err, exit_code
finally:
# Close channels explicitly
for ch in (stdin, stdout, stderr):
if ch:
try:
ch.close()
except Exception:
pass
client.close()
def upload(self, local_path: str, remote_path: str, progress_cb=None):
@@ -115,7 +130,6 @@ class SSHClientWrapper:
client.close()
def check_connection(self) -> bool:
"""Quick connection test."""
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -153,7 +167,6 @@ class SSHClientWrapper:
return False
def install_key(self) -> str:
"""Install SSH public key on server. Returns status message."""
pub_key_path = self.key_path + ".pub"
if not os.path.exists(pub_key_path):
raise FileNotFoundError(f"No public key at {pub_key_path}")
@@ -161,7 +174,6 @@ class SSHClientWrapper:
with open(pub_key_path, "r") as f:
pub_key = f.read().strip()
# Check if already installed
out, _, _ = self.exec_command(
f'grep -c "{pub_key}" ~/.ssh/authorized_keys 2>/dev/null || echo 0',
use_sudo=False
@@ -169,7 +181,6 @@ class SSHClientWrapper:
if out.strip() != "0":
return "Key already installed"
# Install
command = (
f'mkdir -p ~/.ssh && chmod 700 ~/.ssh && '
f'echo "{pub_key}" >> ~/.ssh/authorized_keys && '
@@ -182,18 +193,22 @@ class SSHClientWrapper:
raise Exception(f"Key install failed: {err or out}")
def generate_key(self) -> str:
"""Generate ed25519 SSH key pair if not exists."""
if os.path.exists(self.key_path):
return f"Key already exists: {self.key_path}"
os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
key = paramiko.Ed25519Key.generate()
key.write_private_key_file(self.key_path)
# Write public key
# Set restrictive permissions on private key (Unix)
if platform.system() != "Windows":
os.chmod(self.key_path, 0o600)
pub_key = f"ssh-ed25519 {key.get_base64()} server-manager"
with open(self.key_path + ".pub", "w") as f:
f.write(pub_key + "\n")
log.info(f"SSH key generated: {self.key_path}")
return f"Key generated: {self.key_path}"