dsh-pocket 2.4.0 → 2.5.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/README.en.md CHANGED
@@ -134,6 +134,7 @@ Note: in named-tunnel mode the public PIN is **not auto-rotated** (the address i
134
134
  | `listen EADDRINUSE ... :3081` | A stale dsh-pocket process holds the port: macOS/Linux `lsof -ti :3081 \| xargs kill -9`; Windows `netstat -ano \| findstr :3081` (find the LISTENING PID) → `taskkill /PID <PID> /F`, then retry |
135
135
  | Want a different port (issue #70) | Plugin mode: write `"proxyPort": 3082` into `$DSH_HOME/dsh-pocket/settings.json` and restart `dsh web`. CLI mode: `dsh-pocket --port 3082`. If the port is taken you'll get `EADDRINUSE` — kill the old process or pick another one |
136
136
  | Issue a temporary PIN to a guest (issue #69) | Settings → "Temporary access PINs" → pick public/LAN + duration (1h/24h/7d) + note → Create. Share the 8-char PIN + the access URL with the guest; it auto-expires and can be revoked manually. Temp PINs share the main PIN rate limit |
137
+ | cloudflared install fails on a remote Linux server (issue #45) | If all CDN sources (GitHub / ghproxy / gh.ddlc / gh-proxy) are unreachable on a remote Linux host, install `cloudflared` yourself (e.g. `apt install cloudflared`, `dnf install cloudflared`, or download the tgz and unpack it), then add `"cloudflaredPath": "/path/to/cloudflared"` into `$DSH_HOME/dsh-pocket/settings.json` and restart `dsh web`. The plugin will then use that binary directly and skip the auto-download |
137
138
  | Version stuck below 1.x | `^0.x` ranges never jump to 1.x: update with `--latest` (`dsh plugin --profile web update dsh-pocket --latest -w`) |
138
139
  | Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
139
140
  | After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it: macOS/Linux `lsof -ti :3080 \| xargs kill -9`; Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
@@ -193,7 +194,7 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
193
194
  ```sh
194
195
  npm install
195
196
  node client/build.mjs # rebuild after editing client/
196
- npm test # proxy / auth / compression / tunnel / service / RPC / settings (93 tests)
197
+ npm test # proxy / auth / compression / tunnel / service / RPC / settings (105 tests)
197
198
  ```
198
199
 
199
200
  **Want to try your changes locally without publishing?** Point the installed plugin at your local checkout with a symlink and restart dsh web. Full steps (including switching back to the npm release) are in [LOCAL-DEV.md](./LOCAL-DEV.md).
package/README.md CHANGED
@@ -134,6 +134,7 @@ npx @deepseek-ai/dsh web
134
134
  | `listen EADDRINUSE ... :3081` | 旧 dsh-pocket 进程还占着端口:macOS/Linux `lsof -ti :3081 \| xargs kill -9`;Windows `netstat -ano \| findstr :3081`(找 LISTENING 的 PID)→ `taskkill /PID <PID> /F`,后重试 |
135
135
  | 想换端口(issue #70) | 插件模式:在 `$DSH_HOME/dsh-pocket/settings.json` 写 `"proxyPort": 3082` 后重启 `dsh web`。CLI 模式:`dsh-pocket --port 3082`。端口被占会报 `EADDRINUSE`,杀掉旧进程或换一个端口 |
136
136
  | 给访客一个临时 PIN(issue #69) | 设置页「临时访问 PIN」区块 → 选公网/局域网 + 时长(1h/24h/7d) + 备注 → 生成。把 8 位 PIN + 入口 URL 一起发给对方;过期自动作废,也能手动撤销。临时 PIN 与主 PIN 共用速率限制 |
137
+ | Linux 服务器装不上 cloudflared(issue #45) | 远程 Linux 国内/企业网下所有 CDN 源(GitHub/ghproxy/gh.ddlc/gh-proxy)都连不上时:在服务器上手动装 `cloudflared`(如 `apt install cloudflared`、`dnf install cloudflared`、或下载 tgz 解压到任意目录),然后在 `$DSH_HOME/dsh-pocket/settings.json` 加 `"cloudflaredPath": "/path/to/cloudflared"`,重启 `dsh web` 后插件直接调用它,**不再走自动下载** |
137
138
  | 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
138
139
  | 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
139
140
  | 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:macOS/Linux `lsof -ti :3080 \| xargs kill -9`;Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
@@ -194,7 +195,7 @@ npx @deepseek-ai/dsh web
194
195
  ```sh
195
196
  npm install
196
197
  node client/build.mjs # 改 client/ 后重新打包
197
- npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(93 测试)
198
+ npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(105 测试)
198
199
  ```
199
200
 
200
201
  **改完想在本机先试?** 不用发版:把插件换成指向本地仓库的软链,重启 dsh web 就是本地代码。完整步骤(含怎么换回 npm 官方版本)见 [LOCAL-DEV.md](./LOCAL-DEV.md)。
package/lib/index.js CHANGED
@@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs';
21
21
  import { installPocketRpc } from './web-rpc.js';
22
22
  import { restartHost } from './restart.js';
23
23
  import { advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
24
- import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, tempPinValuesFor, tempPins as readTempPins, createTempPin, revokeTempPin } from './settings.mjs';
24
+ import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, tempPinValuesFor, tempPins as readTempPins, createTempPin, revokeTempPin, cloudflaredPath } from './settings.mjs';
25
25
 
26
26
  const name = 'dsh-pocket';
27
27
  const inject = ['connection', 'webServer'];
@@ -220,6 +220,15 @@ export function apply(ctx, config = {}, internals = {}) {
220
220
  return () => {};
221
221
  }
222
222
 
223
+ // cloudflared 自定义路径(issue #45):远程 Linux 服务器下载源全失败时,
224
+ // 让用户能在 settings.json 写死一个已经存在的二进制路径,绕过自动下载。
225
+ // 同步到 env 让 lib/tunnel.mjs 的 resolveCloudflared 第一时间读到。
226
+ const cfPath = cloudflaredPath();
227
+ if (cfPath) {
228
+ process.env.DSH_POCKET_CLOUDFLARED = cfPath;
229
+ logger.info(`dsh-pocket: using custom cloudflared at ${cfPath} (skipping auto-download) | 使用自定义 cloudflared 路径(跳过自动下载)`);
230
+ }
231
+
223
232
  // 桌面端环境识别(官方兼容模式,见 desktop 的 plugin-development.md):
224
233
  // desktopProfiles / desktopPnpm 只在 DSH Desktop(Electron)里存在。
225
234
  // 桌面端有自己的更新/进程管理,我们这两项功能在此环境**关闭**(不删除),
package/lib/proxy.mjs CHANGED
@@ -306,6 +306,36 @@ function loopbackAuthority(headers, upstream) {
306
306
  // 只在 `GET /` 且请求还没带 dsh-auth-* cookie 时注入:上游拿到 token 会 303 回干净的根
307
307
  // 路径,若每次都注入就会 303 循环(浏览器很快报"重定向次数过多")。
308
308
  const DSH_AUTH_COOKIE = 'dsh-auth-';
309
+ /**
310
+ * 去掉 URL 上所有 `dsh-desktop-*` query 参数(issue #75)。
311
+ *
312
+ * dsh-pocket 在 ≤ 2.1.1 会用 `history.replaceState` 往页面 URL 上写
313
+ * `dsh-desktop-mode=compatibility` 和 `dsh-desktop-platform=<系统>`
314
+ * (desktopEnvPatchScript,已在 2.1.2 删除)。副作用是:用户当时收藏/保存过
315
+ * 的那个地址**一直带着这两个参数**。升级之后我们不再注入了,但用户打开旧
316
+ * 收藏时 URL 里仍然有 —— 上游 `decideDesktopBrowserAccess` 只要见到
317
+ * `dsh-desktop-` 前缀就认定是渲染器请求,普通浏览器没有渲染器 token,直接
318
+ * 403 forbidden。表现就是「我已经升到最新版了,还是 forbidden」。
319
+ *
320
+ * 脏参数是我们写进去的,就得由我们清掉。所有方法、所有路径都清理(不只是
321
+ * `GET /`)——API 与 WebSocket 握手带上这些参数同样会被拦。
322
+ *
323
+ * @param {string} reqUrl - 原始请求路径(含 query)。
324
+ * @returns {string} 清理后的路径;无该前缀参数或解析失败时原样返回。
325
+ */
326
+ export function stripDesktopMarkers(reqUrl) {
327
+ let u;
328
+ try {
329
+ u = new URL(reqUrl ?? '/', 'http://dsh.invalid');
330
+ } catch {
331
+ return reqUrl;
332
+ }
333
+ const doomed = [...u.searchParams.keys()].filter((k) => k.startsWith('dsh-desktop-'));
334
+ if (doomed.length === 0) return reqUrl;
335
+ for (const key of doomed) u.searchParams.delete(key);
336
+ return `${u.pathname}${u.search}`;
337
+ }
338
+
309
339
  export function upstreamPathWithLaunchToken(reqUrl, method, cookieHeader, launchToken) {
310
340
  if (method !== 'GET') return reqUrl;
311
341
  let u;
@@ -470,7 +500,13 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
470
500
  const headers = loopbackAuthority({ ...req.headers }, upstream);
471
501
  // dsh web 浏览器会话 token(issue #77):首屏根路径补一次,换回绑定 authority 的 cookie
472
502
  const launchTok = (typeof launchToken === 'function' ? launchToken() : '') || '';
473
- const upstreamPath = upstreamPathWithLaunchToken(req.url, req.method, req.headers.cookie, launchTok);
503
+ // 先清掉历史遗留的 dsh-desktop-* 参数(issue #75),再补 launch token
504
+ const upstreamPath = upstreamPathWithLaunchToken(
505
+ stripDesktopMarkers(req.url),
506
+ req.method,
507
+ req.headers.cookie,
508
+ launchTok,
509
+ );
474
510
  const proxyReq = httpRequest(
475
511
  { host: upstream.host, port: upstream.port, method: req.method, path: upstreamPath, headers, agent: false },
476
512
  (proxyRes) => {
@@ -571,7 +607,8 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
571
607
  }
572
608
  const headers = loopbackAuthority({ ...req.headers }, upstream);
573
609
  const proxyReq = httpRequest({
574
- host: upstream.host, port: upstream.port, method: req.method, path: req.url, headers, agent: false,
610
+ // 同样清掉历史遗留的 dsh-desktop-* 参数(issue #75)
611
+ host: upstream.host, port: upstream.port, method: req.method, path: stripDesktopMarkers(req.url), headers, agent: false,
575
612
  });
576
613
  proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
577
614
  socket.write('HTTP/1.1 101 Switching Protocols\r\n');
package/lib/settings.mjs CHANGED
@@ -288,3 +288,26 @@ export function revokeTempPin(value, kind) {
288
288
  export function tempPinValuesFor(kind) {
289
289
  return tempPins().filter((p) => p.kind === kind).map((p) => p.value);
290
290
  }
291
+
292
+ // ---------- cloudflared 路径(issue #45:远程 Linux 服务器下载源不可达时手动指定) ----------
293
+ // Linux 服务器在国内/部分企业网下,所有 CDN 源(GitHub / ghproxy / gh.ddlc / gh-proxy)
294
+ // 都连不上时,下载 cloudflared 二进制始终失败。允许用户在 settings.json 里**写死
295
+ // 一个已经存在 / 自己上传的 cloudflared 路径**,跳过下载。`lib/tunnel.mjs` 的
296
+ // `resolveCloudflared` 启动时会优先读 `process.env.DSH_POCKET_CLOUDFLARED`,
297
+ // 找不到再回退到 PATH 探测和下载。`lib/index.js` 在插件 apply 时把 settings
298
+ // 的这个值写入 env,确保 service 走自定义路径。
299
+ // 空字符串 = 清除,回退到默认行为(PATH 探测 + 下载)。
300
+ /** 当前 cloudflared 自定义路径(空 = 用默认)。 */
301
+ export function cloudflaredPath() {
302
+ return readSettings().cloudflaredPath ?? '';
303
+ }
304
+
305
+ /** 设置 cloudflared 自定义路径。空字符串清除(回退到 PATH 探测 + 下载)。 */
306
+ export function setCloudflaredPath(value) {
307
+ const v = String(value ?? '').trim();
308
+ const s = readSettings();
309
+ if (v) s.cloudflaredPath = v;
310
+ else delete s.cloudflaredPath;
311
+ writeSettings(s);
312
+ return v;
313
+ }
package/lib/tunnel.mjs CHANGED
@@ -300,6 +300,20 @@ let downloading = null;
300
300
  export { tsinghuaBottleUrl };
301
301
 
302
302
  export async function resolveCloudflared({ home, onPhase = () => {}, signal } = {}) {
303
+ // 自定义路径(issue #45):用户可能在 settings.json 写了 cloudflaredPath,
304
+ // 或者在外部注入 process.env.DSH_POCKET_CLOUDFLARED;命中就直接用,绕过
305
+ // PATH 探测与下载(远程 Linux 服务器上下载源不可达时的兜底)。
306
+ const explicit = process.env.DSH_POCKET_CLOUDFLARED;
307
+ if (explicit) {
308
+ try {
309
+ await access(explicit);
310
+ return explicit;
311
+ } catch {
312
+ throw new Error(
313
+ `DSH_POCKET_CLOUDFLARED 指向的路径不可执行:${explicit} | cloudflaredPath is set but not accessible: ${explicit}`,
314
+ );
315
+ }
316
+ }
303
317
  if (cloudflaredOnPath()) return 'cloudflared';
304
318
  const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
305
319
  const cacheDir = join(dshHome, 'dsh-pocket', 'bin');
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.4.0"
83
+ "version": "2.5.0"
84
84
  }