# -*- coding: utf-8 -*- r'''连 AWS Lightsail 那台新服务器的 SSH 助手。 == 为什么单独一个模块,不改 devssh.py == devssh.py 是连**随身 WiFi 那台 4G 小机器**的(192.168.2.156,密码登录)。 那台还在跑(网站暂时还挂在上面),不能动它的连接逻辑。 新服务器用**密钥**登录、IP 是公网 <源站IP>,认证方式完全不同, 所以单独一个模块更清楚。 == 用法 == from lssh import run, upload, download, probe run("uname -a") # 跑命令 upload("relay/relay.py", "/opt/ykxmz-relay/relay.py") ''' from __future__ import annotations import os import socket from pathlib import Path import paramiko def _host() -> str: """服务器公网 IP —— **不写死在代码里**。 这个仓库是公开的。IP 写进来等于把源站地址告诉所有人, 而源站躲在 Cloudflare 后面本来就不该被知道(直接打 IP 就绕过 CDN 和防护了)。2026-10-08 就因为 IP 泄漏返工过一次。 取值顺序: 1. 环境变量 YKXMZ_LS_HOST 2. _build/.lightsail_host(一行 IP,已 gitignore) """ env = os.environ.get("YKXMZ_LS_HOST", "").strip() if env: return env f = Path(__file__).resolve().parent / ".lightsail_host" if f.is_file(): return f.read_text(encoding="utf-8").strip() raise SystemExit( "没拿到服务器地址。二选一:\n" " set YKXMZ_LS_HOST=<你的服务器IP>\n" " 或把 IP 写进 _build/.lightsail_host(该文件已在 .gitignore 里)") HOST = _host() USER = os.environ.get("YKXMZ_LS_USER", "admin") PORT = int(os.environ.get("YKXMZ_LS_PORT", "22")) KEY = Path(os.environ.get( "YKXMZ_LS_KEY", str(Path.home() / ".ssh" / "lightsail-ykxmz.pem"))) _client = None def _connect(timeout: int = 25): global _client if _client is not None: try: # 探一下还活着没 _client.exec_command("true", timeout=5) return _client except Exception: # noqa: BLE001 _client = None if not KEY.is_file(): raise RuntimeError( "找不到 SSH 密钥:%s\n" "(把 Lightsail 的 .pem 放到那里,或者设 YKXMZ_LS_KEY)" % KEY) cli = paramiko.SSHClient() cli.set_missing_host_key_policy(paramiko.AutoAddPolicy()) cli.connect(HOST, port=PORT, username=USER, key_filename=str(KEY), timeout=timeout, allow_agent=False, look_for_keys=False, banner_timeout=30, auth_timeout=30) _client = cli return cli def run(command: str, timeout: int = 300, quiet: bool = False) -> tuple: """跑一条命令,返回 (退出码, 输出)。""" cli = _connect() if not quiet: print(" [ls] %s" % command.splitlines()[0][:90]) stdin, stdout, stderr = cli.exec_command(command, timeout=timeout, get_pty=False) out = stdout.read().decode("utf-8", "replace") err = stderr.read().decode("utf-8", "replace") code = stdout.channel.recv_exit_status() return code, (out + err) def upload(local: str, remote: str, mode: int = 0o644) -> None: """传一个文件上去。""" cli = _connect() sftp = cli.open_sftp() try: # 先把远端目录建出来 d = os.path.dirname(remote) if d: try: sftp.stat(d) except IOError: # 逐级建 parts = d.strip("/").split("/") cur = "" for p in parts: cur += "/" + p try: sftp.stat(cur) except IOError: try: sftp.mkdir(cur) except IOError: pass sftp.put(local, remote) sftp.chmod(remote, mode) finally: sftp.close() print(" 已上传 %s -> %s" % (os.path.basename(local), remote)) def download(remote: str, local: str) -> None: cli = _connect() sftp = cli.open_sftp() try: Path(local).parent.mkdir(parents=True, exist_ok=True) sftp.get(remote, local) finally: sftp.close() print(" 已下载 %s -> %s" % (remote, local)) def probe() -> str | None: """通不通。返回一句话描述,或 None。""" try: code, out = run("uname -sm; nproc; free -m | sed -n 2p", timeout=30, quiet=True) if code != 0: return None return out.strip().replace("\n", " | ") except Exception as exc: # noqa: BLE001 return "连接失败:%s" % exc if __name__ == "__main__": print(" 目标: %s@%s:%d 密钥 %s" % (USER, HOST, PORT, KEY)) print(" " + (probe() or "不通"))