From c3bc6c7cd5f0208464301f2f4c772d956920b2dd Mon Sep 17 00:00:00 2001 From: AochenShen99 <142667174+ShenAC-SAC@users.noreply.github.com> Date: Mon, 11 May 2026 17:38:37 +0800 Subject: [PATCH] fix(nginx): defer CORS to gateway allowlist (#2861) * fix(nginx): defer cors to gateway allowlist Remove proxy-level wildcard CORS handling so browser origins are controlled by the Gateway allowlist and stay aligned with CSRF origin checks. * docs: document gateway cors allowlist Clarify that same-origin nginx access needs no CORS headers while split-origin or port-forwarded browser clients must opt in with GATEWAY_CORS_ORIGINS. * docs(gateway): record cors source of truth Document that Gateway CORSMiddleware and CSRFMiddleware share GATEWAY_CORS_ORIGINS as the split-origin source of truth. * fix(gateway): align cors origin normalization * docs: clarify gateway langgraph routing * docs(gateway): update runtime routing note --- .env.example | 5 +- CONTRIBUTING.md | 32 +++++-------- README.md | 2 + backend/CLAUDE.md | 4 +- backend/README.md | 41 ++++++++-------- backend/app/gateway/app.py | 48 +++++++++---------- backend/app/gateway/config.py | 3 -- backend/app/gateway/csrf_middleware.py | 9 +++- backend/docs/API.md | 30 +++++------- backend/docs/ARCHITECTURE.md | 20 ++++---- backend/tests/test_gateway_docs_toggle.py | 42 ++++++++++++++++ backend/tests/test_gateway_runtime_cleanup.py | 23 +++++++++ docker/nginx/nginx.conf | 20 ++------ docker/nginx/nginx.local.conf | 20 ++------ 14 files changed, 169 insertions(+), 130 deletions(-) diff --git a/.env.example b/.env.example index a859ec2a5..43290954b 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,9 @@ JINA_API_KEY=your-jina-api-key # InfoQuest API Key INFOQUEST_API_KEY=your-infoquest-api-key -# CORS Origins (comma-separated) - e.g., http://localhost:3000,http://localhost:3001 -# CORS_ORIGINS=http://localhost:3000 +# Browser CORS allowlist for split-origin or port-forwarded deployments (comma-separated exact origins). +# Leave unset when using the unified nginx endpoint, e.g. http://localhost:2026. +# GATEWAY_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 # Optional: # FIRECRAWL_API_KEY=your-firecrawl-api-key diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7cb2840b..51b834b4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,12 +46,12 @@ Docker provides a consistent, isolated environment with all dependencies pre-con All services will start with hot-reload enabled: - Frontend changes are automatically reloaded - Backend changes trigger automatic restart - - LangGraph server supports hot-reload + - Gateway-hosted LangGraph-compatible runtime supports hot-reload 4. **Access the application**: - Web Interface: http://localhost:2026 - API Gateway: http://localhost:2026/api/* - - LangGraph: http://localhost:2026/api/langgraph/* + - LangGraph-compatible API: http://localhost:2026/api/langgraph/* #### Docker Commands @@ -94,7 +94,7 @@ Use these as practical starting points for development and review environments: If `make docker-init`, `make docker-start`, or `make docker-stop` fails on Linux with an error like below, your current user likely does not have permission to access the Docker daemon socket: ```text -unable to get image 'deer-flow-dev-langgraph': permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock +unable to get image 'deer-flow-gateway': permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock ``` Recommended fix: add your current user to the `docker` group so Docker commands work without `sudo`. @@ -131,9 +131,8 @@ Host Machine Docker Compose (deer-flow-dev) ├→ nginx (port 2026) ← Reverse proxy ├→ web (port 3000) ← Frontend with hot-reload - ├→ api (port 8001) ← Gateway API with hot-reload - ├→ langgraph (port 2024) ← LangGraph server with hot-reload - └→ provisioner (optional, port 8002) ← Started only in provisioner/K8s sandbox mode + ├→ gateway (port 8001) ← Gateway API + LangGraph-compatible runtime with hot-reload + └→ provisioner (optional, port 8002) ← Started only in provisioner/K8s sandbox mode ``` **Benefits of Docker Development**: @@ -184,17 +183,13 @@ Required tools: If you need to start services individually: -1. **Start backend services**: +1. **Start backend service**: ```bash - # Terminal 1: Start LangGraph Server (port 2024) - cd backend - make dev - - # Terminal 2: Start Gateway API (port 8001) + # Terminal 1: Start Gateway API and embedded LangGraph-compatible runtime (port 8001) cd backend make gateway - # Terminal 3: Start Frontend (port 3000) + # Terminal 2: Start Frontend (port 3000) cd frontend pnpm dev ``` @@ -212,10 +207,10 @@ If you need to start services individually: The nginx configuration provides: - Unified entry point on port 2026 -- Routes `/api/langgraph/*` to LangGraph Server (2024) +- Gateway owns `/api/langgraph/*` and translates those public LangGraph-compatible paths to its native `/api/*` routers behind nginx - Routes other `/api/*` endpoints to Gateway API (8001) - Routes non-API requests to Frontend (3000) -- Centralized CORS handling +- Same-origin API routing; split-origin or port-forwarded browser clients should use the Gateway `GATEWAY_CORS_ORIGINS` allowlist - SSE/streaming support for real-time agent responses - Optimized timeouts for long-running operations @@ -235,8 +230,8 @@ deer-flow/ │ └── nginx.local.conf # Nginx config for local dev ├── backend/ # Backend application │ ├── src/ -│ │ ├── gateway/ # Gateway API (port 8001) -│ │ ├── agents/ # LangGraph agents (port 2024) +│ │ ├── gateway/ # Gateway API and LangGraph-compatible runtime (port 8001) +│ │ ├── agents/ # LangGraph agent definitions │ │ ├── mcp/ # Model Context Protocol integration │ │ ├── skills/ # Skills system │ │ └── sandbox/ # Sandbox execution @@ -256,8 +251,7 @@ Browser ↓ Nginx (port 2026) ← Unified entry point ├→ Frontend (port 3000) ← / (non-API requests) - ├→ Gateway API (port 8001) ← /api/models, /api/mcp, /api/skills, /api/threads/*/artifacts - └→ LangGraph Server (port 2024) ← /api/langgraph/* (agent interactions) + └→ Gateway API (port 8001) ← /api/* and /api/langgraph/* (LangGraph-compatible agent interactions) ``` ## Development Workflow diff --git a/README.md b/README.md index 0fc8f173e..9ff1d501b 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,8 @@ make down # Stop and remove containers Access: http://localhost:2026 +The unified nginx endpoint is same-origin by default and does not emit browser CORS headers. If you run a split-origin or port-forwarded browser client, set `GATEWAY_CORS_ORIGINS` to comma-separated exact origins such as `http://localhost:3000`; the Gateway then applies the CORS allowlist and matching CSRF origin checks. + See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed Docker development guide. #### Option 2: Local Development diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 99922a61e..67ee9cc7e 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -207,6 +207,8 @@ Configuration priority: FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). +CORS is same-origin by default when requests enter through nginx on port 2026. Split-origin or port-forwarded browser clients must opt in with `GATEWAY_CORS_ORIGINS` (comma-separated exact origins); Gateway `CORSMiddleware` and `CSRFMiddleware` both read that variable so browser CORS and auth-origin checks stay aligned. + **Routers**: | Router | Endpoints | @@ -223,7 +225,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA | **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | | **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → Gateway. +Proxied through nginx: `/api/langgraph/*` → Gateway LangGraph-compatible runtime, all other `/api/*` → Gateway REST APIs. ### Sandbox System (`packages/harness/deerflow/sandbox/`) diff --git a/backend/README.md b/backend/README.md index 6295eba22..9b4d26fb1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -14,28 +14,31 @@ DeerFlow is a LangGraph-based AI super agent with sandbox execution, persistent │ │ /api/langgraph/* │ │ /api/* (other) ▼ ▼ - ┌────────────────────┐ ┌────────────────────────┐ - │ LangGraph Server │ │ Gateway API (8001) │ - │ (Port 2024) │ │ FastAPI REST │ - │ │ │ │ - │ ┌────────────────┐ │ │ Models, MCP, Skills, │ - │ │ Lead Agent │ │ │ Memory, Uploads, │ - │ │ ┌──────────┐ │ │ │ Artifacts │ - │ │ │Middleware│ │ │ └────────────────────────┘ - │ │ │ Chain │ │ │ - │ │ └──────────┘ │ │ - │ │ ┌──────────┐ │ │ - │ │ │ Tools │ │ │ - │ │ └──────────┘ │ │ - │ │ ┌──────────┐ │ │ - │ │ │Subagents │ │ │ - │ │ └──────────┘ │ │ - │ └────────────────┘ │ - └────────────────────┘ + ┌──────────────────────────────────────────────┐ + │ Gateway API (8001) │ + │ FastAPI REST + LangGraph-compatible runtime │ + │ │ + │ Models, MCP, Skills, Memory, Uploads, │ + │ Artifacts, Threads, Runs, Streaming │ + │ │ + │ ┌────────────────┐ │ + │ │ Lead Agent │ │ + │ │ ┌──────────┐ │ │ + │ │ │Middleware│ │ │ + │ │ │ Chain │ │ │ + │ │ └──────────┘ │ │ + │ │ ┌──────────┐ │ │ + │ │ │ Tools │ │ │ + │ │ └──────────┘ │ │ + │ │ ┌──────────┐ │ │ + │ │ │Subagents │ │ │ + │ │ └──────────┘ │ │ + │ └────────────────┘ │ + └──────────────────────────────────────────────┘ ``` **Request Routing** (via Nginx): -- `/api/langgraph/*` → LangGraph Server - agent interactions, threads, streaming +- `/api/langgraph/*` → Gateway API - LangGraph-compatible agent interactions, threads, runs, and streaming translated to native `/api/*` routers - `/api/*` (other) → Gateway API - models, MCP, skills, memory, artifacts, uploads, thread-local cleanup - `/` (non-API) → Frontend - Next.js web interface diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 2a506df2b..8848f473e 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -1,6 +1,5 @@ import asyncio import logging -import os from collections.abc import AsyncGenerator from contextlib import asynccontextmanager @@ -9,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.gateway.auth_middleware import AuthMiddleware from app.gateway.config import get_gateway_config -from app.gateway.csrf_middleware import CSRFMiddleware +from app.gateway.csrf_middleware import CSRFMiddleware, get_configured_cors_origins from app.gateway.deps import langgraph_runtime from app.gateway.routers import ( agents, @@ -219,7 +218,9 @@ def create_app() -> FastAPI: Configured FastAPI application instance. """ config = get_gateway_config() - docs_kwargs = {"docs_url": "/docs", "redoc_url": "/redoc", "openapi_url": "/openapi.json"} if config.enable_docs else {"docs_url": None, "redoc_url": None, "openapi_url": None} + docs_url = "/docs" if config.enable_docs else None + redoc_url = "/redoc" if config.enable_docs else None + openapi_url = "/openapi.json" if config.enable_docs else None app = FastAPI( title="DeerFlow API Gateway", @@ -239,12 +240,14 @@ API Gateway for DeerFlow - A LangGraph-based AI agent backend with sandbox execu ### Architecture -LangGraph requests are handled by nginx reverse proxy. -This gateway provides custom endpoints for models, MCP configuration, skills, and artifacts. +LangGraph-compatible requests are routed through nginx to this gateway. +This gateway provides runtime endpoints for agent runs plus custom endpoints for models, MCP configuration, skills, and artifacts. """, version="0.1.0", lifespan=lifespan, - **docs_kwargs, + docs_url=docs_url, + redoc_url=redoc_url, + openapi_url=openapi_url, openapi_tags=[ { "name": "models", @@ -307,25 +310,18 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an # CSRF: Double Submit Cookie pattern for state-changing requests app.add_middleware(CSRFMiddleware) - # CORS: when GATEWAY_CORS_ORIGINS is set (dev without nginx), add CORS middleware. - # In production, nginx handles CORS and no middleware is needed. - cors_origins_env = os.environ.get("GATEWAY_CORS_ORIGINS", "") - if cors_origins_env: - cors_origins = [o.strip() for o in cors_origins_env.split(",") if o.strip()] - # Validate: wildcard origin with credentials is a security misconfiguration - for origin in cors_origins: - if origin == "*": - logger.error("GATEWAY_CORS_ORIGINS contains wildcard '*' with allow_credentials=True. This is a security misconfiguration — browsers will reject the response. Use explicit scheme://host:port origins instead.") - cors_origins = [o for o in cors_origins if o != "*"] - break - if cors_origins: - app.add_middleware( - CORSMiddleware, - allow_origins=cors_origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) + # CORS: the unified nginx endpoint is same-origin by default. Split-origin + # browser clients must opt in with this explicit Gateway allowlist so CORS + # and CSRF origin checks share the same source of truth. + cors_origins = sorted(get_configured_cors_origins()) + if cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) # Include routers # Models API is mounted at /api/models @@ -374,7 +370,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an app.include_router(runs.router) @app.get("/health", tags=["health"]) - async def health_check() -> dict: + async def health_check() -> dict[str, str]: """Health check endpoint. Returns: diff --git a/backend/app/gateway/config.py b/backend/app/gateway/config.py index 95221dad2..06a7d5b1a 100644 --- a/backend/app/gateway/config.py +++ b/backend/app/gateway/config.py @@ -8,7 +8,6 @@ class GatewayConfig(BaseModel): host: str = Field(default="0.0.0.0", description="Host to bind the gateway server") port: int = Field(default=8001, description="Port to bind the gateway server") - cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:3000"], description="Allowed CORS origins") enable_docs: bool = Field(default=True, description="Enable Swagger/ReDoc/OpenAPI endpoints") @@ -19,11 +18,9 @@ def get_gateway_config() -> GatewayConfig: """Get gateway config, loading from environment if available.""" global _gateway_config if _gateway_config is None: - cors_origins_str = os.getenv("CORS_ORIGINS", "http://localhost:3000") _gateway_config = GatewayConfig( host=os.getenv("GATEWAY_HOST", "0.0.0.0"), port=int(os.getenv("GATEWAY_PORT", "8001")), - cors_origins=cors_origins_str.split(","), enable_docs=os.getenv("GATEWAY_ENABLE_DOCS", "true").lower() == "true", ) return _gateway_config diff --git a/backend/app/gateway/csrf_middleware.py b/backend/app/gateway/csrf_middleware.py index 08e95be4b..f34882032 100644 --- a/backend/app/gateway/csrf_middleware.py +++ b/backend/app/gateway/csrf_middleware.py @@ -6,7 +6,7 @@ State-changing operations require CSRF protection. import os import secrets -from collections.abc import Callable +from collections.abc import Awaitable, Callable from urllib.parse import urlsplit from fastapi import Request, Response @@ -106,6 +106,11 @@ def _configured_cors_origins() -> set[str]: return origins +def get_configured_cors_origins() -> set[str]: + """Return normalized explicit browser origins from GATEWAY_CORS_ORIGINS.""" + return _configured_cors_origins() + + def _first_header_value(value: str | None) -> str | None: """Return the first value from a comma-separated proxy header.""" if not value: @@ -172,7 +177,7 @@ class CSRFMiddleware(BaseHTTPMiddleware): def __init__(self, app: ASGIApp) -> None: super().__init__(app) - async def dispatch(self, request: Request, call_next: Callable) -> Response: + async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response: _is_auth = is_auth_endpoint(request) if should_check_csrf(request) and _is_auth and not is_allowed_auth_origin(request): diff --git a/backend/docs/API.md b/backend/docs/API.md index dcefe6779..293c1ebd1 100644 --- a/backend/docs/API.md +++ b/backend/docs/API.md @@ -6,16 +6,16 @@ This document provides a complete reference for the DeerFlow backend APIs. DeerFlow backend exposes two sets of APIs: -1. **LangGraph API** - Agent interactions, threads, and streaming (`/api/langgraph/*`) +1. **LangGraph-compatible API** - Agent interactions, threads, and streaming (`/api/langgraph/*`) 2. **Gateway API** - Models, MCP, skills, uploads, and artifacts (`/api/*`) All APIs are accessed through the Nginx reverse proxy at port 2026. -## LangGraph API +## LangGraph-compatible API Base URL: `/api/langgraph` -The LangGraph API is provided by the LangGraph server and follows the LangGraph SDK conventions. +The public LangGraph-compatible API follows LangGraph SDK conventions. In the unified nginx deployment, Gateway owns `/api/langgraph/*` and translates those paths to its native `/api/*` run, thread, and streaming routers. ### Threads @@ -104,17 +104,11 @@ Content-Type: application/json **Recursion Limit:** `config.recursion_limit` caps the number of graph steps LangGraph will execute -in a single run. The `/api/langgraph/*` endpoints go straight to the LangGraph -server and therefore inherit LangGraph's native default of **25**, which is -too low for plan-mode or subagent-heavy runs — the agent typically errors out -with `GraphRecursionError` after the first round of subagent results comes -back, before the lead agent can synthesize the final answer. - -DeerFlow's own Gateway and IM-channel paths mitigate this by defaulting to -`100` in `build_run_config` (see `backend/app/gateway/services.py`), but -clients calling the LangGraph API directly must set `recursion_limit` -explicitly in the request body. `100` matches the Gateway default and is a -safe starting point; increase it if you run deeply nested subagent graphs. +in a single run. The unified Gateway path defaults to `100` in +`build_run_config` (see `backend/app/gateway/services.py`), which is a safer +starting point for plan-mode or subagent-heavy runs. Clients can still set +`recursion_limit` explicitly in the request body; increase it if you run deeply +nested subagent graphs. **Configurable Options:** - `model_name` (string): Override the default model @@ -649,7 +643,7 @@ curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs \ }' ``` -> The `/api/langgraph/*` endpoints bypass DeerFlow's Gateway and inherit -> LangGraph's native `recursion_limit` default of 25, which is too low for -> plan-mode or subagent runs. Set `config.recursion_limit` explicitly — see -> the [Create Run](#create-run) section for details. +> The unified Gateway path defaults `config.recursion_limit` to 100 for +> plan-mode and subagent-heavy runs. Clients may still set +> `config.recursion_limit` explicitly — see the [Create Run](#create-run) +> section for details. diff --git a/backend/docs/ARCHITECTURE.md b/backend/docs/ARCHITECTURE.md index cc0993f7f..e6fdbe217 100644 --- a/backend/docs/ARCHITECTURE.md +++ b/backend/docs/ARCHITECTURE.md @@ -14,8 +14,8 @@ This document provides a comprehensive overview of the DeerFlow backend architec │ Nginx (Port 2026) │ │ Unified Reverse Proxy Entry Point │ │ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ /api/langgraph/* → LangGraph Server (2024) │ │ -│ │ /api/* → Gateway API (8001) │ │ +│ │ /api/langgraph/* → Gateway LangGraph-compatible runtime (8001) │ │ +│ │ /api/* → Gateway REST APIs (8001) │ │ │ │ /* → Frontend (3000) │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────┬────────────────────────────────────────┘ @@ -24,8 +24,8 @@ This document provides a comprehensive overview of the DeerFlow backend architec │ │ │ ▼ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ -│ LangGraph Server │ │ Gateway API │ │ Frontend │ -│ (Port 2024) │ │ (Port 8001) │ │ (Port 3000) │ +│ Embedded Runtime │ │ Gateway API │ │ Frontend │ +│ (inside Gateway) │ │ (Port 8001) │ │ (Port 3000) │ │ │ │ │ │ │ │ - Agent Runtime │ │ - Models API │ │ - Next.js App │ │ - Thread Mgmt │ │ - MCP Config │ │ - React UI │ @@ -52,9 +52,9 @@ This document provides a comprehensive overview of the DeerFlow backend architec ## Component Details -### LangGraph Server +### Embedded LangGraph Runtime -The LangGraph server is the core agent runtime, built on LangGraph for robust multi-agent workflow orchestration. +The LangGraph-compatible runtime runs inside the Gateway process and is built on LangGraph for robust multi-agent workflow orchestration. **Entry Point**: `packages/harness/deerflow/agents/lead_agent/agent.py:make_lead_agent` @@ -78,7 +78,7 @@ The LangGraph server is the core agent runtime, built on LangGraph for robust mu ### Gateway API -FastAPI application providing REST endpoints for non-agent operations. +FastAPI application providing REST endpoints plus the public LangGraph-compatible `/api/langgraph/*` runtime routes. **Entry Point**: `app/gateway/app.py` @@ -353,10 +353,10 @@ SKILL.md Format: POST /api/langgraph/threads/{thread_id}/runs {"input": {"messages": [{"role": "user", "content": "Hello"}]}} -2. Nginx → LangGraph Server (2024) - Proxied to LangGraph server +2. Nginx → Gateway API (8001) + Routes `/api/langgraph/*` to the Gateway's LangGraph-compatible runtime -3. LangGraph Server +3. Embedded LangGraph runtime a. Load/create thread state b. Execute middleware chain: - ThreadDataMiddleware: Set up paths diff --git a/backend/tests/test_gateway_docs_toggle.py b/backend/tests/test_gateway_docs_toggle.py index 54392ee2e..372f93e18 100644 --- a/backend/tests/test_gateway_docs_toggle.py +++ b/backend/tests/test_gateway_docs_toggle.py @@ -122,3 +122,45 @@ def test_health_still_works_when_docs_disabled(): resp = client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "healthy" + + +# --------------------------------------------------------------------------- +# Runtime CORS behavior +# --------------------------------------------------------------------------- + + +def _make_gateway_client(cors_origins: str) -> TestClient: + with patch.dict(os.environ, {"GATEWAY_CORS_ORIGINS": cors_origins}): + _reset_gateway_config() + from app.gateway.app import create_app + + return TestClient(create_app()) + + +def test_gateway_cors_allows_configured_origin(): + """GATEWAY_CORS_ORIGINS should control actual browser CORS responses.""" + client = _make_gateway_client("https://app.example") + + response = client.get("/health", headers={"Origin": "https://app.example"}) + + assert response.status_code == 200 + assert response.headers["access-control-allow-origin"] == "https://app.example" + assert response.headers["access-control-allow-credentials"] == "true" + + +def test_gateway_cors_rejects_unconfigured_origin(): + client = _make_gateway_client("https://app.example") + + response = client.get("/health", headers={"Origin": "https://evil.example"}) + + assert response.status_code == 200 + assert "access-control-allow-origin" not in response.headers + + +def test_gateway_cors_normalizes_configured_default_port(): + client = _make_gateway_client("https://app.example:443") + + response = client.get("/health", headers={"Origin": "https://app.example"}) + + assert response.status_code == 200 + assert response.headers["access-control-allow-origin"] == "https://app.example" diff --git a/backend/tests/test_gateway_runtime_cleanup.py b/backend/tests/test_gateway_runtime_cleanup.py index 3bf7c1a5b..895e04885 100644 --- a/backend/tests/test_gateway_runtime_cleanup.py +++ b/backend/tests/test_gateway_runtime_cleanup.py @@ -53,6 +53,29 @@ def test_nginx_routes_official_langgraph_prefix_to_gateway_api(): assert "proxy_pass http://gateway" in content or "proxy_pass http://$gateway_upstream" in content +def test_nginx_defers_cors_to_gateway_allowlist(): + for path in ("docker/nginx/nginx.local.conf", "docker/nginx/nginx.conf"): + content = _read(path) + + assert "Access-Control-Allow-Origin" not in content + assert "Access-Control-Allow-Methods" not in content + assert "Access-Control-Allow-Headers" not in content + assert "Access-Control-Allow-Credentials" not in content + assert "proxy_hide_header 'Access-Control-Allow-" not in content + assert "if ($request_method = 'OPTIONS')" not in content + + +def test_gateway_cors_configuration_uses_gateway_allowlist(): + gateway_config = _read("backend/app/gateway/config.py") + gateway_app = _read("backend/app/gateway/app.py") + csrf_middleware = _read("backend/app/gateway/csrf_middleware.py") + + assert not re.search(r"(?