""" IoT Gateway — HTTP reverse proxy for ESP32 web servers running in QEMU. When an ESP32 sketch starts a WebServer on port 80, QEMU's slirp networking with hostfwd exposes it on a dynamic host port. This endpoint proxies HTTP requests from the browser to that host port, enabling users to interact with their simulated ESP32 HTTP server. URL pattern: /api/gateway/{client_id}/{path} → http://127.0.0.1:{hostfwd_port}/{path} """ import logging import httpx from fastapi import APIRouter, Request, Response from app.services.esp32_lib_manager import esp_lib_manager router = APIRouter() logger = logging.getLogger(__name__) @router.api_route( '/{client_id}/{path:path}', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], ) async def gateway_proxy(client_id: str, path: str, request: Request) -> Response: """Reverse-proxy an HTTP request to the ESP32's web server.""" inst = esp_lib_manager.get_instance(client_id) if not inst or not inst.wifi_enabled or inst.wifi_hostfwd_port == 0: return Response( content='{"error":"No WiFi-enabled ESP32 instance found for this client"}', status_code=404, media_type='application/json', ) target_url = f'http://127.0.0.1:{inst.wifi_hostfwd_port}/{path}' body = await request.body() # Forward relevant headers (skip hop-by-hop) skip_headers = {'host', 'transfer-encoding', 'connection'} headers = { k: v for k, v in request.headers.items() if k.lower() not in skip_headers } try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.request( method=request.method, url=target_url, content=body, headers=headers, ) except httpx.ConnectError: return Response( content='{"error":"ESP32 HTTP server is not responding. Make sure your sketch starts a WebServer on port 80."}', status_code=502, media_type='application/json', ) except httpx.TimeoutException: return Response( content='{"error":"ESP32 HTTP server timed out"}', status_code=504, media_type='application/json', ) # Forward response back to browser resp_headers = dict(resp.headers) # Remove hop-by-hop headers for h in ('transfer-encoding', 'connection', 'content-encoding'): resp_headers.pop(h, None) return Response( content=resp.content, status_code=resp.status_code, headers=resp_headers, media_type=resp.headers.get('content-type'), )