From e2c8a23a984b6aebe9d73ebc9f7cb0055f7a683d Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:10:08 -0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20SSE=20=E6=B4=BB=E5=8A=A8=E6=B5=81?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=90=8C=E6=BA=90=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8F=8D=E4=BB=A3=E9=83=A8=E7=BD=B2=E8=BF=9E=E4=B8=8D=E4=B8=8A?= =?UTF-8?q?=20:8787?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sseUrl() 原先无条件把 SSE 地址拼成 ${hostname}:8787,连生产静态导出 也带这个硬编码。反代到 443 的部署里页面从 https://域名/ 加载,SSE 却被 指向 https://域名:8787/...,而 8787 未对公网开放,导致活动流连不上、反复重连。 改为用 NODE_ENV 区分:生产默认同源(相对路径),仅 next dev 保留 :8787 以绕过 Next.js /api 反代对 SSE 的缓冲。NEXT_PUBLIC_SSE_BASE 覆盖口保留。 验证:NEXT_EXPORT=1 next build 后生产包里 :8787 分支已被 tree-shake。 补充 README 反向代理部署文档(同源说明 + nginx 关缓冲配置)。 Closes #169 --- README.md | 29 +++++++++++++++++++++++++++++ web/src/lib/api.ts | 28 +++++++++++++++++++--------- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 52ba901..3bddb9c 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,35 @@ CGO_ENABLED=0 go build -tags embedui -o artex ./cmd/artex **常用参数**:`./start.sh -addr :8787 -proxy :8788`(`-addr` 前端+API,`-proxy` 流量录制代理)。启动脚本会把参数原样透传给 `artex`。 +### 反向代理部署(HTTPS / 只开放 443) + +前端和 API/SSE 都由同一个后端端口(默认 `:8787`)提供,实时活动流默认走**同源**地址,因此**无需配置 `NEXT_PUBLIC_SSE_BASE`**,公网只开放 443、把 8787 留在内网即可。 + +SSE 是长连接 + 持续推送,反代**必须关闭缓冲**,否则浏览器能连上却收不到事件(表现为活动流一直转圈)。Nginx 示例: + +```nginx +server { + listen 443 ssl; + server_name your.domain.com; + # ssl_certificate / ssl_certificate_key ... + + location / { + proxy_pass http://127.0.0.1:8787; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE 关键项:关缓冲、长超时、HTTP/1.1 + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_http_version 1.1; + proxy_set_header Connection ""; + } +} +``` + +> 仅当 SSE 需要走与页面不同的来源(如独立子域)时,才在**构建期**设置 `NEXT_PUBLIC_SSE_BASE`(该变量在 `next build` 时固化进静态包,容器运行时再设无效)。 + --- diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 6fe8a9e..1bd4f4b 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -146,14 +146,22 @@ export async function http(path: string, init?: RequestInit): Promise { return r.json(); } -// sseUrl builds a URL for Server-Sent Events streams. SSE must NOT go through the -// Next.js dev `/api` rewrite: that proxy buffers the streamed response, so event -// frames never reach the browser (the EventSource opens but receives 0 messages). -// We therefore connect straight to the Go backend, whose CORS is open. Override -// with NEXT_PUBLIC_SSE_BASE; set it to "" to force same-origin (e.g. behind a -// production reverse proxy that flushes SSE correctly). -// Token is appended as ?token= because SSE bypasses the Next.js proxy and the -// browser does not send cookies cross-port. +// sseUrl builds a URL for Server-Sent Events streams. +// +// Production (static export / Docker): the Go backend serves the web UI *and* the +// SSE streams on the same port, so we default to a same-origin (relative) URL. +// This is what makes reverse-proxy deploys work: a page loaded from +// https:/// connects to https:///api/..., which the proxy forwards +// to the backend — no need to expose :8787 publicly. Hardcoding :8787 here used to +// break exactly that (https://:8787/... is unreachable when only 443 is open). +// +// Dev (next dev): SSE must NOT go through the Next.js `/api` rewrite — that proxy +// buffers the streamed response, so event frames never reach the browser (the +// EventSource opens but receives 0 messages). So in dev only we connect straight +// to the Go backend on :8787, whose CORS is open. +// +// Override either default with NEXT_PUBLIC_SSE_BASE (set it to "" to force same-origin). +// Token is appended as ?token= because SSE can't carry cookies cross-origin. // mockReport returns a canned Markdown report for the demo. function mockReport(_task?: string): string { return `# ARTEX 渗透测试报告 — Acme Corp @@ -180,7 +188,9 @@ function mockReport(_task?: string): string { export function sseUrl(path: string): string { const base = process.env.NEXT_PUBLIC_SSE_BASE ?? - (typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8787` : ""); + (process.env.NODE_ENV !== "production" && typeof window !== "undefined" + ? `${window.location.protocol}//${window.location.hostname}:8787` + : ""); const token = getToken(); const sep = path.includes("?") ? "&" : "?"; return token ? `${base}${path}${sep}token=${encodeURIComponent(token)}` : `${base}${path}`;