Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions lib/web-rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
33 changes: 31 additions & 2 deletions test/webserver-mount.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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 {
Expand Down