dsh-pocket 1.7.5 → 1.8.0
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/api.js +1 -0
- package/client/client.js +18 -2
- package/client/index.jsx +16 -0
- package/lib/index.js +48 -21
- package/lib/proxy.mjs +2 -2
- package/lib/web-rpc.js +15 -3
- package/package.json +1 -1
package/client/api.js
CHANGED
package/client/client.js
CHANGED
|
@@ -47,7 +47,8 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
47
47
|
tunnelStop: "tunnel.stop",
|
|
48
48
|
version: "pocket.version",
|
|
49
49
|
update: "pocket.update",
|
|
50
|
-
restart: "pocket.restart"
|
|
50
|
+
restart: "pocket.restart",
|
|
51
|
+
lanTokenRefresh: "token.lanRefresh"
|
|
51
52
|
});
|
|
52
53
|
function compareVersions(a, b) {
|
|
53
54
|
const pa = String(a).replace(/^[vV]/, "").split(".");
|
|
@@ -1429,6 +1430,13 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1429
1430
|
} catch {
|
|
1430
1431
|
}
|
|
1431
1432
|
};
|
|
1433
|
+
const refreshLanPin = async () => {
|
|
1434
|
+
try {
|
|
1435
|
+
const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
|
|
1436
|
+
setStatus((s) => ({ ...s, lanToken: r.lanToken }));
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1432
1440
|
const lanUrl = status?.lanUrl;
|
|
1433
1441
|
const tunnelUrl = status?.tunnelUrl;
|
|
1434
1442
|
const tunnelPhase = tunnelState?.phase ?? "idle";
|
|
@@ -1503,7 +1511,15 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1503
1511
|
null,
|
|
1504
1512
|
(0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
|
|
1505
1513
|
(0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
|
|
1506
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00")
|
|
1514
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00"),
|
|
1515
|
+
status.lanToken ? (0, import_react2.createElement)(
|
|
1516
|
+
"div",
|
|
1517
|
+
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
|
|
1518
|
+
"\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A",
|
|
1519
|
+
status.lanToken,
|
|
1520
|
+
"\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
|
|
1521
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
|
|
1522
|
+
) : null
|
|
1507
1523
|
) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
|
|
1508
1524
|
),
|
|
1509
1525
|
// 公网
|
package/client/index.jsx
CHANGED
|
@@ -167,6 +167,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
167
167
|
try { setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {})); } catch { /* 忽略 */ }
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
+
// 刷新局域网访问密码(旧密码立即作废)
|
|
171
|
+
const refreshLanPin = async () => {
|
|
172
|
+
try {
|
|
173
|
+
const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
|
|
174
|
+
setStatus((s) => ({ ...s, lanToken: r.lanToken }));
|
|
175
|
+
} catch { /* 忽略 */ }
|
|
176
|
+
};
|
|
177
|
+
|
|
170
178
|
const lanUrl = status?.lanUrl;
|
|
171
179
|
const tunnelUrl = status?.tunnelUrl;
|
|
172
180
|
const tunnelPhase = tunnelState?.phase ?? 'idle';
|
|
@@ -235,6 +243,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
235
243
|
h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
|
|
236
244
|
h('div', { style: styles.code }, lanUrl),
|
|
237
245
|
h('div', { style: styles.muted }, '手机连接同一 WiFi 后扫码即可打开'),
|
|
246
|
+
status.lanToken
|
|
247
|
+
? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
|
|
248
|
+
'🔐 访问密码:',
|
|
249
|
+
status.lanToken,
|
|
250
|
+
'(手机打开需输入;与公网密码分开)',
|
|
251
|
+
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, '刷新 | Refresh'),
|
|
252
|
+
)
|
|
253
|
+
: null,
|
|
238
254
|
)
|
|
239
255
|
: h('div', { style: styles.muted }, '代理未就绪… | proxy starting…'),
|
|
240
256
|
),
|
package/lib/index.js
CHANGED
|
@@ -41,33 +41,58 @@ function currentVersion() {
|
|
|
41
41
|
/** 进程启动时加载的版本(模块加载瞬间固化;用于识别「磁盘已更新但进程还是旧代码」)。 */
|
|
42
42
|
const loadedVersion = currentVersion();
|
|
43
43
|
|
|
44
|
-
// ----------
|
|
45
|
-
// 8 位数字,存本机 $DSH_HOME/dsh-pocket
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
function
|
|
49
|
-
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
|
|
50
|
-
}
|
|
51
|
-
/** 生成 8 位数字密码并写文件(同步,保证后续读取一致)。 */
|
|
52
|
-
function writeNewToken() {
|
|
53
|
-
const fresh = String(Math.floor(10000000 + Math.random() * 90000000));
|
|
44
|
+
// ---------- 访问密码(issue #13 + #18) ----------
|
|
45
|
+
// 公网与局域网**分开**:各自 8 位数字,存本机 $DSH_HOME/dsh-pocket/。
|
|
46
|
+
// 公网密码(token):每次开启公网时轮换(旧链接作废);
|
|
47
|
+
// 局域网密码(token-lan):手动刷新(设置页按钮)。
|
|
48
|
+
function writePinToFile(p, fresh) {
|
|
54
49
|
try {
|
|
55
|
-
mkdirSync(dirname(
|
|
56
|
-
writeFileSync(
|
|
50
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
51
|
+
writeFileSync(p, fresh, { mode: 0o600 });
|
|
57
52
|
} catch { /* 忽略 */ }
|
|
58
53
|
return fresh;
|
|
59
54
|
}
|
|
60
|
-
|
|
61
|
-
function getAccessToken() {
|
|
55
|
+
function readPin(p) {
|
|
62
56
|
try {
|
|
63
|
-
const existing = readFileSync(
|
|
57
|
+
const existing = readFileSync(p, 'utf8').trim();
|
|
64
58
|
if (/^\d{8}$/.test(existing)) return existing;
|
|
65
59
|
} catch { /* 无文件 */ }
|
|
66
|
-
return
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function newPin() {
|
|
63
|
+
return String(Math.floor(10000000 + Math.random() * 90000000));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- 公网密码 ---
|
|
67
|
+
const tokenRel = join('dsh-pocket', 'token');
|
|
68
|
+
function tokenPath() {
|
|
69
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
|
|
70
|
+
}
|
|
71
|
+
/** 当前公网访问密码;文件不存在时生成一个。 */
|
|
72
|
+
function getAccessToken() {
|
|
73
|
+
return readPin(tokenPath()) ?? writePinToFile(tokenPath(), newPin());
|
|
67
74
|
}
|
|
68
|
-
/**
|
|
75
|
+
/** 轮换公网密码(开启公网时调用,旧密码/链接立即作废)。 */
|
|
69
76
|
function rotateAccessToken() {
|
|
70
|
-
return
|
|
77
|
+
return writePinToFile(tokenPath(), newPin());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- 局域网密码(issue #18:局域网与公网分开,手动刷新) ---
|
|
81
|
+
const lanTokenRel = join('dsh-pocket', 'token-lan');
|
|
82
|
+
function lanTokenPath() {
|
|
83
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), lanTokenRel);
|
|
84
|
+
}
|
|
85
|
+
/** 当前局域网访问密码;文件不存在时生成一个。 */
|
|
86
|
+
function getLanToken() {
|
|
87
|
+
return readPin(lanTokenPath()) ?? writePinToFile(lanTokenPath(), newPin());
|
|
88
|
+
}
|
|
89
|
+
/** 刷新局域网密码(设置页按钮触发,旧密码立即作废)。 */
|
|
90
|
+
function refreshLanToken() {
|
|
91
|
+
return writePinToFile(lanTokenPath(), newPin());
|
|
92
|
+
}
|
|
93
|
+
/** 按 Host 分发的访问密码:公网隧道用公网密码,局域网用局域网密码。 */
|
|
94
|
+
function tokenForHost(host) {
|
|
95
|
+
return /trycloudflare\.com$/i.test(String(host ?? '')) ? getAccessToken() : getLanToken();
|
|
71
96
|
}
|
|
72
97
|
|
|
73
98
|
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
@@ -177,10 +202,10 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
177
202
|
injectHtml: isDesktop
|
|
178
203
|
? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
|
|
179
204
|
: undefined,
|
|
180
|
-
//
|
|
205
|
+
// 访问密码(issue #13 + #18):局域网与公网**分开**,所有代理访问都要验证
|
|
181
206
|
auth: {
|
|
182
|
-
getToken: () =>
|
|
183
|
-
isProtected: (
|
|
207
|
+
getToken: (host) => tokenForHost(host),
|
|
208
|
+
isProtected: () => true,
|
|
184
209
|
},
|
|
185
210
|
// 每次公网隧道就绪 → 轮换 8 位密码(旧密码/旧链接立即作废)
|
|
186
211
|
onTunnelReady: () => {
|
|
@@ -195,6 +220,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
195
220
|
service,
|
|
196
221
|
desktop: isDesktop,
|
|
197
222
|
getToken: () => getAccessToken(),
|
|
223
|
+
getLanToken: () => getLanToken(),
|
|
224
|
+
refreshLanToken: () => refreshLanToken(),
|
|
198
225
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
199
226
|
restart: internals.restart ?? (() => pocketRestart(service)),
|
|
200
227
|
restartNotice: internals.restartNotice ?? consumeRestartNotice,
|
package/lib/proxy.mjs
CHANGED
|
@@ -145,7 +145,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
145
145
|
if (auth) {
|
|
146
146
|
const host = String(req.headers.host ?? '');
|
|
147
147
|
const protectedHost = isProtectedHost(host, auth.isProtected);
|
|
148
|
-
const token = protectedHost ? (auth.getToken?.() ?? null) : null;
|
|
148
|
+
const token = protectedHost ? (auth.getToken?.(host) ?? null) : null;
|
|
149
149
|
if (protectedHost && token) {
|
|
150
150
|
// 登录提交:校验密码 → 种 HttpOnly 会话 cookie → 回首页
|
|
151
151
|
if (req.method === 'POST' && req.url?.startsWith('/pocket-login')) {
|
|
@@ -258,7 +258,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
258
258
|
// WebSocket 同样校验(防止绕过 HTTP 认证从 WS 进入)
|
|
259
259
|
if (auth) {
|
|
260
260
|
const host = String(req.headers.host ?? '');
|
|
261
|
-
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.() ?? null) : null;
|
|
261
|
+
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
|
|
262
262
|
if (token && !authCheck(req, token)) {
|
|
263
263
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
264
264
|
socket.destroy();
|
package/lib/web-rpc.js
CHANGED
|
@@ -25,7 +25,7 @@ export function killHint(port) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
28
|
-
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null }) {
|
|
28
|
+
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null }) {
|
|
29
29
|
if (!ctx?.connection?.rpc?.handle) {
|
|
30
30
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
31
31
|
return () => {};
|
|
@@ -33,18 +33,30 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
33
33
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
34
34
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
35
35
|
|
|
36
|
-
// status
|
|
36
|
+
// status 响应:服务状态 + 重启提示 + 停止命令 + 桌面端标志 + 公网/局域网访问密码
|
|
37
37
|
const statusPayload = async () => {
|
|
38
38
|
let notice = null;
|
|
39
39
|
try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
|
|
40
40
|
const s = await service.status();
|
|
41
|
-
return ok({
|
|
41
|
+
return ok({
|
|
42
|
+
...redactStatus(s),
|
|
43
|
+
desktop,
|
|
44
|
+
restartNotice: notice,
|
|
45
|
+
killHint: killHint(s.dshPort ?? 3080),
|
|
46
|
+
accessToken: getToken?.() ?? null,
|
|
47
|
+
lanToken: getLanToken?.() ?? null,
|
|
48
|
+
});
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
try {
|
|
45
52
|
if (endpoint === POCKET_ENDPOINTS.status) {
|
|
46
53
|
return await statusPayload();
|
|
47
54
|
}
|
|
55
|
+
if (endpoint === POCKET_ENDPOINTS.lanTokenRefresh) {
|
|
56
|
+
const fresh = refreshLanToken?.() ?? null;
|
|
57
|
+
if (!fresh) return fail('bad-request', '局域网密码刷新不可用 | LAN PIN refresh unavailable');
|
|
58
|
+
return ok({ lanToken: fresh });
|
|
59
|
+
}
|
|
48
60
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
49
61
|
await service.startTunnel();
|
|
50
62
|
return await statusPayload();
|
package/package.json
CHANGED