Added proxy routes for all OpenCode internal API endpoints to support full application functionality when accessed via session manager: - project, agent, config, model endpoints - thread, chat, conversation endpoints - command, mcp, lsp, vcs endpoints - permission, question, event, status endpoints - internal session endpoint (distinct from container sessions) Also updated Caddyfile for routing configuration.
428 lines
15 KiB
Python
428 lines
15 KiB
Python
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)}",
|
|
)
|