153 lines
4.5 KiB
Python
153 lines
4.5 KiB
Python
#!/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()
|