feat(chat): implement native hub chat with sqlite conversations store, SSE streaming and DSH 5th universe integration
This commit is contained in:
+36
-30
@@ -51,10 +51,6 @@ async def close_http_client():
|
||||
_http_transport = None
|
||||
|
||||
def rewrite_cookie_path(cookie_header: str, universe_id: str) -> str:
|
||||
"""
|
||||
Rewrites the Path attribute of a Set-Cookie header to /u/{universe_id}/
|
||||
when using path-based proxying.
|
||||
"""
|
||||
target_path = f"/u/{universe_id}/"
|
||||
if re.search(r'(?i)\bpath=[^;]*', cookie_header):
|
||||
return re.sub(r'(?i)\bpath=[^;]*', f'Path={target_path}', cookie_header)
|
||||
@@ -95,18 +91,27 @@ async def proxy_request(
|
||||
if request.url.query:
|
||||
target_url = f"{target_url}?{request.url.query}"
|
||||
|
||||
is_dsh = "dsh-vps:3080" in backend_url or universe_id == "dsh"
|
||||
|
||||
req_headers: List[Tuple[bytes, bytes]] = []
|
||||
for raw_k, raw_v in request.headers.raw:
|
||||
k_str = raw_k.decode("latin-1").lower()
|
||||
if k_str not in HOP_BY_HOP_HEADERS and k_str != "host":
|
||||
req_headers.append((raw_k, raw_v))
|
||||
if k_str in HOP_BY_HOP_HEADERS or k_str == "host":
|
||||
continue
|
||||
if is_dsh and k_str == "origin":
|
||||
continue
|
||||
req_headers.append((raw_k, raw_v))
|
||||
|
||||
# Forward original host header info
|
||||
req_headers.append((b"x-forwarded-host", request.headers.get("host", "").encode("latin-1")))
|
||||
# If DSH backend, replicate localhost:3080 Host/Origin for internal origin check
|
||||
if is_dsh:
|
||||
req_headers.append((b"host", b"localhost:3080"))
|
||||
req_headers.append((b"origin", b"http://localhost:3080"))
|
||||
else:
|
||||
req_headers.append((b"x-forwarded-host", request.headers.get("host", "").encode("latin-1")))
|
||||
|
||||
req_headers.append((b"x-forwarded-proto", (request.url.scheme or "http").encode("latin-1")))
|
||||
|
||||
body = await request.body()
|
||||
|
||||
is_path_proxied = universe_id and request.url.path.startswith(f"/u/{universe_id}")
|
||||
|
||||
try:
|
||||
@@ -117,10 +122,8 @@ async def proxy_request(
|
||||
content=body
|
||||
)
|
||||
|
||||
# Pure stateless async request execution — NO cookie jar retention
|
||||
upstream_res = await transport.handle_async_request(upstream_req)
|
||||
|
||||
# Build raw headers list for precise multi-header control
|
||||
raw_headers: List[Tuple[bytes, bytes]] = []
|
||||
media_type = upstream_res.headers.get("content-type")
|
||||
|
||||
@@ -132,11 +135,9 @@ async def proxy_request(
|
||||
continue
|
||||
|
||||
if k_str == "set-cookie" and is_path_proxied:
|
||||
# Path-based proxying: rewrite cookie path
|
||||
v_str = rewrite_cookie_path(v_str, universe_id)
|
||||
raw_headers.append((b"set-cookie", v_str.encode("latin-1")))
|
||||
elif k_str == "location" and is_path_proxied:
|
||||
# Path-based proxying: rewrite location redirect
|
||||
if v_str.startswith("/") and not v_str.startswith(f"/u/{universe_id}"):
|
||||
v_str = f"/u/{universe_id}{v_str}"
|
||||
raw_headers.append((b"location", v_str.encode("latin-1")))
|
||||
@@ -161,7 +162,7 @@ async def proxy_request(
|
||||
except httpx.ConnectError:
|
||||
logger.error(f"Failed to connect to backend at {target_url}")
|
||||
return Response(
|
||||
content=f"<html><head><title>Backend Unavailable</title></head><body style='background:#121212;color:#ef4444;font-family:sans-serif;padding:2rem;'><h2>Instance Hermes inaccessible</h2><p>Le backend sur <code>{backend_url}</code> ne répond pas. Vérifiez le tunnel Tailscale ou l'état du conteneur.</p></body></html>",
|
||||
content=f"<html><head><title>Backend Unavailable</title></head><body style='background:#121212;color:#ef4444;font-family:sans-serif;padding:2rem;'><h2>Instance Hermes inaccessible</h2><p>Le backend sur <code>{backend_url}</code> ne répond pas. Vérifiez le réseau interne ou l'état du conteneur.</p></body></html>",
|
||||
status_code=502,
|
||||
media_type="text/html"
|
||||
)
|
||||
@@ -179,10 +180,6 @@ async def proxy_websocket(
|
||||
path: str,
|
||||
universe_id: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Bi-directional full duplex WebSocket proxy bridge.
|
||||
Forwards connection from client browser to backend Hermes universe instance.
|
||||
"""
|
||||
ws_base = backend_url.replace("https://", "wss://").replace("http://", "ws://").rstrip("/")
|
||||
sub_path = path.lstrip("/")
|
||||
upstream_url = f"{ws_base}/{sub_path}" if sub_path else ws_base
|
||||
@@ -190,30 +187,39 @@ async def proxy_websocket(
|
||||
if client_ws.url.query:
|
||||
upstream_url = f"{upstream_url}?{client_ws.url.query}"
|
||||
|
||||
is_dsh = "dsh-vps:3080" in backend_url or universe_id == "dsh"
|
||||
|
||||
upstream_headers = {}
|
||||
for k, v in client_ws.headers.items():
|
||||
if k.lower() not in WS_HOP_BY_HOP_HEADERS:
|
||||
upstream_headers[k] = v
|
||||
|
||||
upstream_headers["x-forwarded-host"] = client_ws.headers.get("host", "")
|
||||
upstream_headers["x-forwarded-proto"] = client_ws.url.scheme or "http"
|
||||
if is_dsh:
|
||||
upstream_headers["host"] = "localhost:3080"
|
||||
origin_val = "http://localhost:3080"
|
||||
else:
|
||||
upstream_headers["x-forwarded-host"] = client_ws.headers.get("host", "")
|
||||
upstream_headers["x-forwarded-proto"] = client_ws.url.scheme or "http"
|
||||
origin_val = None
|
||||
|
||||
if client_ws.client:
|
||||
upstream_headers["x-forwarded-for"] = client_ws.client.host
|
||||
|
||||
# Check subprotocols
|
||||
subprotocols_raw = client_ws.headers.get("sec-websocket-protocol", "")
|
||||
subprotocols = [s.strip() for s in subprotocols_raw.split(",") if s.strip()] or None
|
||||
|
||||
try:
|
||||
async with websockets.connect(
|
||||
upstream_url,
|
||||
additional_headers=upstream_headers,
|
||||
subprotocols=subprotocols,
|
||||
ping_interval=20,
|
||||
ping_timeout=20,
|
||||
max_size=10 * 1024 * 1024
|
||||
) as upstream_ws:
|
||||
# Accept client connection with negotiated subprotocol
|
||||
connect_kwargs = {
|
||||
"additional_headers": upstream_headers,
|
||||
"subprotocols": subprotocols,
|
||||
"ping_interval": 20,
|
||||
"ping_timeout": 20,
|
||||
"max_size": 10 * 1024 * 1024
|
||||
}
|
||||
if origin_val:
|
||||
connect_kwargs["origin"] = origin_val
|
||||
|
||||
async with websockets.connect(upstream_url, **connect_kwargs) as upstream_ws:
|
||||
await client_ws.accept(subprotocol=upstream_ws.subprotocol)
|
||||
|
||||
async def client_to_upstream():
|
||||
|
||||
Reference in New Issue
Block a user