"""反向隧道 HTTP 路由:/api/userPort/{userName}。 把进来的 HTTP 请求反代到该 user 当前活跃隧道对应的本地端口 (SSH remote forward 绑定的 127.0.0.1:tunnel_port),经隧道回指 user 的本地服务。 """ from __future__ import annotations import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response from sqlalchemy.orm import Session from ..database import get_db from ..dao.tunnel_session_dao import TunnelSessionDAO from ..services.tunnel_service import TunnelService router = APIRouter(prefix="/api/userPort", tags=["tunnel"]) # 不应透传给上游的 hop-by-hop / 控制头 _HOP_BY_HOP = { "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host", "content-length", } def _service(db: Session = Depends(get_db)) -> TunnelService: return TunnelService(TunnelSessionDAO(db)) async def _proxy(request: Request, session, prefix: str) -> Response: """把请求透传到该 user 当前活跃隧道对应的本地端口。 去掉 /api/userPort/{userName} 前缀后才是上游路径,根路径补 /。 """ upstream_path = request.url.path.replace(prefix, "", 1) or "/" url = f"http://127.0.0.1:{session.tunnel_port}{upstream_path}" if request.url.query: url += f"?{request.url.query}" body = await request.body() headers = {k: v for k, v in request.headers.items() if k.lower() not in _HOP_BY_HOP} try: async with httpx.AsyncClient(timeout=30.0) as client: upstream = await client.request( request.method, url, content=body, headers=headers, ) except httpx.RequestError as exc: raise HTTPException(502, f"隧道端口不可达:{exc}") from exc resp_headers = {k: v for k, v in upstream.headers.items() if k.lower() not in _HOP_BY_HOP} return Response(content=upstream.content, status_code=upstream.status_code, headers=resp_headers) # 根路径:/api/userPort/{userName} @router.api_route( "/{userName}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], summary="反代到指定 user 的隧道端口(根路径)", description=( "查 DB 该 user 当前活跃的隧道端口,把请求透传到 127.0.0.1:tunnel_port" "(经 SSH 反向隧道回指 user 的本地服务)。无活跃隧道返回 502。" ), ) async def proxy_to_tunnel( userName: str, request: Request, service: TunnelService = Depends(_service), ) -> Response: session = service.get_active(userName) if session is None: raise HTTPException(502, f"无活跃隧道:user={userName}") return await _proxy(request, session, f"/api/userPort/{userName}") # 子路径:/api/userPort/{userName}/... —— 反向代理必须能透传任意路径与查询串, # 否则上游服务里所有非根路由都会 404。 @router.api_route( "/{userName}/{upstream_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], summary="反代到指定 user 的隧道端口(子路径透传)", description=( "把 /api/userPort/{userName}/ 透传到 127.0.0.1:tunnel_port/。" "无活跃隧道返回 502。" ), ) async def proxy_to_tunnel_path( userName: str, upstream_path: str, request: Request, service: TunnelService = Depends(_service), ) -> Response: session = service.get_active(userName) if session is None: raise HTTPException(502, f"无活跃隧道:user={userName}") return await _proxy(request, session, f"/api/userPort/{userName}")