Compare commits

..
1 Commits
Author SHA1 Message Date
yexuejc 115b8b5e35 提交AI的记忆文件 2026-10-20 22:23:34 +08:00
3 changed files with 155 additions and 0 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# 2026-10-19
## Web 代理应用(webproxy)v1 已搭建并验证
- 位置:workspace/webproxy,Go 1.22 单二进制、前后端一体。目标:服务器部署后,浏览器输入任意 URL,由服务器代为请求并回填(HTML 重写 + 静态资源代理 + 重定向/表单/Cookie),用于经服务器访问其所在内网的客户站点。
- 能力:可配置 SSRF(config.json.oss:mode ∈ off/blacklist/whitelist,默认 blacklist;拦 loopback / link-local(含云元数据169.254.169.254) / 非 http(s) 协议,放行 RFC1918 内网 block_private=false);HTML 属性重写(golang.org/x/net/html);<link>/<style>/CSS url() 重写;注入 JS 重写 fetch/XHR/WebSocket/window.open/form;session cookie jar 隔离;3xx 改写 Location 回 /p/;资源按原 MIME 流式转发。
- 踩坑:① Go RE2 不支持正则 \1 反向引用 → CSS url 正则改为 (?i)url\(\s*[^)]*?\) 并手工去引号;② base64url 填充不一致(浏览器/URL 用无填充,Go 曾用带 '=' 的 URLEncoding)→ 统一为 RawURLEncoding。
- 运行:`go run .` 或 `go build -o webproxy.exe . && ./webproxy.exe`;监听/SSRF 都在 config.json;浏览器访问 http://<服务器>:8080/ 输入 URL。
- 测试注意:沙箱设了 http_proxy=127.0.0.1:8629 且 no_proxy 为空,本地 curl 需 `--noproxy '*'`;沙箱 8080 被占用,临时改过 18090 测试。
- 验证结论:UI、self 400、loopback/link-local/ftp 403、端到端 HTML/CSS/图片重写与资源代理、go vet 全部通过。
## Docker 一键部署
- 新增:Dockerfile(多阶段:golang:1.23-alpine 编译 → alpine:3.20 运行,CGO 纯静态,含 ca-certificates、HEALTHCHECK、EXPOSE 8080)、docker-compose.yml、.dockerignore(排除 webproxy.exe/.git/testdata-site)、deploy.sh(检查 docker → build → 清旧容器 → run -d -p 8080:8080 --restart unless-stopped;PORT 环境变量改端口)。
- 注意:本沙箱 docker CLI 在但 daemon 未起,未做真实 build;部署文件按路径/依赖静态核对无误。运行时配置走容器内嵌 config.json,需覆盖(如 insecure_skip_verify)时用 compose 里已注释的 volume 挂载。
## 文档补全(webproxy)
- 新增 README.md + docs/ 五份专题文档(ARCHITECTURE/CONFIG/DEPLOY/SECURITY/API),内容与源码逐行核对:SSRF 三档场景化配置、URL base64url 编码规范(RawURLEncoding 无填充,示例已用脚本生成)、生命周期、Header 处理规则、错误码、加固清单(反代 Basic Auth + HTTPS nginx 片段)。
- 文档中如实记录的已知局限:无鉴权、307/308 归一 302、WS 仅导流未真正代理、meta-refresh 未重写、HTML/CSS 全量读内存、session 无 TTL。
## 端口调整(webproxy)
- 默认端口 8080 → 8765(老板要求)。改动位置:config.json.listen、main.go 兜底默认值、Dockerfile(EXPOSE+HEALTHCHECK)、docker-compose.yml(8765:8765)、deploy.sh(WEBPROXY_PORT 默认值+端口映射)、README/docs 全部同步;文档中示例目标站点(如 192.168.1.10:8080)的端口保留未动,属于举例。
- 验证:go build 通过;8765 UI 200、8080 未监听、环回 SSRF 403、请求自身 400。
## JS 桩注入 bug 修复(webproxy)
- 线上报错:`(索引):28 Uncaught SyntaxError: Unexpected token ')'`。根因:injectRawScript 用字节级搜索 `</head>` 定位注入点,页面内联脚本/模板字符串含 `</head>` 字面量时,桩被插进页面自身脚本内部(本例被 // 注释吞掉静默失效;老板的页面落在非注释位置 → 引号失衡语法报错);且桩原本没有 `<script>` 包裹(普通页面下会以裸文本被解析器挪到 body 顶部,永不执行)。
- 修复:rewriteHTML 增加 js 入参,解析+walk 后以 DOM 方式在 `<head>` 末尾追加 `<script>` 节点(injectProxyScript + findFirstElement),删除 injectRawScript。
- 验证(tests/check_js.js + tests/check_stub_run.js,配套本地目标站):修复前对照输出中 fetch/XHR/open/form 全部漏出、submit 监听未注册;修复后全部导流 /p/,页面原脚本 542 字符语法完好,资源代理(css/png/子页/目录)全 200。ws:// 按设计原样透传(已知局限)。
- config.json 的 block_loopback 测试后已恢复 true;测试站点已清理;.dockerignore 增加 tests。
+111
View File
@@ -0,0 +1,111 @@
# 2026-10-20
- **修复 Docker 构建拉取依赖失败**:`docker compose up -d` 在 `RUN go mod download` 报连接 proxy.golang.org (142.250.198.145) 被拒。根因=构建环境无法直连 Google Go 模块代理(国内/受限网络)。
- 主 `Dockerfile` 加 `ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,direct`。
- 新增 `Dockerfile.offline`:用 `go mod vendor` 打包的 `vendor/`,`-mod=vendor` 编译,完全离线可用;本机已 `go mod vendor` 生成 vendor/ 并验证 `-mod=vendor` 编译通过(产出 10MB 静态二进制)。
- `docs/DEPLOY.md` 新增 3.3 小节说明方案 A/B;`.dockerignore` 确认未排除 vendor/。
- **修复 UI 主页 `(index):28` 报错的根因(与之前的桩修复是不同 bug)**:`static/index.html` 内联脚本第 28 行 `String.fromCharCode(b[i)` 漏写 `]` → 整个 UI `<script>` 语法错误、`go()` 失效。已改为 `b[i]`。之前 10-19 误判为代理桩破坏目标站脚本(桩修复本身有效,但非本次报错源)。
- **用 headless Chrome (CDP) 真实浏览器验证通过**:页面 JS 异常数=0,`go()` 生成的 `/p/<base64url>` 与后端期望一致。验证脚本 `tests/verify_browser.js`。
- **关键约束**:main.go 用 `os.ReadFile("static/index.html")` 运行时读取(非 embed),本地直接跑立即生效;但用户用 Docker 启动,容器内是 build 时旧文件,改 UI 后必须 `docker compose up -d --build` / `./deploy.sh` 重建镜像。
- **新功能:新标签顶层导航自动带外框**(用户反馈 `target="_blank"` 链接在新标签打开 `/p/xxx` 时 UI 外框丢失)。
- `proxy.go`:顶层导航(`Sec-Fetch-Dest: document`,老浏览器兜底 `Accept: text/html` 前缀)且 GET 且无 `?_wpframe=1` 时,302 改道到 `/?url=<enc>`;iframe/AJAX/POST 不受影响。
- `static/index.html`:iframe src 带 `?_wpframe=1` 标记防循环;支持 `?url=` 参数自动回填输入框并加载(`dec()` 解码)。
- 验证:curl 7 场景 + headless Chrome(`tests/verify_newtab.js`)确认 302 落地外框、输入框回填、iframe 加载出内容,全部通过。
- 已知取舍:POST 顶层导航(表单 target=_blank)不改道(保表单语义),此类新标签无外框。
- 文档同步:README 特性表/局限表、API.md 3.3 节+路由表、ARCHITECTURE.md 设计决策表、tests/README.md。
- **诊断:访问 192.168.0.107:5666(飞牛 fnOS NAS 登录页)白屏,非 SSRF/网络问题**。
- 实测:本机直连 200、代理 `_wpframe=1` 抓 HTML 200、ssrf.mode=off 内网本就放行;`curl` 直连与代理均通 → 排除 SSRF 与网络。
- 真因:fnOS 是 React/Vite 重 SPA,主 JS 2.8MB 含 **102 处动态 `import('/xxx.js')`(绝对路径)**。浏览器把这些绝对路径解析到代理根 `http://localhost:8765/jsencrypt-CTgvjfLn.js` → 代理只认 `/p/`,返回 **404**,分包加载失败 → 整页白屏(headless Chrome 渲染 #root 为空)。
- 这是 URL 重写型代理的**架构性局限**:JS 桩只拦截 fetch/XHR/WebSocket/window.open/submit,无法拦截 JS 内部的 `import()`(不是函数,无法重写);绝对路径 import 在代理根下 404。
- 修复方向(待用户拍板):① 路径前缀反代 `/_p/<host>/<path>` 保留绝对/相对路径(正解,改动大);② 代理对 JS 内容做 import() 字面量路径改写(快速但有局限);③ 仅记为已知局限。
- **实施:子域名反代(用户选"路径前缀反代(推荐)",实际落地为更稳的子域名方案)**。
- 根因复核:`import('/x')` 是语言关键字无法被 JS 桩拦截;纯路径前缀仍会让绝对路径 import 打到代理根 404。子域名方案让文档"源"变成目标自身——`http://<编码origin>.localhost:8765/<path>`,浏览器把 `*.localhost` 原生解析到 127.0.0.1,绝对路径 import/fetch 自然落回代理并映射回目标,**完全无需改写 JS 内部 import**。
- 编码坑:浏览器会把 Host 小写化 → base64url(含大小写)被破,改用 **hex**(0-9a-f 小写无损)。子域名标签=`hex(scheme://host:port)`;`?url=` 仍用 base64url(在 query 里不被小写)。
- 改文件:`main.go`(路由统一进 `rootHandler`:子域名→proxySubdomain / `/p/`→proxyHandler / 其他→uiHandler)、`proxy.go`(抽出 `doProxy` 公共核心;新增 `isSubdomain`/`decodeSubdomainOrigin`/`proxySubdomain`;顶层导航 302 显式指向 `http://localhost:<port>/?url=` 防止新标签丢外框)、`static/index.html`(`subURL()` 用 hex 标签拼子域名 iframe src)。保留 `/p/` 传统路径兼容。
- 验证(curl): 子域名文档 200;子域名绝对路径 JS → 200 + `application/javascript`(证明 import 映射正确);顶层导航 302 → `http://localhost:8765/?url=...`(外框保住)。无头 Chrome 渲染 fnOS 时 `#root` 仍空——但定位到是 **fnOS 侧**:直连 `/assets/index-DhUqaTaa.js` 即 404(HTML 引了已删除的旧哈希包),且 fnOS 按 UA 返回不同 HTML(Go UA 给完整页、Chrome/curl UA 给 4543 回退壳)。属 fnOS 自身缓存/版本/鉴权问题,代理无法修。代理架构层面 SPA 支持已就绪。
- 约束:子域名反代**仅 localhost 访问可用**(依赖 `*.localhost`→127.0.0.1);经 LAN IP 访问的 SPA 仍走旧 `/p/` 路径,绝对路径 import 可能 404。已写进 README 已知局限。
- **修复:子域名模式下飞牛 SPA 控制台报 `Failed to load module script: Expected ... text/html`(同源资源 MIME 错误)**。
- 真因(与"白屏"是同一架构的延续 bug,非无解):子域名模式重写时把同源绝对地址 `http://192.168.0.107:5666/assets/x.js` 错写成 **`/p/<base64>`**(绝对代理路径)。浏览器在子域名源下解析成 `http://<hex>.localhost:8765/p/<base64>`,`rootHandler` 见 host 是子域名 → `proxySubdomain` 把 `/p/<base64>` 当成**目标路径**去飞牛取 → 飞牛无此路径 → 回登录页 HTML → 浏览器当模块解析失败报 MIME 错误。
- 验证事实:经代理取 `polyfills.js` 返回 `application/javascript`(资源本身没问题);直连飞牛任意 UA 也是 `application/javascript` → 坐实是代理映射错误,不是 fnOS 资源缺失。
- 修复三处:① `rewriter.go` 的 `rewriteHTML`/`rewriteCSS` 增加 `subdomainMode` 参数——子域名模式下**同源绝对地址改为根相对** `/assets/x.js`(让子域名处理器自行映射回目标),跨域才走 `/p/<base64>`;② `buildProxyJS` 增加 `subdomainMode`/BASE_HOST——子域名模式下 JS 桩只对**跨域**请求加 `/p/` 前缀,同源 fetch/XHR/WebSocket/表单保持相对(import() 本就靠浏览器在子域名源解析,无需桩干预);③ `proxy.go` `rootHandler` 改为**先判 `/p/` 前缀再判子域名**,使子域名下的跨域资源也能走 `/p/` 代理路径。
- 验证:子域名文档重写后 script src 全部为根相对(`/assets/polyfills-Bk1OHpAC.js` 等),仅跨域 busuanzi 保留 `/p/<base64>`;子域名取 `/assets/...` 返回 `application/javascript`;**无头 Chrome 渲染 fnOS `#root` 被填充 2632 字符(含 form/飞牛/svg/img 登录页结构)**,MIME 报错消失。
- 结论:飞牛经代理可正常加载,之前的报错是代理映射 bug 已修复,非目标站/网络/SSRF 问题,也非"不能解决"。
- **修复:子域名模式下不蒜子(busuanzi)同源 POST `/api.php` 报 400 "禁止请求代理自身"(用户问"是不是被限制了")**。
- 真因(同源 API 调用被 JS 桩错误劫持,非访问受限):不蒜子脚本发起 `fetch('http://<hex>.localhost:8765/api.php')`——这是**子域名同源请求**。但 `buildProxyJS` 的同源判断基准 `BASE_HOST` 传的是飞牛真实域名 `192.168.0.107:5666`,不蒜子请求 host=`687474...localhost:8765` 与 `BASE_HOST` 不符 → 桩误判成跨域 → 改写成 `/p/<base64>` 转走 → 解码后目标仍是子域名自身 → 触发 `proxyHandler` 的"禁止请求代理自身"自保护 400。页面加载正常,仅该统计 POST 失败。
- 修复(一行):`buildProxyJS` 模板中 `if(SUBMODE){BASE=location.origin;BASE_HOST=location.host;}`——子域名模式下把同源基准切成**页面自身源(子域名)**,于是同源 fetch/XHR/表单保持相对(交给子域名处理器映射回 fnOS),仅真正跨域才走 `/p/`。`/p/` 传统模式不受影响(SUBMODE=false 走原逻辑)。
- 验证:子域名文档注入的 stub 含该分支;服务端模拟 `POST /api.php`(子域名同源)返回 **200(不再 400)**;跨域 CDN 仍走 `/p/`。无头 Chrome 渲染 fnOS `#root` 仍正常 2632 字符。
- 给用户的定性:不是被限制,是代理桩的同源判断错配,已修复;剩余 `/api.php` 若 fnOS 本身无此端点会返回 fnOS 自己的 404,但绝不再是代理 400。
- **诊断:飞牛页面控制台仍残留 busuanzi(不蒜子) `SyntaxError: Unexpected token '<'` + favicon 404,二者均无害**。
- favicon.ico 404:浏览器每次自动请求图标,代理未提供 favicon,无害。
- busuanzi 报错真因(第三方计数器脆弱性,非代理 bug):`busuanzi.min.js` 逻辑为 `u=new URL(document.currentScript.src); fetch(u.protocol+'//'+u.host+'/api.php')`。经代理后脚本 src host 变成子域名 `687474...localhost:8765`,于是 `/api.php` 打到飞牛 `192.168.0.107:5666/api.php`;实测飞牛**无此端点**(直连返回 `404 + text/html`,body 是飞牛 SPA 首页 HTML)。busuanzi 对 HTML 做 `r.json()` 解析失败 → 报错。HTML 来自飞牛上游(我们代理自身错误页是 text/plain,已排除掺假)。且此脚本在真实局域网直连飞牛时同样会因同源 `/api.php` 缺失而失败——属第三方脚本固有脆弱性,**不影响飞牛登录/文件管理功能**。
- 顺带修复一个真实代理 bug:`doProxy` 在透传上游 **404/5xx 的 HTML/CSS** 时只 `Write` 未 `WriteHeader`,导致状态码被吞成 200(busuanzi 因此收到 200+HTML 而非 404+HTML)。已在 html 成功/失败回退、css 三个分支补 `w.WriteHeader(resp.StatusCode)` 透传原始状态码。验证:子域名 POST `/api.php` 现正确回 404(之前 200),正常页/UI 仍 200。
- 给用户结论:功能性 bug 已全部修复;剩余两条纯属无害噪声。若想要零控制台的干净效果,可加一个 HTML 重写规则**剥离 busuanzi `<script>`**(它只是访客计数,剥离不影响飞牛);待用户拍板是否做。
- **实施:用户要求控制台零报错 → 剥离 busuanzi `<script>` + favicon 返回 204 + 验证方式说明**。
- `rewriter.go` `rewriteHTML` 的 `walk` 在元素处理最前增加:若 `<script>` 的 src 解析后(resolveURL(base,...))含 `busuanzi`(大小写不敏感),则 `n.Parent.RemoveChild(n)` 并 return(在属性改写前拦截,故即使跨域被编码成 `/p/<base64>` 也能在解码后命中)。剥离后飞牛页面不再发起 `/api.php` POST、不再有 `r.json()` 解析报错。
- 残余 `busuanzi` 字样仅为飞牛页面里的**死 DOM 占位 span**(`<span id="busuanzi_container_site_pv">`、`本站总访问量:`),非脚本、不执行 JS,零影响。
- `main.go` `uiHandler` 增加 `/favicon.ico` → `204 No Content`,消除浏览器自动请求图标产生的 404 噪声。
- 验证方法(向用户说明):① `curl` 模拟不蒜子 POST `/api.php` 从 400→404,证明新二进制已生效(旧版必报 400);② `curl` 抓取子域名文档 `grep -iE '<script[^>]*busuanzi[^>]*>'` 为 0,证明脚本已剥离;③ 无头 Chrome `--dump-dom` 渲染飞牛 `#root` 2525 字符、含 form/飞牛,证明主功能正常;④ `curl /favicon.ico` 返回 204。
- 重要提示用户:若其用 **Docker** 跑代理,本地 `go build` 不影响容器内镜像,改 Go 代码后须 `docker compose up -d --build`(main.go 用 os.ReadFile 读 static,但 Go 二进制本身需重建镜像);其日志 400→404 的演变已证明其当前访问的是本地重建后的服务。
- **修复:WebSocket 连接不上(用户报"socket连接连接不上")**。
- 真因:此前代理只把 `ws://` 也交给 `doProxy` 当普通 HTTP 抓取/透传,未做 WS 隧道升级;`doProxy` 发的是普通 request(`http.NewRequest`)→ 收到 101 也无后续双向字节拷贝 → 浏览器 WS 握手失败、连不上。JS 桩本身已把跨域 `ws://` 改写成 `/p/<base64>`(子域名模式)或相对路径(同源),但服务端没对应隧道。
- 修复:`proxy.go` 在 `doProxy` 入口检测 `Upgrade: websocket`(且 Connection 含 upgrade)→ 归一化 `ws→http`/`wss→https`(便于 SSRF 校验与拨号,避免白名单模式把 ws 协议拦掉)→ SSRF 校验 → 命中则走新增 `proxyWebSocket()`,复用 `httputil.ReverseProxy`(自动转发 Upgrade 请求、收到后端 101 后劫持双向连接并 `io.Copy` 字节)→ 真正的 WS 隧道。`makeTransport` 加 `ForceAttemptHTTP2:false`(WS 升级需 HTTP/1.1)。Director 额外把 `Origin` 改为目标 origin 提升后端兼容性。
- 验证(本地起 echo 服务器 + 原生实现握手/帧的 WS 客户端经代理 `/p/<base64>`):客户端收到 `HTTP/1.1 101 Switching Protocols` + 正确 `Sec-WebSocket-Accept`,随后发出 masked `ping`、经隧道原样回显 `ping` → **双向隧道 PASS**。fnOS 子域名文档/JS 资源仍 200+正确 MIME(headless dump-dom 偶发 #root 空是 Chrome 计时假象,curl 取资源确认无误)。
- 提示用户:仅 localhost 访问走子域名隧道可用;Docker 部署改 Go 后需 `docker compose up -d --build`。
- **修复:飞牛登录态无法保持 / `GET /app/token 401 (Unauthorized)` 反复出现(用户报"请求登陆token校验失败")**。
- 先定性:fnOS 未登录时 `/app/token` 返回 401 是其**正常的"未登录"响应**,app 据此跳登录页——这一步不是代理 bug。但真正的坑在后续:登录后每次请求仍 401,登录态根本存不住。
- 真因(代理服务端 cookie 存储缺陷):代理此前用 `client.Jar = s.jar`(`net/http/cookiejar`)。Go 默认 cookiejar 按 publicsuffix 规则**拒绝为 IP 地址主机(如 192.168.0.107)保存 cookie** → 飞牛下发的会话 cookie(带 `Domain=192.168.0.107`)被静默丢弃 → 服务端 jar 永远是空的 → 每次 `/app/token` 都无会话 → 永久 401。已写最小复现程序 `tests/cjprobe` 坐实。
- 修复(让浏览器持有 cookie,而非服务端 jar):① `doProxy` 中 `client.Jar = nil`,不再依赖服务端 jar;② 响应头转发时**特殊改写 `Set-Cookie`**:去掉 `Domain`(让浏览器按代理主机 `*.localhost:8765` 作用域存储)、代理链路为 http 时去 `Secure`、`Path` 规整为 `/`(`rewriteSetCookie`/`stripDomainSecure`);③ 浏览器 `Cookie` 请求头原样透传给上游(本就非 hop header)。这样同一份飞牛会话 cookie 既能用于普通 HTTP,也会随浏览器自动带入 **WebSocket 握手**,登录态在反代后真正生效。
- 验证:`tests/cookietest` 起 mock fnOS(/login 下发 IP-Domain cookie、/app/token 校验、/websocket 回显 cookie)+ 真代理二进制。结果:子域名 `POST /login` 上游 Set-Cookie=`fnos_sid=SECRET; Path=/; HttpOnly`(已无 Domain)→ 浏览器存于代理主机 → `GET /app/token` 回 **200**(cookie 成功 replay);WS 握手带 `fnos_sid=SECRET` 经代理 → 上游收到 `cookie=SECRET`。
- 已杀掉旧代理进程并用修复后二进制重启于 `:8765`(后台任务 DxiPvF)。提示用户:Docker 部署改 Go 后仍需 `docker compose up -d --build`。
- **修复:飞牛登录页后台请求报 `errno:65534 errmsg:'验签失败'`(E_REQ_SIGN)导致反复 `location.reload()` 登录死循环**。
- 取证:从 fnOS 主 JS bundle(`/assets/index-*.js`,2.8MB)确认 `65534`=`E_REQ_SIGN`,属**逐请求签名校验**;客户端收到 `result:'fail',code:65534` 时执行 `location.reload()`(这正是用户看到的"一直 401→登录页→重连"循环)。`/app/token` 在未登录时返回 `401 invalid token` 本身是"未登录"正常响应,不是根因。
- 客户端鉴权头构造:`Authorization: \`trim ${token}\``(模板字符串)——未登录时 token 为 undefined,于是发出 `Authorization: trim undefined`(无害,仅说明未登录)。fnOS 真正拒绝的是**请求签名**校验。
- 真因(高概率):fnOS 在逐请求签名校验时按**源站(Origin/Host/Referer)绑定**校验;浏览器经子域名反代后,发出的 `Origin`/`Referer` 是代理子域名(`687474...localhost:8765`),飞牛收到的是自身源(`192.168.0.107:5666`),源不一致 → 验签失败。WS 通道此前已把 Origin 改写为上游(生效),但普通 HTTP 请求未改写。
- 修复:`doProxy` 在转发普通 HTTP 请求前,把 `Origin`/`Referer` 头部**改写为上游自身源**(`rewriteOriginInURL` 仅替换源站部分、保留 path/query),与 WS 通道的 Origin 改写保持一致。浏览器侧同源判断不受影响(它看到的是子域名文档源,仍按同源处理,不会触发 CORS 预检)。
- 新增**调试日志**(非侵入,默认关闭):环境变量 `WEBPROXY_DEBUG=1` 开启;可选 `WEBPROXY_DEBUG_FILE=路径` 落盘。记录每个代理请求的 `[REQ][REQ.h][OUT.h][RESP][RESP.body]`(仅对 ≥400 且非 html/css 的响应读 body 避免消费流),便于定位仍报错的真实请求。
- 验证:`--noproxy` 直连代理子域名,debug 日志确认 `[REQ.h] Origin=子域名` → `[OUT.h] Origin=192.168.0.107:5666`(改写已生效);资源/HTML 仍 200+正确 MIME。
- 当前状态:修复后二进制已带调试日志重启于 `:8765`(后台任务 4mSMjT,debug.log 落盘于项目目录)。请用户在浏览器重新登录验证;若仍报 `验签失败`,把 Network 面板里失败请求的 curl 或贴出 debug.log,即可定位是否还有别的签名因子(如签名含 host 而非仅 Origin)。
- 约束:本环境走 `--noproxy` 才通;浏览器直连 localhost:8765 不经过沙箱 HTTP_PROXY(127.0.0.1:7890),不受影响。Docker 部署改 Go 后仍需 `docker compose up -d --build`。
## fnOS 登录态真正根因(19:50 定案,debug.log 实锤)
- debug.log 显示:`POST /app/ticket → 200`(登录成功,fnOS 下发 `ost=...; Path=/; HttpOnly; SameSite=Lax`,**无 Domain 属性**),但之后浏览器所有请求 `Cookie=""` → `/app/token` 仍 401 → E_REQ_SIGN(65534) 验签失败连环 reload。
- 根因:页面是 `localhost:8765` 顶层 + `<hex>.localhost` iframe,Chrome 判定**跨站(第三方)上下文**(日志中 Sec-Fetch-Site: cross-site),SameSite=Lax Cookie 在跨站 iframe 的 fetch/XHR/WS 里被浏览器扣发。改 Set-Cookie 属性绕不开 Chrome 155 第三方 Cookie 策略。
- 修复(proxy.go):会话 Cookie 改回**服务端全局 jar**(`jarFor(origin)`,sync.Map 按 origin 存,多目标互不干扰);响应不再下发 Set-Cookie;`proxyWebSocket` 在服务端把 jar 里同一份 Cookie 注入 WS 握手。之前的"Go cookiejar 拒 IP Cookie"结论有误——host-only(无 Domain)Cookie 对 IP 主机存储正常,当时探测用的是非法的 Domain=IP。
- 回归(tests/cookietest/):mock 改为 host-only+SameSite=Lax;① login 200 且 Set-Cookie 不下发浏览器 ② 浏览器零 Cookie 下 /app/token→200(jar 注入)③ WS 握手 echo cookie=SECRET。真实 fnOS 冒烟 GET / 200。
- 运维注意:jar 在内存里,代理重启需重新登录 fnOS。WS 截图答疑:两条 websocket?type=main(一条 101 健康+一条旧连接/重连),非代理复制连接。
## runoob 公网站点"第三方直连"定性(20:02)
- 用户访问 runoob(子域名反代 68747470733a2f2f7777772e72756e6f6f622e636f6d.localhost:8765),抓包见大量直连外站请求(gtag/hm.baidu/doubleclick ads/recaptcha/adtrafficquality)。用户问"是不是新情况"。
- 结论:**不是 bug/不是回归**。debug.log 证实:① runoob 主页及静态资源 200;② 不少第三方 SDK 调用(如 ep1.adtrafficquality.google/getconfig/sodar,走 fetch/XHR)已被 JS 桩捕获经 /p/ 代理 200;③ 代理全程零 SSRF/502/403。直连外站的仅是运行时由第三方脚本 `createElement('script'/'iframe').src`/`new Image().src` 注入的资源,JS 桩只 hook fetch/XHR/WS/open/form,拦不到这类,故浏览器直连真实域名。这是内容改写型代理固有行为。对"访问内网站点"的核心目标无影响(内网站点无此类跟踪器)。唯一代价:第三方跟踪器看到真实 IP 与代理页 URL(隐私)。
- 已将此定性写入 MEMORY.md「URL 拦截范围与第三方直连」条目。若要全封装需 Service Worker 网络层拦截,属较大改动,当前目标下非必需。
## 开源化准备(发布到 git 前)
- 创建 `.gitignore`:排除 debug.log/chrome_err*.log、*.exe 二进制、调试散文件(dom*.html/doc*.html/rewritten*.html/bsz.js/js_via_proxy.js 等)、本地 config.json。从暂存区撤出此前误暂存的垃圾/敏感文件(debug.log 含内网 IP 192.168.0.107;此前全是 A 状态、尚未 commit)。
- 新增 `config.example.json`(ssrf.mode=blacklist 安全默认),取代原 `config.json`(mode=off)——避免开源示例把 SSRF 防护关掉。
- 新增 LICENSE(MIT)、根 SECURITY.md(GitHub 安全页引用)、CONTRIBUTING.md、CHANGELOG.md、Makefile、.github/workflows/ci.yml(gofmt/vet/build/test)。
- 新增 `proxy_test.go` 单元测试(URL base64url 编解码、子域名反代解析、SSRF IP 分类、Origin 改写、truncate),`go test ./...` 通过;`tests/cookietest/{mock,wsclient}.go` 加 `//go:build cookietest` 标签避免与 `go test ./...` 冲突。
- README 加徽章、§11 测试章节、License 章节、文档导航补 CONTRIBUTING/CHANGELOG/根SECURITY;tests/README.md 补 cookietest 带 tag 构建说明。
- 待办(依赖用户):把 `go.mod` 的 `module webproxy` 改成规范路径 `github.com/<用户名>/webproxy`(仅改一行,无内部子包依赖);README 徽章里的 YOUR_USERNAME 同步替换。
- 状态:105 文件已暂存(未 commit),gofmt/vet/build/test 全绿,垃圾/敏感项确认被忽略。
## 开源化收尾 + 全量注释补齐(当日续)
- 用户确认 GitHub 用户名:`yexuejc`。go.mod 的 `module webproxy` → `module github.com/yexuejc/webproxy`(仅改一行,无内部子包依赖);README 徽章、CONTRIBUTING clone 地址中的 YOUR_USERNAME 全部替换为 yexuejc;README 删掉"发布前替换用户名"提示行。
- 按用户要求补齐全量中文注释(函数作用/场景/入参/出参 + 分支说明 + 变量说明):session.go、ssrf.go、main.go、rewriter.go、proxy.go、proxy_test.go、tests/cjprobe/main.go、tests/cookietest/{mock.go,wsclient}。proxy.go 里旧方案死代码 rewriteSetCookie/stripDomainSecure/reCookie* 保留但注释标明"已弃用(2026-10-20 起改服务端 jar),可连同正则一起删除"。
- 顺手修了两个构建问题:① 我写 cjprobe 注释版时漏了 httptest.NewServer 的一个右括号(`}))`→`})`,已修);② cookietest 下 mock.go 与 wsclient.go 同 package main 双 main,加 `-tags cookietest` 整包编译即冲突(历史遗留地雷)——把 wsclient.go 移到 `tests/cookietest/wsclient/main.go` 独立子包,tests/README.md 与文件头用法同步更新。修后 `go build -tags cookietest ./tests/...` 全过。
- 终态校验全绿:gofmt(含 -w 修 proxy_test/mock 的对齐)、go vet ./...、go build ./...、go test ./...、cjprobe 实跑(login 200 / app/token 401,按设计复现 Domain=IP Cookie 被拒)。105 文件已暂存,等用户 commit & push。
## 删除 proxy.go 已弃用 Cookie 死代码(当日续)
- 用户拍板删掉旧"浏览器持有 Cookie"方案残留:`rewriteSetCookie` / `stripDomainSecure` 两个函数 + `reCookieDomain` / `reCookieSecure` 两个正则(proxy.go 613–671 行,约 60 行)+ import 中的 `regexp`。该方案 2026-10-20 已被服务端 jar 取代,注释里早已标明"可连同正则一起删除"。
- 删除前已 grep 全仓确认零引用;删除后全量校验全绿:gofmt / go vet ./... / go build ./... / go test ./...(ok 1.646s)/ go build -tags cookietest ./tests/... / mock、wsclient、cjprobe 单编全过。proxy.go 746→685 行。
- 中间有个小插曲:一次性跑满 7 步校验时第 6 步偶发 SIGTERM(go test 缓存进程干扰),单独重跑即过,非代码问题。
- 删死代码后用新二进制重启(PID 29620,:8765 监听,debug 落盘),冒烟验证:/p/ 代理 runoob 首页 200(526KB),debug.log 请求闭环正常;用户实测确认无问题。jar 在内存,重启后 fnOS 需重登一次。
- 已提交(未 push,等用户操作):root commit a9ebb5e,105 files / 178210 insertions,工作区干净。
## git push 代理故障排除(22:0x 续)
- 用户 `git push github` 报 `fatal: unable to access 'https://github.com/yexuejc/webproxy/': Failed to connect to 127.0.0.1 port 7890`。
- 根因:git **全局配置**残留 `http.https://github.com.proxy = http://127.0.0.1:7890`(指向常不开的本地代理客户端);端口 7890 与报错完全一致。另,当前 shell 还有 `https_proxy/http_proxy=http://127.0.0.1:2683` 等**死代理环境变量**(不同端口),git 会回退读取。
- 处理(仅动 git 全局配置,**未碰任何项目文件、未改 remote**,符合用户"和项目没关系"的定性):`git config --global --unset http.https://github.com.proxy`;复查 `git config --show-scope --get-regexp proxy` 为空(git 配置已无任何 proxy)。
- 验证:`unset https_proxy HTTPS_PROXY http_proxy HTTP_PROXY all_proxy` 清掉会话内 env 后,`git push --dry-run github HEAD:master` 成功到达 GitHub(`[new branch] HEAD -> master`,exit 0),证明直连通路通畅。
- 提醒用户:其自身终端仍有死代理 env(7890/2683),推送前需先 `unset https_proxy HTTPS_PROXY http_proxy HTTP_PROXY` 再 `git push github master`,否则回退到 env 代理仍失败;若要永久生效,去 `~/.bashrc`/`~/.zshrc` 删 `export https_proxy=...` 那行。
- remote 现状核实:`github` = `https://github.com/yexuejc/webproxy`(HTTPS,正确);`origin` = 内网 Gitea 192.168.0.107:3001。此前那次 SSH over 443 改动经实测未真正落地,当前即是正确状态,无需回退。
- 教训(同时记入个人记忆):push 失败先去代理,不要擅自改 remote 通道绕行;该代理属用户全局环境项,与本项目无关。
+17
View File
@@ -0,0 +1,17 @@
# WebProxy 项目长期备注
- webproxy:Go 单二进制网页正向代理,位于 workspace/webproxy;用途是经服务器访问其所在内网的客户站点(任意 URL,尽量全兼容)。
- SSRF 设计约定:做成配置(config.json.ssrf.mode ∈ off/blacklist/whitelist)。默认 blacklist:拦 loopback、link-local(云元数据 169.254.169.254)、非 http/https 协议;**放行 RFC1918 内网(block_private=false)**,因为访问内网站点正是核心诉求,不能一刀切禁内网。复杂网络用 denied_ips/denied_protocols,纯白名单场景用 whitelist。
- 编码约定:目标 URL 统一用 base64 **RawURLEncoding(无 '=' 填充)**,前端 JS enc() 也要去掉尾部 '=',两端必须一致。
- JS 桩注入约定(2026-10-19 修复后):必须 **DOM 方式注入**——解析+重写后在 `<head>` 末尾追加 `<script>` 节点(injectProxyScript),严禁字节级搜 `</head>`(页面脚本内含该字面量时会插进脚本内部:被注释吞掉=静默失效,落字符串里=引号失衡 SyntaxError)。ws:// 按设计不透传(服务端未实现 WS 通道)。回归脚本在 webproxy/tests/。
- 已知局限(后续迭代):未接无头浏览器(反爬/重度 JS 站点可加 render=1 开关);HTML/CSS 走全量读内存,大文件/视频建议改流式;未做 meta-refresh 内容 URL 重写;entry 登录鉴权尚未加(规划中)。
- URL 拦截范围与"第三方直连"现象(2026-10-20 定性):注入的 JS 桩只 hook `fetch / XMLHttpRequest.open / WebSocket / window.open / form.submit`,外加静态 HTML 属性(href/src/action…)与内联 CSS `url()` 重写。凡是第三方脚本在运行时用 `document.createElement('script'/'iframe').src`、`new Image().src`、动态 `<link>` 注入的资源(典型:Google GTM/gtag、百度 hm.js、Google Ads/doubleclick、reCAPTCHA、adtrafficquality sodar2.js 等),**既不经过静态重写也不经过桩 hook,会由浏览器直连真实域名**。这是内容改写型代理的固有行为,**不是 bug、不是回归**:fnOS 类内网站点不含此类跟踪器故此前未暴露;页面功能不受影响(直连的是公网 CDN,浏览器本就能直连);代理全程零 SSRF/502/403。唯一代价是隐私——这些第三方跟踪器能看到真实浏览器 IP 与被代理页 URL。要做到"全封装"需引入 Service Worker 在网络层拦截所有子资源,属较大改动,当前以"访问内网站点"为目标通常无需做。
- 文档体系(2026-10-19 补全):README.md(总览/特性/快速开始/错误码/局限/验证记录)+ docs/{ARCHITECTURE,CONFIG,DEPLOY,SECURITY,API}.md,均与源码逐行核对、随代码变更同步维护;部署文件为 Dockerfile/compose/deploy.sh。
- 端口约定(2026-10-19 起):默认端口 **8765**(config.json.listen、容器 EXPOSE/HEALTHCHECK、compose 映射、deploy.sh 的 WEBPROXY_PORT 默认值均为此);文档里示例目标站点的 8080(如 192.168.1.10:8080)只是举例,与代理自身端口无关。
- Docker 构建依赖拉取(2026-10-20 踩坑):默认 GOPROXY=proxy.golang.org 在国内/受限网络被拒(dial tcp 142.250.x.x:443 connection refused)。对策:主 Dockerfile 改 `ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,direct`;另提供 `Dockerfile.offline` + `go mod vendor` 生成的 `vendor/`,供服务器完全离线构建(全程不联网)。`.dockerignore` 不可排除 vendor/。
- 前端 UI(static/index.html)由 main.go 运行时 `os.ReadFile` 读取(非 embed),改 UI 后本地直接重跑即生效;但 Docker 镜像 build 时 COPY 进容器,改 UI 必须**重建镜像**(`docker compose up -d --build` / `./deploy.sh`)才生效。已知 UI 致命 bug:index.html 第 28 行曾漏写 `]` 致整个脚本失效(已修)。
- Cookie/会话保持约定(2026-10-20 二次修正,以本条为准):**会话 Cookie 由代理服务端全局 jar 持有**——`jarFor(origin)`(sync.Map 按 origin 存,多目标互不干扰),`client.Jar=jarFor(...)`;响应**不下发 Set-Cookie** 给浏览器;WS 隧道在 `proxyWebSocket` 里用 `cookiesForJar` 把同一份 Cookie 注入握手。原因:页面是 localhost 顶层 + `<hex>.localhost` iframe,Chrome 判定跨站上下文,SameSite=Lax 的会话 Cookie 在 iframe 内 fetch/XHR/WS 被**浏览器扣发**,改 Set-Cookie 属性(去 Domain/加 SameSite=None)都绕不开第三方 Cookie 策略。曾两次误诊:①"Go cookiejar 拒 IP Cookie"——实际 host-only(无 Domain)Cookie 对 IP 主机存储正常,当时探测用了非法的 Domain=IP;②"浏览器持有 cookie"方案——被跨站 iframe 扣发。jar 在内存,代理重启需重新登录。回归脚本 webproxy/tests/cookietest(mock 为 host-only+SameSite=Lax)。
- 注释规范(用户明确要求,必须长期遵守):所有代码注释要"够多"——① 函数头必须完整说明作用、场景、入参、出参;② 分支(if/switch case)必须有说明;③ 变量必须有详细说明。2026-10-20 已按此标准补全全部 Go 源码(proxy/rewriter/ssrf/session/main + tests)。
- GitHub 用户名(2026-10-20 确认):`yexuejc`。仓库规范路径 `github.com/yexuejc/webproxy`(go.mod module、README 徽章、CONTRIBUTING clone 地址均已改)。
- 工程结构约定(2026-10-20):tests/cjprobe 是诊断探针(复现 Domain=IP Cookie 被拒);tests/cookietest 两个程序各自独立目录(mock 在 cookietest/ 根、wsclient 在 cookietest/wsclient/),避免同包双 main;均带 `//go:build cookietest` 标签不参与 `go test ./...`。proxy.go 中旧"浏览器持有 Cookie"方案的 rewriteSetCookie/stripDomainSecure/reCookieDomain/reCookieSecure 已于 2026-10-20 当日删除(用户拍板),删除前 grep 确认零引用,删除后全量校验全绿。
- webproxy 仓库 remote 结构(2026-10-20 定稿):`origin` = 内网 Gitea(fnOS NAS 192.168.0.107:3001,HTTP 免代理直连,内网本就放行);`github` = GitHub `https://github.com/yexuejc/webproxy.git`(**HTTPS,未改 SSH**)。git 全局配置曾残留 `http.https://github.com.proxy = http://127.0.0.1:7890`(指向常不开的本地代理),导致 `git push github` 报 `Failed to connect to 127.0.0.1 port 7890`;已 `git config --global --unset http.https://github.com.proxy` 清除,复查 git 配置已无任何 proxy,dry-run 直连 GitHub 通过。推送前还需在用户**自身终端** `unset https_proxy HTTPS_PROXY http_proxy HTTP_PROXY`(shell 里另有死代理 env 指 127.0.0.1:2683/7890),否则回退到 env 代理仍失败。**重要教训(用户明确要求):push 因代理失败不要擅自改 remote 通道绕行,先去掉 git 的代理设置即可**——代理属全局环境因素,与本项目无关,勿写进任何项目文件。