feat(ws): implement full duplex bi-directional WebSocket proxy bridge supporting subdomains and path-based routing

This commit is contained in:
bolbol
2026-08-20 15:04:12 +01:00
parent 442e128450
commit 23168a13e8
3 changed files with 134 additions and 4 deletions
+34 -3
View File
@@ -1,4 +1,4 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
from app.config import settings, UNIVERSES, BASE_DIR
from app.routers import api, proxy
from app.proxy import close_http_client, proxy_request
from app.proxy import close_http_client, proxy_request, proxy_websocket
from app.personas import ensure_dirs
def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
@@ -55,7 +55,7 @@ app = FastAPI(
lifespan=lifespan
)
# Subdomain Routing Middleware
# Subdomain Routing Middleware for HTTP
@app.middleware("http")
async def subdomain_routing_middleware(request: Request, call_next):
host = request.headers.get("host", "")
@@ -96,6 +96,37 @@ async def index_view(request: Request):
}
)
# WebSocket Proxy Route (intercepts any WebSocket connection across subdomains)
@app.websocket("/{path:path}")
@app.websocket("")
async def websocket_proxy_endpoint(websocket: WebSocket, path: str = ""):
host = websocket.headers.get("host", "")
target = get_subdomain_target(host)
if target:
backend_url, universe_id = target
await proxy_websocket(
client_ws=websocket,
backend_url=backend_url,
path=path,
universe_id=universe_id
)
elif path.startswith("u/"):
parts = path.split("/", 2)
if len(parts) >= 2 and parts[1] in UNIVERSES:
u_id = parts[1]
subpath = parts[2] if len(parts) > 2 else ""
await proxy_websocket(
client_ws=websocket,
backend_url=UNIVERSES[u_id].backend_url,
path=subpath,
universe_id=u_id
)
else:
await websocket.close(code=1008)
else:
await websocket.close(code=1008)
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host=settings.host, port=settings.port, reload=settings.debug)