dsh-pocket 2.9.0 → 2.9.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/client/client.js +7 -0
- package/client/mobile/sessionGuard.ts +11 -0
- package/lib/proxy.mjs +9 -4
- package/lib/tunnel.mjs +6 -6
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -505,11 +505,16 @@ function startFileGuard(readFile) {
|
|
|
505
505
|
|
|
506
506
|
// client/mobile/sessionGuard.ts
|
|
507
507
|
var SESSION_META = "dsh-pocket-session";
|
|
508
|
+
var ACCESS_META = "dsh-pocket-access";
|
|
508
509
|
function readFingerprint() {
|
|
509
510
|
const meta = document.querySelector(`meta[name="${SESSION_META}"]`);
|
|
510
511
|
const fp = meta?.content?.trim();
|
|
511
512
|
return fp && fp.length > 0 ? fp : null;
|
|
512
513
|
}
|
|
514
|
+
function readAccess() {
|
|
515
|
+
const meta = document.querySelector(`meta[name="${ACCESS_META}"]`);
|
|
516
|
+
return meta?.content?.trim() ?? null;
|
|
517
|
+
}
|
|
513
518
|
function mountBadge(fp) {
|
|
514
519
|
const badge = document.createElement("div");
|
|
515
520
|
badge.setAttribute("data-mobile-nav", "session-fp");
|
|
@@ -556,6 +561,8 @@ function mountWarning() {
|
|
|
556
561
|
return () => overlay.remove();
|
|
557
562
|
}
|
|
558
563
|
function startSessionGuard() {
|
|
564
|
+
if (readAccess() !== "public") return () => {
|
|
565
|
+
};
|
|
559
566
|
let cleanupBadge = null;
|
|
560
567
|
let cleanupWarn = null;
|
|
561
568
|
const evaluate = () => {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// 弹红色全屏警告并拦截交互,防止用户在被钓鱼页输入 8 位密码。
|
|
13
13
|
|
|
14
14
|
const SESSION_META = 'dsh-pocket-session'
|
|
15
|
+
const ACCESS_META = 'dsh-pocket-access'
|
|
15
16
|
|
|
16
17
|
function readFingerprint(): string | null {
|
|
17
18
|
const meta = document.querySelector<HTMLMetaElement>(`meta[name="${SESSION_META}"]`)
|
|
@@ -19,6 +20,12 @@ function readFingerprint(): string | null {
|
|
|
19
20
|
return fp && fp.length > 0 ? fp : null
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** 读取代理注入的访问类型标记(public | lan)。 */
|
|
24
|
+
function readAccess(): string | null {
|
|
25
|
+
const meta = document.querySelector<HTMLMetaElement>(`meta[name="${ACCESS_META}"]`)
|
|
26
|
+
return meta?.content?.trim() ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function mountBadge(fp: string): () => void {
|
|
23
30
|
const badge = document.createElement('div')
|
|
24
31
|
badge.setAttribute('data-mobile-nav', 'session-fp')
|
|
@@ -52,6 +59,10 @@ function mountWarning(): () => void {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export function startSessionGuard(): () => void {
|
|
62
|
+
// 局域网访问不存在「快速隧道子域被 Cloudflare 回收复用」的风险(issue #83),
|
|
63
|
+
// 防钓鱼校验只在公网启用;局域网直接不挂,避免误报阻断用户使用。
|
|
64
|
+
if (readAccess() !== 'public') return () => {}
|
|
65
|
+
|
|
55
66
|
let cleanupBadge: (() => void) | null = null
|
|
56
67
|
let cleanupWarn: (() => void) | null = null
|
|
57
68
|
const evaluate = (): void => {
|
package/lib/proxy.mjs
CHANGED
|
@@ -494,6 +494,11 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
494
494
|
const server = createServer((req, res) => {
|
|
495
495
|
const host = String(req.headers.host ?? '');
|
|
496
496
|
const isPublic = classifyHost(host) === 'public';
|
|
497
|
+
// 访问类型标记(issue #83):sessionGuard 只在公网启用——局域网 IP 是用户自己电脑的地址,
|
|
498
|
+
// 不存在「快速隧道子域被 Cloudflare 回收复用」的风险,不应弹防钓鱼警告。
|
|
499
|
+
const accessMeta = isPublic
|
|
500
|
+
? '<meta name="dsh-pocket-access" content="public">'
|
|
501
|
+
: '<meta name="dsh-pocket-access" content="lan">';
|
|
497
502
|
// 局域网访问关闭(issue #54):拦截经局域网 IP/主机名访问的请求;
|
|
498
503
|
// 公网(含任意非内网 Host——issue #66 fail closed)与 loopback(本机/cloudflared 回连)放行,
|
|
499
504
|
// 公网流量随后照常走访问密码认证。
|
|
@@ -526,7 +531,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
526
531
|
'cache-control': 'no-store',
|
|
527
532
|
'retry-after': String(rl.retryAfter),
|
|
528
533
|
});
|
|
529
|
-
res.end(loginPageHtml('locked', isPublic, rl.retryAfter, sessionFingerprint));
|
|
534
|
+
res.end(loginPageHtml('locked', isPublic, rl.retryAfter, isPublic ? sessionFingerprint : null));
|
|
530
535
|
return;
|
|
531
536
|
}
|
|
532
537
|
let body = '';
|
|
@@ -547,7 +552,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
547
552
|
limiter?.record(ip);
|
|
548
553
|
log?.(`dsh-pocket: login failed from ${ip} | 登录失败 IP: ${ip}`);
|
|
549
554
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
550
|
-
res.end(loginPageHtml(true, isPublic, 0, sessionFingerprint));
|
|
555
|
+
res.end(loginPageHtml(true, isPublic, 0, isPublic ? sessionFingerprint : null));
|
|
551
556
|
}
|
|
552
557
|
});
|
|
553
558
|
return;
|
|
@@ -558,7 +563,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
558
563
|
// 锁定期间打开登录页也给提示(HTTP 200 + 锁定文案;429 语义留给 POST 拒绝)
|
|
559
564
|
const rl = limiter?.status(ip) ?? { locked: false, retryAfter: 0 };
|
|
560
565
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
561
|
-
res.end(loginPageHtml(rl.locked ? 'locked' : false, isPublic, rl.retryAfter, sessionFingerprint));
|
|
566
|
+
res.end(loginPageHtml(rl.locked ? 'locked' : false, isPublic, rl.retryAfter, isPublic ? sessionFingerprint : null));
|
|
562
567
|
} else {
|
|
563
568
|
res.writeHead(401, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
564
569
|
res.end('{"error":"unauthorized"}');
|
|
@@ -631,7 +636,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
631
636
|
// 防密码被钓(issue #82):注入会话指纹 meta(与登录页同源)。
|
|
632
637
|
// 仅当页面尚无该标记时注入,避免重复;不存在 <head> 时跳过(dsh web 必有)。
|
|
633
638
|
if (!html.includes(SESSION_MARK)) {
|
|
634
|
-
html = html.replace(/<head[^>]*>/i, (m) => `${m}${sessionMeta}`);
|
|
639
|
+
html = html.replace(/<head[^>]*>/i, (m) => `${m}${sessionMeta}${accessMeta}`);
|
|
635
640
|
}
|
|
636
641
|
if (!html.includes(INJECT_MARK)) {
|
|
637
642
|
html = html.replace(/<head[^>]*>/i, (m) => `${m}${injectHtml}`);
|
package/lib/tunnel.mjs
CHANGED
|
@@ -27,15 +27,15 @@ export const QUICK_TUNNEL_URL_RE = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare
|
|
|
27
27
|
*
|
|
28
28
|
* cloudflared 参数错误(如 "Incorrect Usage: flag provided but not defined")的
|
|
29
29
|
* 关键信息在输出**开头**,尾部整段都是 usage 帮助文本(对用户没用);而运行期错误
|
|
30
|
-
* (403 / 协议 /
|
|
30
|
+
* (403 / 协议 / 网络)的关键信息在**尾部**。所以:参数错误取该行,其它仍取尾部。
|
|
31
31
|
* 最多 500 字符,与历史上限一致。
|
|
32
32
|
*/
|
|
33
33
|
export function firstMeaningfulErrorLine(buf) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
34
|
+
const lines = String(buf ?? '').trim().split(/\r?\n/);
|
|
35
|
+
// 命中哪行就返回哪行:cloudflared 先打版本横幅时,参数错误未必是首行
|
|
36
|
+
const usageIdx = lines.findIndex((l) => /^(?:Incorrect Usage|flag provided but not defined|unknown flag|unknown command)/i.test(l.trim()));
|
|
37
|
+
if (usageIdx >= 0) return lines[usageIdx].trim().slice(0, 500);
|
|
38
|
+
return lines.slice(-4).join('\n').trim().slice(0, 500);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function platformBinary() {
|
package/package.json
CHANGED