# GHCS v0.2 · terminal_router.py · SSH Web终端后端 · WebSocket 部署路径:`~/guanghu-dev/ghcs-mono/backend/terminal_router.py` ```python """ GHCS v0.2 - SSH Web终端 WebSocket + paramiko · 浏览器内操作服务器 """ import os import json import asyncio import logging from typing import Optional from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException from auth import verify_token logger = logging.getLogger("ghcs.terminal") router = APIRouter(prefix="/ws", tags=["terminal"]) SSH_KEY_PATH = os.getenv("GHCS_SSH_KEY", os.path.expanduser("~/.ssh/id_rsa")) # 可用服务器白名单 SERVERS = { "ghcs": { "name": "GHCS部署台", "host": "43.139.207.172", "port": 22, "username": "root", "key_filename": SSH_KEY_PATH, }, } # 命令白名单前缀(安全:只允许白名单内的命令执行) # 空列表 = 不做命令过滤(开发阶段可用,正式上线前必须设置) COMMAND_WHITELIST_PREFIXES: list[str] = [] # 危险命令黑名单(即使白名单为空,这些命令也绝对不允许执行) DANGEROUS_COMMANDS = [ "rm -rf /", "mkfs", "dd if=", "> /dev/sda", ":(){ :|:& };:", ] def _validate_command(cmd: str) -> bool: """验证命令是否安全""" cmd_stripped = cmd.strip() if not cmd_stripped: return True # 黑名单检查 for dangerous in DANGEROUS_COMMANDS: if dangerous in cmd_stripped: return False # 如果配置了白名单,检查前缀 if COMMAND_WHITELIST_PREFIXES: return any( cmd_stripped.startswith(prefix) for prefix in COMMAND_WHITELIST_PREFIXES ) return True @router.get("/terminal/servers") async def list_terminal_servers(): """列出可连接的服务器(无需鉴权,仅返回服务器名称和IP)""" return { "servers": [ {"key": k, "name": v["name"], "host": v["host"]} for k, v in SERVERS.items() ] } @router.websocket("/terminal/{server_key}") async def terminal_websocket(websocket: WebSocket, server_key: str): """WebSocket终端连接""" if server_key not in SERVERS: await websocket.close(code=4004, reason=f"未知服务器: {server_key}") return await websocket.accept() import paramiko import threading server = SERVERS[server_key] client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) channel = None running = True try: # 连接SSH client.connect( hostname=server["host"], port=server["port"], username=server["username"], key_filename=server["key_filename"], timeout=10, ) # 打开交互式shell channel = client.invoke_shell( term="xterm-256color", width=120, height=40, ) channel.settimeout(0.0) logger.info(f"[TERMINAL] connected to {server_key} ({server['host']})") def read_ssh(): """从SSH读取输出 → 推送到WebSocket""" nonlocal running while running: try: if channel.recv_ready(): data = channel.recv(4096) if data: asyncio.run_coroutine_threadsafe( websocket.send_bytes(data), asyncio.get_event_loop(), ) else: import time time.sleep(0.01) except Exception: if running: import time time.sleep(0.05) # 启动读取线程 reader_thread = threading.Thread(target=read_ssh, daemon=True) reader_thread.start() # 主循环:接收WebSocket输入 → 发送到SSH while True: try: data = await websocket.receive_text() # 解析JSON消息 try: msg = json.loads(data) except json.JSONDecodeError: msg = {"type": "stdin", "data": data} if msg.get("type") == "resize": # 终端窗口大小调整 cols = msg.get("cols", 120) rows = msg.get("rows", 40) channel.resize_pty(width=cols, height=rows) elif msg.get("type") == "stdin": # 普通输入 stdin_data = msg.get("data", "") if _validate_command(stdin_data): channel.send(stdin_data) else: channel.send(b"\r\n[GHCS] 命令被安全策略阻止\r\n") else: # 兼容纯文本输入 if _validate_command(data): channel.send(data) else: channel.send(b"\r\n[GHCS] 命令被安全策略阻止\r\n") except WebSocketDisconnect: break except Exception as e: logger.error(f"[TERMINAL] error: {e}") break except Exception as e: logger.error(f"[TERMINAL] SSH连接失败: {e}") await websocket.send_text(json.dumps({ "type": "error", "message": f"SSH连接失败: {str(e)}" })) finally: running = False if channel: try: channel.close() except Exception: pass try: client.close() except Exception: pass logger.info(f"[TERMINAL] disconnected from {server_key}") ``` --- ## 接口说明 | 接口 | 方法 | 作用 | | --- | --- | --- | | `GET /ws/terminal/servers` | GET | 列出可连接的服务器 | | `WS /ws/terminal/{server_key}` | WebSocket | 建立终端连接 | ## WebSocket 消息格式 ### 发送(前端 → 后端) ```json {"type": "stdin", "data": "ls -la\n"} {"type": "resize", "cols": 120, "rows": 40} ``` ### 接收(后端 → 前端) - SSH stdout/stderr → 二进制字节流(bytes) - 错误消息 → `{"type": "error", "message": "..."}` ## 安全设计 - `DANGEROUS_COMMANDS` 黑名单:绝对不允许执行的命令 - `COMMAND_WHITELIST_PREFIXES`:正式上线后可启用白名单模式 - 开发阶段白名单为空 = 不做命令过滤 · 方便调试 ## 🔧 部署步骤 ### Step 1 · 保存文件 复制代码 → VS Code 新建 `~/guanghu-dev/ghcs-mono/backend/terminal_router.py` → ⌘S ### Step 2 · 安装依赖 ```bash cd ~/guanghu-dev/ghcs-mono/backend source venv/bin/activate pip install websockets paramiko ``` ### Step 3 · 改 [main.py](http://main.py) 在 `from api_keys import router as api_keys_router` 下方加: ```python from terminal_router import router as terminal_router ``` 在 `app.include_router(api_keys_router)` 下方加: ```python app.include_router(terminal_router) ``` ### Step 4 · 重启后端 ```bash pkill -f "uvicorn main:app" uvicorn main:app --host 127.0.0.1 --port 5002 & ``` ### Step 5 · 验证 ```bash # 测试服务器列表接口 curl http://127.0.0.1:5002/ws/terminal/servers ``` 预期返回:`{"servers":[{"key":"ghcs","name":"GHCS部署台","host":"43.139.207.172"}]}`