dsh-pocket 2.10.2 → 2.10.3

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/README.md CHANGED
@@ -118,6 +118,7 @@ npx @deepseek-ai/dsh web
118
118
  - **公网判定是 fail closed**(issue #66):除本机(loopback)和局域网私网地址外,**一切陌生域名(包括你自建隧道/反向代理指向本机端口的固定域名)一律按公网处理、强制公网密码**——不存在「换域名绕过密码」的口子
119
119
  - 局域网模式不暴露公网,只有同一网络内的设备能访问
120
120
  - 适合个人自用;公网密码存本机 `$DSH_HOME/dsh-pocket/token`(默认每次开启公网自动换新,**自定义后不换**),局域网密码存 `$DSH_HOME/dsh-pocket/token-lan`(设置页手动刷新),开关/自定义标记存 `$DSH_HOME/dsh-pocket/settings.json`
121
+ - **CLI 模式(命令行直跑 `dsh-pocket`)也有密码**(issue #90 修复前这条路是无认证的):默认随机生成 8 位密码,打印在终端、并已内嵌进二维码(**扫码体验不变**),手动敲地址时需要填写,本机访问免密。`--pin <值>` 或 `DSH_POCKET_PIN=<值>` 自定义(至少 6 位);`--no-auth` 可关闭,**不推荐**——那等于把能执行代码的 DSH 裸暴露给任何能连上该端口的人
121
122
 
122
123
  ## 💻 DSH Desktop(桌面版)
123
124
 
@@ -10,24 +10,93 @@
10
10
  // 手机看到的界面 = 电脑上的界面,实时同步(WebSocket 流式透传)。
11
11
 
12
12
  import { networkInterfaces } from 'node:os';
13
+ import { realpathSync } from 'node:fs';
14
+ import { pathToFileURL } from 'node:url';
13
15
  import { createRequire } from 'node:module';
14
- import { createPocketProxy } from '../lib/proxy.mjs';
16
+ import { randomBytes, randomInt } from 'node:crypto';
17
+ import { createPocketProxy, classifyHost } from '../lib/proxy.mjs';
15
18
  import { startQuickTunnel } from '../lib/tunnel.mjs';
16
19
 
17
20
  const require = createRequire(import.meta.url);
18
21
 
19
- function parseArgs(argv) {
20
- const args = { port: 3081, host: '0.0.0.0', public: false, upstream: { host: '127.0.0.1', port: 3080 } };
22
+ /** 自定义密码的最短长度(issue #40:别让人设 `1234`)。 */
23
+ export const MIN_PIN_LENGTH = 6;
24
+
25
+ export function parseArgs(argv) {
26
+ const args = {
27
+ port: 3081,
28
+ host: '0.0.0.0',
29
+ public: false,
30
+ upstream: { host: '127.0.0.1', port: 3080 },
31
+ pin: null,
32
+ noAuth: false,
33
+ };
21
34
  for (let i = 0; i < argv.length; i++) {
22
35
  const a = argv[i];
23
36
  if (a === '--public') args.public = true;
24
37
  else if (a === '--port') args.port = Number(argv[++i]) || 3081;
25
38
  else if (a === '--host') args.host = argv[++i] ?? '0.0.0.0';
39
+ else if (a === '--pin') args.pin = String(argv[++i] ?? '');
40
+ else if (a === '--no-auth') args.noAuth = true;
26
41
  else if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
27
42
  }
28
43
  return args;
29
44
  }
30
45
 
46
+ /**
47
+ * 决定本次运行用哪个访问密码(issue #90 第 8 条)。
48
+ *
49
+ * CLI 此前**完全不构造 auth**,且默认监听 `0.0.0.0`——任何能连到这个端口的人都能
50
+ * 直接操作 dsh web,而 dsh web 能在宿主机上执行任意代码。插件版一直有 PIN,
51
+ * 只有 CLI 这条路是裸的,属实是缺陷而非取舍。
52
+ *
53
+ * 修法上刻意**不改默认监听地址**:CLI 的主用法就是「手机连同一 WiFi 扫码」,
54
+ * 默认绑 loopback 等于把这个功能废掉。改成默认生成 PIN,并把它内嵌进二维码的
55
+ * `?token=` —— 扫码体验完全不变,手动敲地址的人才会看到登录页。
56
+ *
57
+ * 优先级:`--pin` > `DSH_POCKET_PIN` > CSPRNG 随机生成。
58
+ * @returns {{ pin: string|null, source: 'flag'|'env'|'generated'|'disabled', error?: string }}
59
+ */
60
+ export function resolvePin({ pin = null, noAuth = false } = {}, env = {}) {
61
+ if (noAuth) return { pin: null, source: 'disabled' };
62
+ const explicit = pin != null && pin !== '' ? { value: String(pin), source: 'flag' }
63
+ : (env.DSH_POCKET_PIN ? { value: String(env.DSH_POCKET_PIN), source: 'env' } : null);
64
+ if (explicit) {
65
+ if (explicit.value.length < MIN_PIN_LENGTH) {
66
+ return {
67
+ pin: null,
68
+ source: explicit.source,
69
+ error: `访问密码至少 ${MIN_PIN_LENGTH} 位(当前 ${explicit.value.length} 位)。`
70
+ + `弱口令在公网上撑不过几分钟 | Access password must be at least ${MIN_PIN_LENGTH} characters.`,
71
+ };
72
+ }
73
+ return { pin: explicit.value, source: explicit.source };
74
+ }
75
+ return { pin: String(randomInt(10_000_000, 100_000_000)), source: 'generated' };
76
+ }
77
+
78
+ /**
79
+ * 构造给 createPocketProxy 的 auth(issue #90 第 8 条)。
80
+ * `isProtected` 与插件版语义保持一致:本机免密(能在本机直连本来就说明已经上了机器),
81
+ * 局域网与公网一律要密码。返回 null 表示不启用认证(`--no-auth`)。
82
+ */
83
+ export function buildAuth(pin) {
84
+ if (!pin) return null;
85
+ return {
86
+ sessionKey: randomBytes(32).toString('hex'),
87
+ isProtected: (host) => classifyHost(host) !== 'loopback',
88
+ getToken: () => pin,
89
+ };
90
+ }
91
+
92
+ /** 把访问密码拼进入口 URL,让二维码扫了就能直达(与插件版一致)。 */
93
+ export function entryUrl(base, pin) {
94
+ if (!pin) return base;
95
+ const u = new URL(base);
96
+ u.searchParams.set('token', pin);
97
+ return u.toString();
98
+ }
99
+
31
100
  function printHelp() {
32
101
  console.log(`dsh-pocket — 手机访问电脑上的 DeepSeek Harness
33
102
 
@@ -35,11 +104,16 @@ function printHelp() {
35
104
  dsh-pocket 局域网模式(手机同一 WiFi)
36
105
  dsh-pocket --public 公网模式(cloudflared 隧道,人在外面)
37
106
  dsh-pocket --port 3081 自定义代理端口
107
+ dsh-pocket --host 自定义监听地址(默认 0.0.0.0)
108
+ dsh-pocket --pin <值> 自定义访问密码(至少 ${MIN_PIN_LENGTH} 位;也可用环境变量 DSH_POCKET_PIN)
109
+ dsh-pocket --no-auth 关闭访问密码(不推荐,见下)
38
110
  dsh-pocket --help 帮助
39
111
 
40
112
  前提:dsh web 已在 127.0.0.1:3080 运行(npx @deepseek-ai/dsh web)。
41
113
 
42
- 安全提醒:dsh web 能执行代码。二维码/URL 就是钥匙,请勿发给别人。
114
+ 安全提醒:dsh web 能在这台机器上执行代码。默认会随机生成一个 8 位访问密码,
115
+ 二维码里已内嵌该密码(扫码直达),手动输入地址时需要填写。本机访问免密。
116
+ --no-auth 会让任何能连到监听端口的人直接控制 dsh web,仅在完全可信的网络里用。
43
117
  `);
44
118
  }
45
119
 
@@ -63,12 +137,32 @@ function printQr(url, label) {
63
137
  async function main() {
64
138
  const args = parseArgs(process.argv.slice(2));
65
139
 
140
+ const resolved = resolvePin(args, process.env);
141
+ if (resolved.error) {
142
+ console.error(`❌ ${resolved.error}`);
143
+ process.exit(1);
144
+ }
145
+ const pin = resolved.pin;
146
+ const auth = buildAuth(pin);
147
+
66
148
  console.log('🚀 dsh-pocket 启动中…');
67
- const { port, close } = await createPocketProxy(args);
149
+ const { port, close } = await createPocketProxy({ ...args, auth });
150
+
151
+ if (pin) {
152
+ console.log(`\n🔐 访问密码:${pin}${resolved.source === 'generated' ? '(本次随机生成)' : ''}`);
153
+ console.log(' 二维码已内嵌密码,扫码直达;手动输入地址时需要填写。本机访问免密。');
154
+ if (resolved.source === 'generated') {
155
+ console.log(` 固定密码:--pin <值> 或 DSH_POCKET_PIN=<值>`);
156
+ }
157
+ } else {
158
+ console.log('\n⚠️ 已用 --no-auth 关闭访问密码。');
159
+ console.log(' dsh web 能在这台机器上执行任意代码——现在任何能连到');
160
+ console.log(` ${args.host}:${args.port} 的人都可以直接控制它。请仅在完全可信的网络里这样用。`);
161
+ }
68
162
 
69
163
  const lan = lanIPv4();
70
164
  if (lan) {
71
- printQr(`http://${lan}:${port}`, '📶 局域网访问(手机连同一 WiFi):');
165
+ printQr(entryUrl(`http://${lan}:${port}`, pin), '📶 局域网访问(手机连同一 WiFi):');
72
166
  } else {
73
167
  console.log('⚠️ 未检测到局域网 IP,跳过局域网二维码');
74
168
  }
@@ -90,7 +184,7 @@ async function main() {
90
184
  console.log('🌐 正在建立公网隧道(cloudflared)…');
91
185
  try {
92
186
  tunnel = await startQuickTunnel({ port, signal: controller.signal });
93
- printQr(tunnel.url, '🌐 公网访问(人在外面也能用):');
187
+ printQr(entryUrl(tunnel.url, pin), '🌐 公网访问(人在外面也能用):');
94
188
  console.log(' 隧道会持续运行;Ctrl+C 退出(下次启动会换新 URL)');
95
189
  } catch (err) {
96
190
  console.error(`❌ 公网隧道失败:${err.message}(局域网二维码仍可用)`);
@@ -103,7 +197,20 @@ async function main() {
103
197
  await new Promise(() => {});
104
198
  }
105
199
 
106
- main().catch((err) => {
107
- console.error(`❌ dsh-pocket: ${err?.message ?? err}`);
108
- process.exit(1);
109
- });
200
+ // 只有被直接执行时才启动(测试里 import 本文件拿纯函数时不能把服务跑起来)。
201
+ // npm 会把 bin 装成 symlink,argv[1] 是 symlink 路径而 import.meta.url 是真实路径,
202
+ // 所以必须先 realpath 再比较,否则装完的 CLI 会变成什么都不做。
203
+ const isDirectRun = (() => {
204
+ try {
205
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1] ?? '')).href;
206
+ } catch {
207
+ return false;
208
+ }
209
+ })();
210
+
211
+ if (isDirectRun) {
212
+ main().catch((err) => {
213
+ console.error(`❌ dsh-pocket: ${err?.message ?? err}`);
214
+ process.exit(1);
215
+ });
216
+ }
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.10.2"
83
+ "version": "2.10.3"
84
84
  }