"""SSH client with optional E2E encryption. When E2E is enabled, commands are encrypted with the tunnel key before being sent over SSH, and responses are decrypted on return. The Go agent on the VPS handles decryption/execution/re-encryption. When E2E is disabled, commands run as plain text over SSH (still encrypted by SSH transport, just no additional application-layer encryption). """ import paramiko import config import json _client = None def get_client(): global _client if _client and _client.get_transport() and _client.get_transport().is_active(): return _client cfg = config.load() _client = paramiko.SSHClient() _client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) key_path = cfg.get("ssh_key_path", "") if not key_path: raise RuntimeError("SSH key path not configured") # Try Ed25519 first, fall back to RSA try: key = paramiko.Ed25519Key.from_private_key_file(key_path) except Exception: try: key = paramiko.RSAKey.from_private_key_file(key_path) except Exception: key = paramiko.ECDSAKey.from_private_key_file(key_path) _client.connect( hostname=cfg["vps_host"], port=cfg["vps_port"], username=cfg["vps_user"], pkey=key, timeout=15, ) return _client def run(cmd, timeout=30): """Execute a command on the VPS. Uses E2E encryption when enabled.""" import e2e if e2e.is_e2e_enabled(): return _run_e2e(cmd, timeout) else: return _run_plain(cmd, timeout) def _run_plain(cmd, timeout=30): """Execute command via plain SSH (no E2E).""" client = get_client() _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() return {"stdout": out, "stderr": err, "exit_code": code} def _run_e2e(cmd, timeout=30): """Execute command via E2E encrypted tunnel. 1. Encrypt command with tunnel key 2. SSH: echo | setec-agent 3. Agent decrypts, executes, encrypts response 4. Decrypt response locally """ import e2e tunnel_key = e2e.get_tunnel_key() # Build the wrapped command agent_cmd = e2e.wrap_command_for_agent(tunnel_key, cmd) # Execute via SSH client = get_client() _, stdout, stderr = client.exec_command(agent_cmd, timeout=timeout) out = stdout.read().decode("utf-8", errors="replace").strip() err = stderr.read().decode("utf-8", errors="replace").strip() code = stdout.channel.recv_exit_status() # If agent returned an error on stderr, it's an agent-level failure if code != 0 and not out: return { "stdout": "", "stderr": f"E2E agent error: {err}" if err else "E2E agent returned non-zero with no output", "exit_code": code, } # Decrypt the response try: response = e2e.decrypt_response(tunnel_key, out) return { "stdout": response.get("stdout", ""), "stderr": response.get("stderr", ""), "exit_code": response.get("exit_code", 0), } except Exception as ex: # If decryption fails, the output might be from a non-E2E command # or the agent isn't installed. Return raw output with warning. return { "stdout": "", "stderr": f"E2E decryption failed: {str(ex)}\nRaw output: {out[:200]}", "exit_code": -1, } def run_plain_always(cmd, timeout=30): """Always run without E2E — used for agent deployment and setup commands.""" return _run_plain(cmd, timeout) def close(): global _client if _client: _client.close() _client = None