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.
Files changed (2) hide show
  1. package/lib/proxy.mjs +38 -7
  2. 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
- /** 校验请求是否已认证;返回 true 放行。`tokens` 允许传多个(主 PIN + 临时 PIN)。 */
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; // token(未配置密码)→ 放行
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 === cookieFor(token, sessionKey)) return true;
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
- if (!authCheck(req, acceptedTokens, sessionKey)) {
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
- if (token && !authCheck(req, acceptedTokens, auth.sessionKey ?? null)) {
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
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.5.0"
83
+ "version": "2.5.1"
84
84
  }