dsh-pocket 2.5.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/proxy.mjs +38 -7
- package/package.json +1 -1
package/lib/proxy.mjs
CHANGED
|
@@ -268,24 +268,51 @@ function isHtmlRequest(req) {
|
|
|
268
268
|
return accept.includes('text/html') || req.url === '/' || /\.html?$/i.test(String(req.url));
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
/**
|
|
271
|
+
/** 校验请求是否已认证。返回 { ok, rawQueryToken }:
|
|
272
|
+
* - ok=true:已认证;rawQueryToken 是 URL `?token=<PIN>` 命中的那条原始密码(用于种 cookie);
|
|
273
|
+
* - 已有 cookie 命中 → rawQueryToken=null(不要重复种)。 */
|
|
272
274
|
function authCheck(req, tokens, sessionKey) {
|
|
273
275
|
const list = (Array.isArray(tokens) ? tokens : tokens ? [tokens] : []).filter(Boolean);
|
|
274
|
-
if (list.length === 0) return true
|
|
276
|
+
if (list.length === 0) return { ok: true, rawQueryToken: null };
|
|
275
277
|
const cookies = parseCookies(req.headers.cookie);
|
|
276
278
|
const cookieTok = cookies[TOKEN_COOKIE];
|
|
277
279
|
if (cookieTok) {
|
|
278
280
|
for (const token of list) {
|
|
279
|
-
if (cookieTok === cookieFor(token, sessionKey)) return true;
|
|
281
|
+
if (cookieTok === cookieFor(token, sessionKey)) return { ok: true, rawQueryToken: null };
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
const qTok = new URL(req.url ?? '/', 'http://x').searchParams.get('token');
|
|
283
285
|
if (qTok) {
|
|
286
|
+
// URL `?token=<原始 PIN>` 用于分享/扫码直达:用户拿到 URL 就能直输明文 PIN,
|
|
287
|
+
// 不需要把 sha256 哈希也告诉他们。匹配上后由调用方通过 maybeSeedAuthCookie
|
|
288
|
+
// 种 HttpOnly 哈希 cookie,让浏览器后续子资源(assets/*.js 等)也走 cookie
|
|
289
|
+
// 路径——避免「主页 200 但子资源 401」白屏(issue #35)。
|
|
284
290
|
for (const token of list) {
|
|
285
|
-
if (qTok ===
|
|
291
|
+
if (qTok === token) return { ok: true, rawQueryToken: qTok };
|
|
286
292
|
}
|
|
287
293
|
}
|
|
288
|
-
return false;
|
|
294
|
+
return { ok: false, rawQueryToken: null };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 当请求通过 `?token=<原始 PIN>` 进入且尚无 cookie 时,在响应里种 HttpOnly cookie。
|
|
298
|
+
* monkey-patch res.writeHead 把 set-cookie 头注入到第一个响应里——同 POST /pocket-login
|
|
299
|
+
* 路径(lib/proxy.mjs 显式 res.writeHead 加 set-cookie)效果一致。 */
|
|
300
|
+
function maybeSeedAuthCookie(req, res, rawToken, sessionKey) {
|
|
301
|
+
if (!rawToken || !sessionKey) return;
|
|
302
|
+
if (parseCookies(req.headers.cookie)[TOKEN_COOKIE]) return; // 已有 cookie 就不重复种
|
|
303
|
+
const expected = cookieFor(rawToken, sessionKey);
|
|
304
|
+
if (!expected) return;
|
|
305
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
306
|
+
res.writeHead = function (statusCode, headers) {
|
|
307
|
+
const h = { ...(headers ?? {}) };
|
|
308
|
+
const cookie = `${TOKEN_COOKIE}=${expected}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${COOKIE_MAX_AGE}`;
|
|
309
|
+
// 已有 set-cookie 头(数组/字符串都支持)则追加
|
|
310
|
+
const prev = h['set-cookie'];
|
|
311
|
+
if (Array.isArray(prev)) h['set-cookie'] = [...prev, cookie];
|
|
312
|
+
else if (typeof prev === 'string') h['set-cookie'] = [prev, cookie];
|
|
313
|
+
else h['set-cookie'] = cookie;
|
|
314
|
+
return origWriteHead(statusCode, h);
|
|
315
|
+
};
|
|
289
316
|
}
|
|
290
317
|
|
|
291
318
|
/** 把浏览器可见的权威改写成 loopback 权威(Host 和 Origin 都改)。 */
|
|
@@ -483,7 +510,8 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
483
510
|
});
|
|
484
511
|
return;
|
|
485
512
|
}
|
|
486
|
-
|
|
513
|
+
const authResult = authCheck(req, acceptedTokens, sessionKey);
|
|
514
|
+
if (!authResult.ok) {
|
|
487
515
|
if (isHtmlRequest(req)) {
|
|
488
516
|
// 锁定期间打开登录页也给提示(HTTP 200 + 锁定文案;429 语义留给 POST 拒绝)
|
|
489
517
|
const rl = limiter?.status(ip) ?? { locked: false, retryAfter: 0 };
|
|
@@ -495,6 +523,8 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
495
523
|
}
|
|
496
524
|
return;
|
|
497
525
|
}
|
|
526
|
+
// ?token=<原始 PIN> 直达时种 HttpOnly cookie,让浏览器后续子资源也走 cookie 路径(issue #35)
|
|
527
|
+
if (authResult.rawQueryToken) maybeSeedAuthCookie(req, res, authResult.rawQueryToken, sessionKey);
|
|
498
528
|
}
|
|
499
529
|
}
|
|
500
530
|
const headers = loopbackAuthority({ ...req.headers }, upstream);
|
|
@@ -599,7 +629,8 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
599
629
|
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
|
|
600
630
|
const altTokens = token && typeof auth.getAltTokens === 'function' ? (auth.getAltTokens(host) ?? []) : [];
|
|
601
631
|
const acceptedTokens = token ? [token, ...altTokens] : [];
|
|
602
|
-
|
|
632
|
+
const wsAuth = authCheck(req, acceptedTokens, auth.sessionKey ?? null);
|
|
633
|
+
if (token && !wsAuth.ok) {
|
|
603
634
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
604
635
|
socket.destroy();
|
|
605
636
|
return;
|
package/package.json
CHANGED