mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(nginx): canonicalize the loopback origin onto localhost localhost and 127.0.0.1 (or [::1]) are the same loopback listener but separate cookie jars to a browser, so an admin who sets up on one spelling lands on /login when they later open the other, with no hint why (#6156). Cookies cannot bridge the gap: no Domain attribute spans both hosts. All three nginx configs now map the numeric spellings to "localhost", the name every banner and doc already prints, and the main server answers a 301 to the same URL on the canonical host. The published port survives because the map keys on $http_host and reattaches the captured port; Docker's arbitrary host-port mapping is unaffected. Safe methods only: GET/HEAD redirect, while API POSTs pass through untouched, since a redirected POST strands clients that do not re-send bodies. Non-loopback Hosts (LAN names, ingress domains) never match, and the canonical target itself is not a match, so there is no redirect loop. The new suite parses both maps out of each config and evaluates the full method x host matrix against nginx semantics, including the suffix-spoof and redirect-loop cases, and pins the maps identical across the Docker, local-dev, and Helm configs so they cannot drift. Fixes #6156 Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> * docs: keep the loopback note in the nginx configs The root AGENTS.md paragraph pushed the middlewares and sandbox instruction chains over the 96 KiB hard limit. The nginx configs already carry the full explanation next to the maps, so drop the duplicated paragraph instead of shrinking the budgets. * fix(nginx): let WebSocket upgrades bypass the loopback canonical redirect willem-bd caught this running the stack locally: the canonicalization map keyed on method:host alone, so a GET with an Upgrade header got the same 301 as a plain GET. WebSocket clients do not follow redirects, so every WS handshake against a 127.0.0.1/[::1] spelling died at the redirect. Key the origin map on method:upgrade:host instead. Handshakes carry a non-empty Upgrade value, fall off the GET::/HEAD:: patterns, and proxy through untouched. Plain GET/HEAD behavior is unchanged. --------- Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
384 lines
16 KiB
Nginx Configuration File
384 lines
16 KiB
Nginx Configuration File
events {
|
|
worker_connections 1024;
|
|
}
|
|
pid /tmp/nginx.pid;
|
|
http {
|
|
# Basic settings
|
|
sendfile on;
|
|
tcp_nopush on;
|
|
tcp_nodelay on;
|
|
keepalive_timeout 65;
|
|
types_hash_max_size 2048;
|
|
|
|
# Compress textual delivery only. SSE and already-compressed media are
|
|
# deliberately absent because buffering/compression hurts their latency.
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
gzip_min_length 1024;
|
|
gzip_comp_level 5;
|
|
gzip_types
|
|
text/css
|
|
text/javascript
|
|
application/javascript
|
|
application/json
|
|
application/xml
|
|
image/svg+xml;
|
|
|
|
# Logging
|
|
access_log /dev/stdout;
|
|
error_log /dev/stderr;
|
|
|
|
# Docker internal DNS (for resolving k3s hostname)
|
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
|
|
# Preserve an upstream proxy's X-Forwarded-Proto so the Gateway sees the real
|
|
# client scheme when DeerFlow's nginx runs BEHIND another TLS-terminating
|
|
# reverse proxy (e.g. Pangolin/Traefik, Cloudflare, Caddy). Without this the
|
|
# Gateway treats HTTPS browser traffic as HTTP and rejects the login POST with
|
|
# 403 "Cross-site auth request denied" (Origin scheme mismatch), and also drops
|
|
# the Secure flag / max-age on session cookies. Falls back to $scheme when nginx
|
|
# is itself the TLS edge (standalone `make dev` / Docker), so default
|
|
# deployments are unchanged.
|
|
map $http_x_forwarded_proto $forwarded_proto {
|
|
default $scheme;
|
|
"~*^https" https;
|
|
}
|
|
|
|
# Only mark frontend requests as connection upgrades when the browser
|
|
# actually requested one. Next.js dev HMR also uses long-lived HTTP
|
|
# streams, and forcing "Connection: upgrade" on those requests makes
|
|
# nginx treat the upstream response as an invalid upgrade handshake.
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' '';
|
|
}
|
|
|
|
# Loopback has two spellings and browsers scope cookies per host, so
|
|
# serving both "localhost" and "127.0.0.1" (or "[::1]") splits the login
|
|
# session into separate jars (#6156). Canonicalize on "localhost", the
|
|
# spelling every banner and doc prints, keeping the caller's port.
|
|
map $http_host $loopback_canonical_host {
|
|
default "";
|
|
"127.0.0.1" "localhost";
|
|
"~*^127\.0\.0\.1:(?<loopback_port>\d+)$" "localhost:$loopback_port";
|
|
"[::1]" "localhost";
|
|
"~*^\[::1\]:(?<loopback_port>\d+)$" "localhost:$loopback_port";
|
|
}
|
|
|
|
# Safe methods only: redirecting an API POST would strand clients that
|
|
# do not re-send bodies across the redirect. A GET carrying an Upgrade
|
|
# header (a WebSocket handshake) must proxy straight through instead:
|
|
# WebSocket clients never follow the 301.
|
|
map $request_method:$http_upgrade:$loopback_canonical_host $loopback_origin {
|
|
default "";
|
|
"~*^GET::." $loopback_canonical_host;
|
|
"~*^HEAD::." $loopback_canonical_host;
|
|
}
|
|
|
|
# ── Main server (path-based routing) ─────────────────────────────────
|
|
server {
|
|
listen 2026 default_server;
|
|
listen [::]:2026 default_server;
|
|
server_name _;
|
|
|
|
# Canonicalize the loopback origin (#6156); see the maps above.
|
|
if ($loopback_origin != "") {
|
|
return 301 $scheme://$loopback_origin$request_uri;
|
|
}
|
|
|
|
# Resolve Docker service names at request time to avoid stale upstream
|
|
# IPs when containers restart and receive new addresses.
|
|
set $gateway_upstream gateway:8001;
|
|
set $frontend_upstream frontend:3000;
|
|
|
|
# Default proxy settings for all locations (streaming/SSE support)
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
|
|
# Keep the unified nginx endpoint same-origin by default. When split
|
|
# frontend/backend or port-forwarded deployments need browser CORS,
|
|
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
|
|
# CSRF origin checks stay aligned instead of approving every origin at
|
|
# the proxy layer.
|
|
|
|
# LangGraph-compatible API routes served by Gateway.
|
|
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
|
location /api/langgraph/ {
|
|
rewrite ^/api/langgraph/(.*) /api/$1 break;
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
proxy_set_header Connection '';
|
|
|
|
# SSE/Streaming support
|
|
proxy_set_header X-Accel-Buffering no;
|
|
|
|
# Long chat/text-prompt support (issue #3952): a sufficiently long
|
|
# pasted prompt otherwise exceeds nginx's default 1m
|
|
# client_max_body_size, or gets spooled to a temp file via
|
|
# proxy_request_buffering before reaching Gateway -- on a non-root
|
|
# local run that temp directory may not be writable, producing a
|
|
# raw nginx 500 instead of a graceful application error. Mirrors
|
|
# the uploads location's fix below (same mechanism, sized for text
|
|
# prompts rather than binary file uploads).
|
|
client_max_body_size 20M;
|
|
proxy_request_buffering off;
|
|
|
|
# Timeouts for long-running requests
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Chunked transfer encoding
|
|
chunked_transfer_encoding on;
|
|
}
|
|
|
|
# Custom API: Models endpoint
|
|
location /api/models {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Custom API: Memory endpoint
|
|
location /api/memory {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Custom API: MCP configuration endpoint
|
|
location /api/mcp {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Admin-only local .skill archive upload from Settings.
|
|
location = /api/skills/install/upload {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
client_max_body_size 101M;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Skills configuration endpoint
|
|
location /api/skills {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
# Installing a .skill archive runs one LLM security scan per file, and a
|
|
# custom-skill edit or rollback runs one more; none sets its own timeout.
|
|
# The upload endpoint above already allows 600s for the same work.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Agents endpoint
|
|
location /api/agents {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Custom API: Uploads endpoint
|
|
location ~ ^/api/threads/[^/]+/uploads {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
# Large file upload support
|
|
client_max_body_size 100M;
|
|
proxy_request_buffering off;
|
|
|
|
# Disable response buffering to avoid permission errors
|
|
}
|
|
|
|
# Custom API: Project documents (multipart upload, promote-from-thread,
|
|
# attach-to-thread, content download, delete). Without its own location
|
|
# the upload fell through to /api/ and nginx's 1m default rejected
|
|
# anything larger with a 413 before Gateway saw it, although Gateway
|
|
# allows uploads.max_file_size (50 MiB by default) on this route. Same
|
|
# ceiling and streaming settings as the thread uploads route above; the
|
|
# read timeout keeps what /api/ already granted these routes.
|
|
location ~ ^/api/projects/[^/]+/documents {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
client_max_body_size 100M;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Live browser stream is a WebSocket upgrade. It must be matched before
|
|
# the generic /api/threads regex below (which omits Upgrade/Connection
|
|
# forwarding and would downgrade it to plain HTTP).
|
|
location ~ ^/api/threads/[^/]+/browser/stream {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Long-lived Live stream: keep the upgraded connection open.
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Other endpoints under /api/threads
|
|
location ~ ^/api/threads {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
# /compact and /suggestions hold the response open for a model call,
|
|
# /runs/wait for a whole run. nginx's 60s default would 504 them mid-work:
|
|
# the compaction still commits, and the waited run is cancelled.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# API Documentation: Swagger UI
|
|
location /docs {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# API Documentation: ReDoc
|
|
location /redoc {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# API Documentation: OpenAPI Schema
|
|
location /openapi.json {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Health check endpoint (gateway)
|
|
location /health {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# ── Provisioner API (sandbox management) ────────────────────────
|
|
# Use a variable so nginx resolves provisioner at request time (not startup).
|
|
# This allows nginx to start even when provisioner container is not running.
|
|
location /api/sandboxes {
|
|
set $provisioner_upstream provisioner:8002;
|
|
proxy_pass http://$provisioner_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
}
|
|
|
|
# Catch-all for /api/ routes not covered above (e.g. /api/v1/auth/*).
|
|
# More specific prefix and regex locations above still take precedence.
|
|
location /api/ {
|
|
proxy_pass http://$gateway_upstream;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
|
|
# Disable buffering to avoid permission errors when nginx
|
|
# runs as a non-root user (e.g. local development).
|
|
|
|
# /api/runs/wait blocks on a whole run and /api/input-polish on a model
|
|
# call. nginx's 60s default would 504 them mid-work, and the waited run is
|
|
# cancelled on the disconnect.
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# All other requests go to frontend
|
|
location / {
|
|
proxy_pass http://$frontend_upstream;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
}
|
|
}
|