#!/usr/bin/env python3 """ Windows / macOS 本地开发:SSH 隧道把线上 MinIO/PocketBase 映射到本机。 默认映射: localhost:9100 -> 服务器 127.0.0.1:9100 (MinIO S3 API) localhost:8090 -> 服务器 127.0.0.1:8090 (PocketBase,可选) 用法: python scripts/dev-tunnel.py 环境变量(可写在 .env.local): DEV_SSH_HOST=82.157.112.245 DEV_SSH_USER=ubuntu DEV_SSH_PASSWORD=你的密码 DEV_TUNNEL_MINIO_PORT=9100 DEV_TUNNEL_POCKETBASE=0 # 设为 1 时同时映射 8090 """ from __future__ import annotations import os import socket import sys import threading import time from pathlib import Path try: import paramiko except ImportError: print("请先安装 paramiko: pip install paramiko", file=sys.stderr) raise ROOT = Path(__file__).resolve().parent.parent def _read_env_file(path: Path) -> dict[str, str]: values: dict[str, str] = {} if not path.exists(): return values for raw in path.read_text(encoding="utf-8").splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) values[key.strip()] = value.strip().strip('"').strip("'") return values def load_env() -> None: merged: dict[str, str] = {} for path in [ROOT / ".env.local", ROOT / ".env", ROOT / "backend" / ".env"]: merged.update(_read_env_file(path)) for key, value in merged.items(): os.environ.setdefault(key, value) def forward_tunnel(local_port: int, remote_host: str, remote_port: int, transport) -> None: def handle(client: socket.socket, addr) -> None: try: chan = transport.open_channel("direct-tcpip", (remote_host, remote_port), addr) except Exception as exc: print(f"[tunnel] open_channel failed {local_port}->{remote_host}:{remote_port}: {exc}") client.close() return def relay(src, dst) -> None: try: while True: data = src.recv(1024 * 64) if not data: break dst.sendall(data) except Exception: pass finally: try: src.close() except Exception: pass try: dst.close() except Exception: pass threading.Thread(target=relay, args=(client, chan), daemon=True).start() threading.Thread(target=relay, args=(chan, client), daemon=True).start() server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(("127.0.0.1", local_port)) server.listen(32) print(f"[tunnel] listening 127.0.0.1:{local_port} -> {remote_host}:{remote_port}") while True: client_sock, addr = server.accept() threading.Thread( target=handle, args=(client_sock, addr), daemon=True, ).start() def main() -> None: load_env() host = os.getenv("DEV_SSH_HOST", "82.157.112.245") user = os.getenv("DEV_SSH_USER", "ubuntu") password = os.getenv("DEV_SSH_PASSWORD", "") if not password: print("缺少 DEV_SSH_PASSWORD,请在 .env.local 中配置", file=sys.stderr) sys.exit(1) minio_local = int(os.getenv("DEV_TUNNEL_MINIO_PORT", "9100")) pb_enabled = os.getenv("DEV_TUNNEL_POCKETBASE", "0").lower() in {"1", "true", "yes"} pb_local = int(os.getenv("DEV_TUNNEL_POCKETBASE_PORT", "8090")) client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) print(f"[tunnel] connecting {user}@{host} ...") client.connect(host, username=user, password=password, timeout=20) transport = client.get_transport() if transport is None: print("SSH transport unavailable", file=sys.stderr) sys.exit(1) threading.Thread( target=forward_tunnel, args=(minio_local, "127.0.0.1", 9100, transport), daemon=True, ).start() if pb_enabled: threading.Thread( target=forward_tunnel, args=(pb_local, "127.0.0.1", 8090, transport), daemon=True, ).start() print("[tunnel] ready. Ctrl+C to stop.") try: while True: time.sleep(1) except KeyboardInterrupt: print("\n[tunnel] stopping") finally: client.close() if __name__ == "__main__": main()