import os import re import time from urllib.parse import urlparse from fastapi import APIRouter, HTTPException, Request, Response import httpx from session_manager import session_manager from http_pool import make_http_request from logging_config import ( RequestContext, log_request, log_session_operation, log_security_event, ) router = APIRouter(tags=["proxy"]) ALL_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"] def get_session_from_cookie(request: Request) -> str: session_id = request.cookies.get("lovdata_session") if not session_id: raise HTTPException( status_code=400, detail="No active session - please access via /session/{id}/ first", ) return session_id @router.api_route("/global/{path:path}", methods=ALL_METHODS) async def proxy_global_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"global/{path}") @router.api_route("/assets/{path:path}", methods=ALL_METHODS) async def proxy_assets_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"assets/{path}") @router.api_route("/provider/{path:path}", methods=ALL_METHODS) async def proxy_provider_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"provider/{path}") @router.api_route("/provider", methods=ALL_METHODS) async def proxy_provider_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "provider") @router.api_route("/project", methods=ALL_METHODS) async def proxy_project_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "project") @router.api_route("/path", methods=ALL_METHODS) async def proxy_path_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "path") @router.api_route("/find/{path:path}", methods=ALL_METHODS) async def proxy_find_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"find/{path}") @router.api_route("/file", methods=ALL_METHODS) async def proxy_file_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "file") @router.api_route("/file/{path:path}", methods=ALL_METHODS) async def proxy_file_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"file/{path}") # Additional OpenCode API endpoints for root-path operation @router.api_route("/project/{path:path}", methods=ALL_METHODS) async def proxy_project_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"project/{path}") @router.api_route("/agent", methods=ALL_METHODS) async def proxy_agent_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "agent") @router.api_route("/agent/{path:path}", methods=ALL_METHODS) async def proxy_agent_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"agent/{path}") @router.api_route("/config", methods=ALL_METHODS) async def proxy_config_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "config") @router.api_route("/config/{path:path}", methods=ALL_METHODS) async def proxy_config_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"config/{path}") @router.api_route("/model", methods=ALL_METHODS) async def proxy_model_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "model") @router.api_route("/model/{path:path}", methods=ALL_METHODS) async def proxy_model_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"model/{path}") @router.api_route("/thread/{path:path}", methods=ALL_METHODS) async def proxy_thread_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"thread/{path}") @router.api_route("/chat/{path:path}", methods=ALL_METHODS) async def proxy_chat_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"chat/{path}") @router.api_route("/tree", methods=ALL_METHODS) async def proxy_tree_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "tree") @router.api_route("/tree/{path:path}", methods=ALL_METHODS) async def proxy_tree_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"tree/{path}") @router.api_route("/conversation", methods=ALL_METHODS) async def proxy_conversation_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "conversation") @router.api_route("/conversation/{path:path}", methods=ALL_METHODS) async def proxy_conversation_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"conversation/{path}") # OpenCode session and communication endpoints for message sending @router.api_route("/command", methods=ALL_METHODS) async def proxy_command_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "command") @router.api_route("/command/{path:path}", methods=ALL_METHODS) async def proxy_command_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"command/{path}") @router.api_route("/mcp", methods=ALL_METHODS) async def proxy_mcp_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "mcp") @router.api_route("/mcp/{path:path}", methods=ALL_METHODS) async def proxy_mcp_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"mcp/{path}") @router.api_route("/lsp", methods=ALL_METHODS) async def proxy_lsp_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "lsp") @router.api_route("/lsp/{path:path}", methods=ALL_METHODS) async def proxy_lsp_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"lsp/{path}") @router.api_route("/vcs", methods=ALL_METHODS) async def proxy_vcs_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "vcs") @router.api_route("/vcs/{path:path}", methods=ALL_METHODS) async def proxy_vcs_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"vcs/{path}") @router.api_route("/permission", methods=ALL_METHODS) async def proxy_permission_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "permission") @router.api_route("/permission/{path:path}", methods=ALL_METHODS) async def proxy_permission_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"permission/{path}") @router.api_route("/question", methods=ALL_METHODS) async def proxy_question_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "question") @router.api_route("/question/{path:path}", methods=ALL_METHODS) async def proxy_question_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"question/{path}") @router.api_route("/event", methods=ALL_METHODS) async def proxy_event_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "event") @router.api_route("/event/{path:path}", methods=ALL_METHODS) async def proxy_event_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"event/{path}") @router.api_route("/status", methods=ALL_METHODS) async def proxy_status_to_session(request: Request): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, "status") @router.api_route("/status/{path:path}", methods=ALL_METHODS) async def proxy_status_path_to_session(request: Request, path: str): session_id = get_session_from_cookie(request) return await proxy_to_session(request, session_id, f"status/{path}") # OpenCode internal session endpoint (different from our container sessions) # Must be defined BEFORE /session/{session_id}/{path} to match first @router.api_route("/session", methods=ALL_METHODS) async def proxy_internal_session_to_session(request: Request): session_id = get_session_from_cookie(request) # Proxy the request directly, preserving query params path = "session" return await proxy_to_session(request, session_id, path) @router.api_route("/session/{session_id}/{path:path}", methods=ALL_METHODS) async def proxy_to_session(request: Request, session_id: str, path: str): start_time = time.time() with RequestContext(): log_request( request.method, f"/session/{session_id}/{path}", 200, 0, operation="proxy_start", session_id=session_id, ) session = await session_manager.get_session(session_id) if not session or session.status != "running": duration_ms = (time.time() - start_time) * 1000 log_request( request.method, f"/session/{session_id}/{path}", 404, duration_ms, session_id=session_id, error="Session not found or not running", ) raise HTTPException( status_code=404, detail="Session not found or not running" ) docker_host = os.getenv("DOCKER_HOST", "http://docker-daemon:2375") parsed = urlparse(docker_host) container_host = parsed.hostname or "docker-daemon" container_url = f"http://{container_host}:{session.port}" url = f"{container_url}/{path}" if request.url.query: url += f"?{request.url.query}" body = await request.body() headers = dict(request.headers) headers.pop("host", None) if session.auth_token: headers["Authorization"] = f"Bearer {session.auth_token}" headers["X-Session-Token"] = session.auth_token headers["X-Session-ID"] = session.session_id try: log_session_operation( session_id, "proxy_request", method=request.method, path=path ) response = await make_http_request( method=request.method, url=url, headers=headers, content=body, ) duration_ms = (time.time() - start_time) * 1000 log_request( request.method, f"/session/{session_id}/{path}", response.status_code, duration_ms, session_id=session_id, operation="proxy_complete", ) log_security_event( "proxy_access", "info", session_id=session_id, method=request.method, path=path, status_code=response.status_code, ) content = response.content response_headers = dict(response.headers) content_type = response.headers.get("content-type", "") if "text/html" in content_type: try: html = content.decode("utf-8") session_prefix = f"/session/{session_id}" html = re.sub(r'src="/', f'src="{session_prefix}/', html) html = re.sub(r'href="/', f'href="{session_prefix}/', html) html = re.sub(r'content="/', f'content="{session_prefix}/', html) content = html.encode("utf-8") response_headers["content-length"] = str(len(content)) except UnicodeDecodeError: pass resp = Response( content=content, status_code=response.status_code, headers=response_headers, ) resp.set_cookie( key="lovdata_session", value=session_id, httponly=True, samesite="lax", max_age=86400, ) return resp except httpx.TimeoutException as e: duration_ms = (time.time() - start_time) * 1000 log_request( request.method, f"/session/{session_id}/{path}", 504, duration_ms, session_id=session_id, error="timeout", ) log_security_event( "proxy_timeout", "warning", session_id=session_id, method=request.method, path=path, error=str(e), ) raise HTTPException( status_code=504, detail="Request to session container timed out" ) except httpx.RequestError as e: duration_ms = (time.time() - start_time) * 1000 log_request( request.method, f"/session/{session_id}/{path}", 502, duration_ms, session_id=session_id, error=str(e), ) log_security_event( "proxy_connection_error", "error", session_id=session_id, method=request.method, path=path, error=str(e), ) raise HTTPException( status_code=502, detail=f"Failed to connect to session container: {str(e)}", )