dsh-pocket 2.1.0 → 2.1.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/index.js +13 -0
- package/lib/proxy.mjs +30 -3
- package/lib/service.mjs +4 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -269,6 +269,19 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
269
269
|
getToken: (host) => tokenForHost(host),
|
|
270
270
|
isProtected: (host) => (classifyHost(host) === 'public' ? true : lanAuthEnabled()),
|
|
271
271
|
},
|
|
272
|
+
// dsh web 浏览器会话启动 token(issue #77):新版 dsh(>= 0.1.2-alpha.1)要求根路径
|
|
273
|
+
// 带一次 `?token=` 换 cookie,否则 /api 与 WebSocket 全 401。token 每次进程启动都变,
|
|
274
|
+
// 所以每次请求实时从 connection 服务取;老版本没有这个方法 → 返回空,行为不变。
|
|
275
|
+
launchToken: () => {
|
|
276
|
+
try {
|
|
277
|
+
const fn = ctx.connection?.authenticatedUrl;
|
|
278
|
+
if (typeof fn !== 'function') return '';
|
|
279
|
+
const url = new URL(fn.call(ctx.connection, `http://127.0.0.1:${dshPort}`));
|
|
280
|
+
return url.searchParams.get('token') ?? '';
|
|
281
|
+
} catch {
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
},
|
|
272
285
|
// 隧道模式(issue #66):'quick'(默认,随机 trycloudflare.com)| 'named'(固定域名)
|
|
273
286
|
getTunnelConfig: () => ({ mode: tunnelMode(), token: tunnelToken(), hostname: tunnelHostname() }),
|
|
274
287
|
// 隧道就绪回调:快速模式每次开启轮换 8 位密码(旧链接作废);命名模式地址固定,
|
package/lib/proxy.mjs
CHANGED
|
@@ -279,6 +279,28 @@ function loopbackAuthority(headers, upstream) {
|
|
|
279
279
|
return headers;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
// ---------- dsh web 浏览器会话 token(issue #77) ----------
|
|
283
|
+
// 新版 dsh web(>= 0.1.2-alpha.1)给浏览器会话加了启动 token:根路径 `GET /` 必须带一次
|
|
284
|
+
// `?token=<启动 token>` 换一个绑定 authority 的 cookie,之后 /api 与 WebSocket 才放行;
|
|
285
|
+
// 否则一律 401("dsh web authentication required")。手机扫码进来的 URL 天然没有这个
|
|
286
|
+
// token,所以代理要在转发时补一次。
|
|
287
|
+
//
|
|
288
|
+
// 只在 `GET /` 且请求还没带 dsh-auth-* cookie 时注入:上游拿到 token 会 303 回干净的根
|
|
289
|
+
// 路径,若每次都注入就会 303 循环(浏览器很快报"重定向次数过多")。
|
|
290
|
+
const DSH_AUTH_COOKIE = 'dsh-auth-';
|
|
291
|
+
export function upstreamPathWithLaunchToken(reqUrl, method, cookieHeader, launchToken) {
|
|
292
|
+
if (method !== 'GET') return reqUrl;
|
|
293
|
+
let u;
|
|
294
|
+
try { u = new URL(reqUrl ?? '/', 'http://dsh.invalid'); } catch { return reqUrl; }
|
|
295
|
+
if (u.pathname !== '/') return reqUrl;
|
|
296
|
+
// 登录成功后跳回的 `/?dsh-pocket-auth=1`:强制重做一次握手(旧 cookie 可能已过期/被撤销)
|
|
297
|
+
const force = u.searchParams.has('dsh-pocket-auth');
|
|
298
|
+
if (!force && String(cookieHeader ?? '').includes(DSH_AUTH_COOKIE)) return reqUrl;
|
|
299
|
+
if (!launchToken) return reqUrl;
|
|
300
|
+
u.searchParams.set('token', launchToken);
|
|
301
|
+
return `${u.pathname}${u.search}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
282
304
|
// ---------- WebSocket 心跳注入(PR #41,issue #29) ----------
|
|
283
305
|
// DSH 客户端与宿主的 WebSocket downlink 都不发 ping/pong(客户端只读流、
|
|
284
306
|
// 宿主只推帧),空闲连接会被路由器 NAT 空闲超时或手机系统省电机制**静默**
|
|
@@ -348,7 +370,7 @@ function attachWebSocketHeartbeat(socket, { intervalMs = 30_000, missLimit = 2 }
|
|
|
348
370
|
* @param {() => boolean} [opts.lanAccessEnabled] 局域网访问是否开启(默认开启)。关闭时拦截经局域网 Host 的请求(公网/loopback 不受影响)。
|
|
349
371
|
* @returns {Promise<{server:import('node:http').Server, close:()=>Promise<void>}>}
|
|
350
372
|
*/
|
|
351
|
-
export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true } = {}) {
|
|
373
|
+
export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true, launchToken = () => '' } = {}) {
|
|
352
374
|
const limiter = auth ? createRateLimiter(rateLimit ?? {}) : null;
|
|
353
375
|
const server = createServer((req, res) => {
|
|
354
376
|
const host = String(req.headers.host ?? '');
|
|
@@ -392,7 +414,9 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
392
414
|
if (submitted === token) {
|
|
393
415
|
limiter?.clear(ip);
|
|
394
416
|
res.writeHead(302, {
|
|
395
|
-
|
|
417
|
+
// 带上 dsh-pocket-auth=1:登录成功后强制重做一次浏览器会话握手,
|
|
418
|
+
// 换掉可能已过期/被撤销的 dsh web 会话 cookie(issue #77)
|
|
419
|
+
location: '/?dsh-pocket-auth=1',
|
|
396
420
|
'set-cookie': `${TOKEN_COOKIE}=${cookieFor(token, sessionKey)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${COOKIE_MAX_AGE}`,
|
|
397
421
|
'cache-control': 'no-store',
|
|
398
422
|
});
|
|
@@ -421,8 +445,11 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
421
445
|
}
|
|
422
446
|
}
|
|
423
447
|
const headers = loopbackAuthority({ ...req.headers }, upstream);
|
|
448
|
+
// dsh web 浏览器会话 token(issue #77):首屏根路径补一次,换回绑定 authority 的 cookie
|
|
449
|
+
const launchTok = (typeof launchToken === 'function' ? launchToken() : '') || '';
|
|
450
|
+
const upstreamPath = upstreamPathWithLaunchToken(req.url, req.method, req.headers.cookie, launchTok);
|
|
424
451
|
const proxyReq = httpRequest(
|
|
425
|
-
{ host: upstream.host, port: upstream.port, method: req.method, path:
|
|
452
|
+
{ host: upstream.host, port: upstream.port, method: req.method, path: upstreamPath, headers, agent: false },
|
|
426
453
|
(proxyRes) => {
|
|
427
454
|
log?.(`${req.method} ${req.url} -> ${proxyRes.statusCode}`);
|
|
428
455
|
const contentType = String(proxyRes.headers['content-type'] ?? '');
|
package/lib/service.mjs
CHANGED
|
@@ -180,6 +180,8 @@ export function createPocketService({
|
|
|
180
180
|
injectHtml,
|
|
181
181
|
/** 访问令牌认证配置(issue #13):{ getToken, isProtected },传给代理 */
|
|
182
182
|
auth,
|
|
183
|
+
/** @type {() => string} dsh web 浏览器会话启动 token(issue #77;老版本返回空字符串) */
|
|
184
|
+
launchToken = () => '',
|
|
183
185
|
/** 隧道配置(issue #66):() => ({ mode:'quick'|'named', token, hostname });named 用固定域名 */
|
|
184
186
|
getTunnelConfig,
|
|
185
187
|
/** 隧道就绪回调(lib/index.js 用它轮换公网密码;参数为 'quick'|'named') */
|
|
@@ -257,6 +259,8 @@ export function createPocketService({
|
|
|
257
259
|
...(auth ? { auth } : {}),
|
|
258
260
|
// 每次请求实时读开关:设置页切换后立即生效,无需重启代理
|
|
259
261
|
lanAccessEnabled: () => getLanEnabled(),
|
|
262
|
+
// dsh web 浏览器会话启动 token(issue #77):实时取,新版 dsh 才有
|
|
263
|
+
...(launchToken ? { launchToken } : {}),
|
|
260
264
|
});
|
|
261
265
|
if (p !== port) {
|
|
262
266
|
console.log(`dsh-pocket: port ${port} busy, proxy on ${p} | 端口 ${port} 被占用,代理改用 ${p}`);
|
package/package.json
CHANGED