""" ============================================ RUHI-CORE - Built-in Reverse Proxy Routes traffic to internal service ports ============================================ """ import httpx from fastapi import Request, Response from fastapi.responses import StreamingResponse from loguru import logger from core.port_manager import port_manager from core.process_manager import process_manager class ReverseProxy: """ Reverse proxy to route traffic from /proxy/{service_name}/ to the internal port of the running service """ def __init__(self): self._client = httpx.AsyncClient( timeout=30.0, follow_redirects=True, limits=httpx.Limits( max_connections=100, max_keepalive_connections=20 ) ) logger.info("🔀 ReverseProxy initialized") async def proxy_request(self, service_name: str, path: str, request: Request) -> Response: """Proxy an incoming request to the target service""" # Find the service service = process_manager.get_service_by_name(service_name) if not service: return Response( content=f'{{"error": "Service \'{service_name}\' not found"}}', status_code=404, media_type="application/json" ) if service.status != "running": return Response( content=f'{{"error": "Service \'{service_name}\' is not running (status: {service.status})"}}', status_code=503, media_type="application/json" ) if not service.port: return Response( content=f'{{"error": "Service \'{service_name}\' has no port assigned"}}', status_code=503, media_type="application/json" ) # Build target URL target_url = f"http://127.0.0.1:{service.port}/{path}" # Build headers (forward relevant ones) headers = {} for key, value in request.headers.items(): if key.lower() not in ("host", "connection", "content-length", "transfer-encoding"): headers[key] = value headers["X-Forwarded-For"] = request.client.host headers["X-Forwarded-Proto"] = request.url.scheme headers["X-Forwarded-Host"] = request.headers.get("host", "") headers["X-Real-IP"] = request.client.host try: # Get request body body = await request.body() # Make proxied request response = await self._client.request( method=request.method, url=target_url, headers=headers, content=body, params=dict(request.query_params) ) # Build response headers resp_headers = dict(response.headers) # Remove hop-by-hop headers for h in ("connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length"): resp_headers.pop(h, None) return Response( content=response.content, status_code=response.status_code, headers=resp_headers, media_type=response.headers.get("content-type") ) except httpx.ConnectError: return Response( content=f'{{"error": "Cannot connect to service \'{service_name}\' on port {service.port}"}}', status_code=502, media_type="application/json" ) except httpx.TimeoutException: return Response( content=f'{{"error": "Request to service \'{service_name}\' timed out"}}', status_code=504, media_type="application/json" ) except Exception as e: logger.error(f"Proxy error for {service_name}: {str(e)}") return Response( content=f'{{"error": "Proxy error: {str(e)}"}}', status_code=502, media_type="application/json" ) async def close(self): """Close the HTTP client""" await self._client.aclose() # Global instance reverse_proxy = ReverseProxy()