From 58313169c23804ac1db4b8a50f9b6a02475435a7 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Wed, 1 Jul 2026 20:59:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(stage):=20nginx=20=E5=8A=A0=E5=85=A7?= =?UTF-8?q?=E7=B6=B2=20IP=20=E7=9B=B4=E9=80=A3=20server=20block=EF=BC=88de?= =?UTF-8?q?mo=20=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent 本機 demo 需直連內網 192.168.0.130:9527 建 tunnel(繞過公網邊界 proxy — 那台剝 WS upgrade header、碰不到)。但直連 IP 時 Host=192.168.0.130 不命中 stage-9527 白名單被 444 擋。 加一個 server_name 192.168.0.130 的 server block(複製 stage-9527 block、 XFP 改 http),8 個 location 全對齊含 /tunnel/connect WS upgrade。現有兩 block(default 白名單 + stage-9527)一字未動。 安全:公網經 proxy 進來 Host 固定 stage-9527、偽造 Host 打不進;IP block 只內網可達;default 444 白名單未削弱。⚠️ demo 專用、正式上線前應移除或 加 allow/deny(註解已標)。 搭配 .env.stage VISIONA_RELAY_PUBLIC_URL 改內網 ws(部署時設、缺一不可)。 Reviewer 通過 0C/0M/0Mi。stage 純 demo 無正式使用者。 Co-Authored-By: Claude Opus 4.8 (1M context) --- docker/nginx.stage.conf | 172 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/docker/nginx.stage.conf b/docker/nginx.stage.conf index 0ea8cbc..d779457 100644 --- a/docker/nginx.stage.conf +++ b/docker/nginx.stage.conf @@ -346,3 +346,175 @@ server { proxy_cache off; } } + +# ============================================================ +# 內網 IP 直連 server block(demo 用) +# ------------------------------------------------------------ +# 目的:讓 visionA Agent / 內網瀏覽器以 http://192.168.0.130:9527 +# 直連 container(繞過公網邊界 proxy,那層剝 WS upgrade header、 +# 碰不到 /tunnel/connect)。 +# +# 為什麼安全(見 spec §4 完整評估): +# - 公網邊界 proxy 進來的 Host 一律被改寫成 stage-9527.innovedus.com +# → 永遠走上面那個 block,外部偽造 Host:192.168.0.130 也打不進這條。 +# - 這條只有「能直接連到 130:9527 的內網來源」可命中。 +# - default_server 仍是 server_name _ 那條,本 block 不加 default_server。 +# +# DRY 取捨:務實複製 stage-9527 block 的 location。WS upgrade map +# $connection_upgrade 在 http context 定義(檔頭 38-41 行),本 block 直接繼承。 +# +# X-Forwarded-Proto:實際是 http 進來,但 backend 不讀 XFP(已 code 確認), +# 設 http 或 https 都不影響。這裡設 http 以符合實際(避免 backend 未來 +# 萬一改讀 XFP 時組出錯誤的 https absolute URL)。 +# +# ⚠️ demo 專用,正式上線前應移除,或改用 allow <內網網段>; deny all; 限制來源。 +# ============================================================ +server { + listen 80; + listen [::]:80; + server_name 192.168.0.130; + + # 與 stage-9527 block 對齊的全域行為 + client_max_body_size 100M; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + gzip on; + gzip_min_length 1024; + gzip_types + text/plain text/css text/xml text/javascript + application/json application/javascript application/xml + image/svg+xml; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # ── 淺層健康檢查 ── + location = /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } + + # ── /api/* → api-server :3721(含 /api/pairing/exchange、models、devices)── + location /api/ { + proxy_pass http://visiona_api; + proxy_http_version 1.1; + proxy_buffering off; + + proxy_set_header Host $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 http; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Connection ""; + + proxy_no_cache 1; + proxy_cache_bypass 1; + add_header Cache-Control "no-store, no-cache, must-revalidate, private" always; + add_header Pragma "no-cache" always; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + } + + # ── /storage/* → api-server :3721(presigned 下載)── + location /storage/ { + proxy_pass http://visiona_api; + proxy_http_version 1.1; + proxy_buffering off; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto http; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Connection ""; + + proxy_no_cache 1; + proxy_cache_bypass 1; + add_header Cache-Control "private, no-store" always; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + } + + # ── /preset-models/* → api-server :3721(B8 預設模型 .nef)── + location /preset-models/ { + proxy_pass http://visiona_api; + proxy_http_version 1.1; + proxy_buffering off; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto http; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Connection ""; + + expires 1d; + add_header Cache-Control "public, max-age=86400" always; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + } + + # ── /tunnel/connect — WebSocket upgrade(最關鍵)── + # 完整複製 stage-9527 block 的 WS 設定:http/1.1 + Upgrade + Connection + # + 86400s timeout + buffering off。$connection_upgrade 繼承自 http-context map。 + location /tunnel/connect { + proxy_pass http://visiona_tunnel; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + + proxy_buffering off; + + proxy_set_header Host $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 http; + } + + # ── Next.js hashed static(內網瀏覽器用)── + location /_next/static/ { + proxy_pass http://visiona_frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + + # ── 其他靜態資源 ── + location ~* ^/(favicon\.ico|.*\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|eot))$ { + proxy_pass http://visiona_frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + + expires 1d; + add_header Cache-Control "public, max-age=86400" always; + } + + # ── catch-all → Next.js frontend :3000(內網瀏覽器用)── + location / { + proxy_pass http://visiona_frontend; + proxy_http_version 1.1; + + proxy_set_header Host $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 http; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Connection ""; + + proxy_buffering off; + proxy_cache off; + } +}