From 2749548a99b258affa75f56426aa861f388f69ea Mon Sep 17 00:00:00 2001 From: qgx1992 Date: Thu, 10 Sep 2026 19:28:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(rpc):=20=E4=BF=AE=20requestRejection=20?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=20this=20=E7=BB=91=E5=AE=9A=E8=87=B4=20/dsh-?= =?UTF-8?q?pocket/*=20=E5=85=A8=E9=83=A8=20403=EF=BC=88issue=20#117?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dsh 0.1.5-rc.1 的 HostConnectionService.requestRejection 是依赖 this 的类方法 (内部读 this.trustedHosts / this.browserAuth)。mountPocketWebRoute 把方法从 ctx.connection 上抽成裸函数后再调用,this 丢失 → TypeError → 被 `catch { rejection = 403 }` 吞成 403,于是本机 / 带会话 cookie 的浏览器 / 移动端 的**所有** /dsh-pocket/* 请求恒为 403: - 设置页 pocket.status 全挂 → 局域网区块卡在「代理未就绪…」、二维码始终不显示 - 公网隧道 tunnel.start 报 transport failure for /dsh-pocket/tunnel.start: HTTP 403 改为以方法形式调用(connection.requestRejection(req)),与 dsh 自己的 /api 路由 (client-connection 的 register())一致;等价于 issue #117 报告者建议的 requestRejection.call(ctx.connection, req)。 回归测试:test/webserver-mount.test.js 新增用例,用与 dsh 同构的「类方法 + 依赖 this」的 fake connection 覆盖。已在修复前验证其失败(403),修复后通过(200)。 Refs #117 --- lib/web-rpc.js | 15 ++++++++++++--- test/webserver-mount.test.js | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/lib/web-rpc.js b/lib/web-rpc.js index 93ba47c..b543e97 100644 --- a/lib/web-rpc.js +++ b/lib/web-rpc.js @@ -201,15 +201,24 @@ async function pocketHttpBridge(req, res, fetchHandler, maxBodyBytes) { function mountPocketWebRoute(ctx, { channel, handler, log }) { const webServer = ctx?.webServer; if (!webServer || typeof webServer.register !== 'function') return null; - const requestRejection = ctx?.connection?.requestRejection; + // 必须持有 connection 本体并以**方法形式**调用 requestRejection(issue #117): + // dsh 的 HostConnectionService.requestRejection 是类方法,内部读 this.trustedHosts / + // this.browserAuth。先把方法抽成裸函数(`const fn = ctx.connection.requestRejection`) + // 再调用会丢失 this → TypeError → 被下面的 catch 兜底成 403,于是**任何**请求 + // (本机、带会话 cookie 的浏览器、移动端)都被判 forbidden,设置页 status RPC 全挂 + // (用户可见症状:局域网区块一直显示「代理未就绪…」,公网隧道开启报 403)。 + // dsh 自己的 /api 路由也是以方法形式调用的(见 client-connection 的 register()); + // 本仓库 lib/index.js 处理 authenticatedUrl 时同样用 fn.call(ctx.connection, ...) 绑定。 + // 等价写法:requestRejection.call(ctx.connection, req)(issue #117 报告者的建议)。 + const connection = ctx?.connection; const fetchHandler = pocketFetchHandler(channel, handler, log); const route = { kind: 'prefix', path: channel, handler: async (req, res) => { let rejection; - if (typeof requestRejection === 'function') { - try { rejection = requestRejection(req); } catch { rejection = 403; } + if (typeof connection?.requestRejection === 'function') { + try { rejection = connection.requestRejection(req); } catch { rejection = 403; } } else if (!isTrustedLoopbackRequest(req)) { rejection = 403; } diff --git a/test/webserver-mount.test.js b/test/webserver-mount.test.js index 9bc9724..458742d 100644 --- a/test/webserver-mount.test.js +++ b/test/webserver-mount.test.js @@ -79,12 +79,12 @@ function postJson(port, path, body, { host = '127.0.0.1', origin, contentType = } /** 装配一个跑在 fake webServer 上的 dsh-pocket RPC,返回 { port, stop, ctx, installDispose }。 */ -async function setup({ requestRejection, opts = {} } = {}) { +async function setup({ requestRejection, opts = {}, connection } = {}) { const webServer = fakeWebServer(); // 故意把 rpc.handle 设成抛错:若走回退路径就直接失败,证明走的是直接 mount 路径。 const ctx = { webServer, - connection: { + connection: connection ?? { rpc: { handle: () => { throw new Error('test: must not call rpc.handle when webServer is available'); } }, requestRejection, }, @@ -129,6 +129,35 @@ test('认证门:requestRejection 返回 403 → 403 forbidden', async () => { } finally { await env.stop(); } }); +test('issue #117:requestRejection 必须以方法形式调用(保留 this),否则任何请求都被兜成 403', async () => { + // 与 dsh 的 HostConnectionService.requestRejection 同构:**类方法**,内部依赖 this。 + // 旧实现把方法抽成裸函数再调用(const fn = ctx.connection.requestRejection; fn(req)) + // → this 丢失 → TypeError → 被 catch 兜底成 403,于是本机/带 cookie 的浏览器/移动端 + // 所有请求全被判 forbidden,设置页 status RPC 全挂(用户症状:「代理未就绪…」)。 + class FakeConnection { + constructor() { this.trustedHosts = ['127.0.0.1']; } + // 未绑定 this 时 this.trustedHosts 读取即抛 TypeError + requestRejection() { + if (!Array.isArray(this.trustedHosts)) { + throw new TypeError("Cannot read properties of undefined (reading 'trustedHosts')"); + } + return undefined; // 放行 + } + } + const conn = new FakeConnection(); + // 故意设成抛错:证明走的是直接 mount 路径而非 rpc.handle 回退 + conn.rpc = { handle: () => { throw new Error('test: must not call rpc.handle when webServer is available'); } }; + const env = await setup({ connection: conn }); + try { + const res = await postJson(env.port, `${POCKET_RPC_CHANNEL}/${POCKET_ENDPOINTS.status}`, { rpcId: 'th1', method: POCKET_ENDPOINTS.status, payload: {} }); + // 旧实现(this 丢失 → catch → 403 forbidden)在此断言失败 + assert.equal(res.status, 200, `期望放行 200,实际 ${res.status} ${res.body}`); + const body = JSON.parse(res.body); + assert.equal(body.result.ok, true); + assert.equal(body.result.value.dshPort, 3080); + } finally { await env.stop(); } +}); + test('认证门:无 requestRejection 且 Host 非 loopback → 403', async () => { const env = await setup({ requestRejection: undefined }); try {