Files
ssh-e2e/test_e2e.py
T
leefer 27a3b4c6a8 SSH 隧道管理器:pywebview+paramiko 桌面应用
- 本地端口转发隧道管理(密码/私钥认证,自动重连,随应用启动)
- 主机与隧道可视化配置,DPAPI 加密存储机密,known_hosts 校验
- 自定义无边框窗口,深色界面,托盘驻留(最小化/关闭到托盘)
- 内置本地测试 SSH 服务端与端到端自测脚本
2026-09-24 17:14:59 +08:00

144 lines
5.0 KiB
Python

"""端到端验证:走真实的 Api 代码路径建立隧道,再请求经隧道转发的 HTTP 服务。
前置: 先运行 `python test_sshd.py`(127.0.0.1:2222, test/test123)。
用法: .venv/Scripts/python test_e2e.py
"""
import socket
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
ROOT = Path(__file__).parent
sys.path.insert(0, str(ROOT))
TEST_DIR = ROOT / ".test_data"
LOCAL_PORT = 13000 # 隧道本机端口
SVC_PORT = 3210 # 模拟"只对远程主机本机开放"的服务端口
def wait_port(port: int, expect_open: bool, timeout: float = 15.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
s = socket.socket()
s.settimeout(0.4)
try:
s.connect(("127.0.0.1", port))
s.close()
if expect_open:
return True
except OSError:
if not expect_open:
return True
finally:
s.close()
time.sleep(0.25)
return False
def tunnel_status(api, tid: str) -> str:
for t in api.get_state()["state"]["tunnels"]:
if t["id"] == tid:
return t["runtime"]["status"]
return "gone"
def main() -> int:
import shutil
shutil.rmtree(TEST_DIR, ignore_errors=True)
TEST_DIR.mkdir(parents=True)
from storage import Store
from engine import TunnelManager
from main import Api
events = []
manager = TunnelManager(lambda ev: events.append(ev), TEST_DIR)
store = Store(TEST_DIR)
api = Api(store, manager)
# 1) 后台起"远程服务"
www = TEST_DIR / "www"
www.mkdir(exist_ok=True)
(www / "index.html").write_text("<h1>TUNNEL-E2E-OK</h1>", encoding="utf-8")
svc = subprocess.Popen(
[sys.executable, "-m", "http.server", str(SVC_PORT), "--directory", str(www)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
assert wait_port(SVC_PORT, True), "http 服务未启动"
# 2) 起本地测试 SSH 服务端
sshd = subprocess.Popen([sys.executable, str(ROOT / "test_sshd.py")],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
assert wait_port(2222, True), "test_sshd 未启动"
try:
# 3) 通过 Api 添加主机和隧道
r = api.save_host({"name": "本机测试", "hostname": "127.0.0.1", "port": 2222,
"username": "test", "auth": "password", "password": "test123"})
assert r["ok"], r
host_id = r["id"]
# 密码错误的情况应失败
r = api.probe_host({"hostname": "127.0.0.1", "port": 2222, "username": "test",
"auth": "password", "password": "wrong"})
assert r["ok"] is False, f"错误密码竟能通过: {r}"
print("[PASS] 错误密码被正确拒绝:", r["message"])
r = api.probe_host({"hostname": "127.0.0.1", "port": 2222, "username": "test",
"auth": "password", "password": "test123"})
assert r["ok"], r
print("[PASS] 主机测试连接:", r["message"])
r = api.save_tunnel({"host_id": host_id, "name": "e2e", "local_port": LOCAL_PORT,
"remote_host": "127.0.0.1", "remote_port": SVC_PORT,
"autostart": False, "autoreconnect": True})
assert r["ok"], r
tid = r["id"]
# 4) 启动隧道并等待建立
assert api.start_tunnel(tid)["ok"]
deadline = time.time() + 20
while time.time() < deadline and tunnel_status(api, tid) != "connected":
time.sleep(0.3)
assert tunnel_status(api, tid) == "connected", \
"隧道未能建立: " + json_events_tail(events)
print(f"[PASS] 隧道已建立 127.0.0.1:{LOCAL_PORT} → 127.0.0.1:{SVC_PORT}")
# 5) 经隧道请求 HTTP 服务
with urllib.request.urlopen(f"http://127.0.0.1:{LOCAL_PORT}", timeout=10) as resp:
body = resp.read().decode()
assert "TUNNEL-E2E-OK" in body, body
print("[PASS] 经隧道取回内容:", body.strip())
# 6) 断开后端口应关闭
api.stop_tunnel(tid)
deadline = time.time() + 10
while time.time() < deadline and tunnel_status(api, tid) != "stopped":
time.sleep(0.2)
assert tunnel_status(api, tid) == "stopped"
assert wait_port(LOCAL_PORT, False, timeout=5), "断开后本地端口仍可连接"
print("[PASS] 断开隧道,本地端口已释放")
# 7) DPAPI 加密落盘校验
store.save()
raw = (TEST_DIR / "config.json").read_text(encoding="utf-8")
assert "test123" not in raw and "dpapi:" in raw
print("[PASS] 密码已以 DPAPI 加密存储,明文不落盘")
print("\n全部通过 ✔")
return 0
finally:
manager.stop_all()
svc.terminate()
sshd.terminate()
def json_events_tail(events) -> str:
import json
lines = [e for e in events if e.get("type") == "log"][-6:]
return "\n".join(json.dumps(e, ensure_ascii=False) for e in lines)
if __name__ == "__main__":
sys.exit(main())