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:
49
tools/ssh.py
49
tools/ssh.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SSH utility for Claude Code — connects to servers by alias.
|
||||
Credentials stored locally in servers.json, NEVER exposed to AI API.
|
||||
Credentials stored locally in servers.json (encrypted), NEVER exposed to AI API.
|
||||
|
||||
Usage:
|
||||
python ssh.py ALIAS "command" # run as configured user (auto-sudo if needed)
|
||||
@@ -24,22 +24,59 @@ import paramiko
|
||||
|
||||
# Shared config — same file used by ServerManager GUI
|
||||
SHARED_DIR = os.path.expanduser("~/.server-connections")
|
||||
SERVERS_FILE = os.path.join(SHARED_DIR, "servers.json")
|
||||
SETTINGS_FILE = os.path.join(SHARED_DIR, "settings.json")
|
||||
DEFAULT_SERVERS_FILE = os.path.join(SHARED_DIR, "servers.json")
|
||||
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519")
|
||||
SSH_CONFIG_PATH = os.path.expanduser("~/.ssh/config")
|
||||
|
||||
# Encryption support — encryption.py is copied to SHARED_DIR by GUI setup
|
||||
if SHARED_DIR not in sys.path:
|
||||
sys.path.insert(0, SHARED_DIR)
|
||||
try:
|
||||
from encryption import decrypt, encrypt, is_encrypted
|
||||
HAS_ENCRYPTION = True
|
||||
except ImportError:
|
||||
HAS_ENCRYPTION = False
|
||||
|
||||
|
||||
def _get_servers_file() -> str:
|
||||
"""Get servers file path from settings.json or use default."""
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
try:
|
||||
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
||||
settings = json.load(f)
|
||||
path = settings.get("servers_path", "")
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
except Exception:
|
||||
pass
|
||||
return DEFAULT_SERVERS_FILE
|
||||
|
||||
|
||||
# ── Data ──────────────────────────────────────────────
|
||||
|
||||
def load_servers():
|
||||
with open(SERVERS_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
servers_file = _get_servers_file()
|
||||
with open(servers_file, "rb") as f:
|
||||
raw = f.read()
|
||||
if HAS_ENCRYPTION and is_encrypted(raw):
|
||||
text = decrypt(raw)
|
||||
data = json.loads(text)
|
||||
else:
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
return data, {s["alias"]: s for s in data.get("servers", [])}
|
||||
|
||||
|
||||
def save_servers(data):
|
||||
with open(SERVERS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
servers_file = _get_servers_file()
|
||||
text = json.dumps(data, indent=2, ensure_ascii=False)
|
||||
if HAS_ENCRYPTION:
|
||||
encrypted = encrypt(text)
|
||||
with open(servers_file, "wb") as f:
|
||||
f.write(encrypted)
|
||||
else:
|
||||
with open(servers_file, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
# ── Connection ────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user