22474cdba5
Backend (ruff): - Fix 106 errors: move imports to top of file (E402) - Remove unused imports (F401) - Add missing imports for undefined names (F821) - Remove unused variables (F841) - Fix test_models.py broken RefreshToken test - Fix test_projects_api.py missing TestClient import Frontend (eslint): - Remove unused imports/variables across 10 files - Fix explicit any types in client.ts and sessions.ts - Clean up empty block statements in terminal.tsx Quality gates: ruff (pass), eslint (pass), tsc --noEmit (pass), pytest (98 passed, 4 pre-existing failures)
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Instance proxy router for forwarding HTTP requests to running containers."""
|
|
|
|
import logging
|
|
import uuid
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
|
from src.models.tool_instance import ToolInstance
|
|
from src.models.tool_type import ToolType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/instances", tags=["instance-proxy"])
|
|
|
|
|
|
async def _proxy_request(
|
|
request: Request,
|
|
instance_id: uuid.UUID,
|
|
path: str,
|
|
user_id: uuid.UUID,
|
|
session: AsyncSession,
|
|
) -> Response:
|
|
"""Proxy an HTTP request to a running instance."""
|
|
instance = await session.get(ToolInstance, instance_id)
|
|
if instance is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="instance not found"
|
|
)
|
|
|
|
# Verify ownership
|
|
if instance.owner_id != user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="not authorized to access this instance",
|
|
)
|
|
|
|
if instance.status != "running" or not instance.container_name:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="instance is not running",
|
|
)
|
|
|
|
# Get the tool type to find the internal port
|
|
tool_type = await session.get(ToolType, instance.tool_type_id)
|
|
internal_port = tool_type.default_port if tool_type and tool_type.default_port else instance.port
|
|
|
|
# Build target URL using internal port
|
|
target_url = f"http://{instance.container_name}:{internal_port}"
|
|
if path:
|
|
target_url += f"/{path}"
|
|
|
|
# Get query string
|
|
query_string = str(request.query_params)
|
|
if query_string:
|
|
target_url += f"?{query_string}"
|
|
|
|
# Forward headers (excluding host and cookies)
|
|
headers: dict[str, str] = {}
|
|
for key, value in request.headers.items():
|
|
if key.lower() not in ("host", "cookie", "content-length"):
|
|
headers[key] = value
|
|
|
|
# Forward the request
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
body = await request.body()
|
|
response = await client.request(
|
|
method=request.method,
|
|
url=target_url,
|
|
headers=headers,
|
|
content=body,
|
|
follow_redirects=False,
|
|
timeout=30.0,
|
|
)
|
|
except Exception as exc:
|
|
logger.error("Proxy error to %s: %s", target_url, exc)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"failed to reach instance: {exc}",
|
|
)
|
|
|
|
# Build response
|
|
response_headers = dict(response.headers)
|
|
# Remove hop-by-hop headers
|
|
for header in ("content-encoding", "transfer-encoding", "connection"):
|
|
response_headers.pop(header, None)
|
|
|
|
return Response(
|
|
content=response.content,
|
|
status_code=response.status_code,
|
|
headers=response_headers,
|
|
)
|
|
|
|
|
|
@router.get("/{instance_id}/proxy/{path:path}")
|
|
@router.post("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.put("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.delete("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.patch("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.head("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.options("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
async def proxy_to_instance(
|
|
request: Request,
|
|
instance_id: uuid.UUID,
|
|
path: str = "",
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Response:
|
|
"""Proxy requests to a running tool instance.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
instance_id: UUID of the instance.
|
|
path: The path to proxy to the instance.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
Response from the proxied instance.
|
|
"""
|
|
return await _proxy_request(request, instance_id, path, user_id, session)
|