dsh-mobile 0.4.5 → 0.4.7

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/docs/README.md CHANGED
@@ -8,10 +8,13 @@
8
8
  | --- | --- | --- |
9
9
  | [Cloudflare 命名隧道](CLOUDFLARE_TUNNEL.md) | [Cloudflare named tunnel](CLOUDFLARE_TUNNEL.en.md) | 用 Cloudflare 账号令牌把 cloudflared 从随机快速隧道换成固定公网域名 |
10
10
  | [自建 FRP 使用指南](SELF_HOSTED_FRP.md) | [Self-hosted FRP guide](SELF_HOSTED_FRP.en.md) | 已有 VPS 时用 frps + Caddy 自建远程通道,避开公共隧道带宽限制 |
11
+ | [接入你既有的 frps](ATTACH_EXISTING_FRPS.md) | [Attach to an existing frps](ATTACH_EXISTING_FRPS.en.md) | 复用 VPS 上已运行的 frps,不自动修改服务器;可选公开 CA + Caddy 或自签 CA + TCP 透传,须核对监听与证书有效期。 |
11
12
  | [自有 HTTPS 反向代理](SELF_HOSTED_ORIGIN.md) | [Own HTTPS reverse proxy](SELF_HOSTED_ORIGIN.en.md) | 复用已有的 Lucky / Nginx / Caddy 公网 HTTPS 入口,无需隧道组件 |
12
13
 
13
14
  > 命名隧道与两个自建提供方都要求手机端走 App 的**远程访问**扫码流程;`SELF_HOSTED_ORIGIN.en.md` 是精简版,字段表与排错清单以中文版为准。
14
15
 
16
+ > 既有 frps 接入从 0.4.6 起提供;其中自签入口需要 0.4.6 Android App,旧版不能配对。
17
+
15
18
  ## 验证记录 / Verification records
16
19
 
17
20
  | 文档 | 内容 |
@@ -2,6 +2,8 @@
2
2
 
3
3
  [中文指南](SELF_HOSTED_FRP.md)
4
4
 
5
+ This page covers managed deployment, in which the plugin installs frps and Caddy. If your VPS already runs frps, version 0.4.6 can attach to it directly; see the [attachment guide](ATTACH_EXISTING_FRPS.en.md).
6
+
5
7
  Self-hosted FRP is for users who already operate a VPS and want to avoid the bandwidth limits of public tunnels. The phone reaches Caddy on the VPS over HTTPS, crosses the encrypted FRP tunnel, and then reaches DSH on the computer. FRP only transports the request; DSH pairing is still required.
6
8
 
7
9
  ```text
@@ -18,7 +20,7 @@ Android app
18
20
  - **Domain mode**: use your own public domain; Caddy obtains and renews the certificate automatically.
19
21
  - **Public IPv4 mode**: use the VPS public IPv4 address directly. The plugin uses Certbot to request a roughly six-day Let's Encrypt IP certificate and installs a daily renewal timer. Documentation ranges such as `203.0.113.10`, private addresses, and other reserved addresses are rejected; enter a real routable public IP.
20
22
 
21
- The custom remote-origin flow requires Android app 0.3.3 or later. Older supported apps continue to work with LAN, cpolar, and Tailscale.
23
+ The publicly trusted entry in this guide requires Android app 0.3.3 or later for custom remote origins. Self-signed passthrough requires the 0.4.6 app with remote CA pinning; older apps cannot use that entry and continue to work with LAN, cpolar, and Tailscale.
22
24
 
23
25
  ## Manual deployment vs automatic deployment
24
26
 
@@ -2,6 +2,12 @@
2
2
 
3
3
  [English guide](SELF_HOSTED_FRP.en.md)
4
4
 
5
+ 本页介绍托管部署(插件安装 frps 与 Caddy)。
6
+
7
+ > **已经有一台跑着 frps 的 VPS?** 从 0.4.6 起可直接接入,见 [接入你既有的 frps](ATTACH_EXISTING_FRPS.md):
8
+ > 插件不装不改你的 frps、**零 SSH**;还提供**完全不用公开证书**的自签穿透档(frps 只做 TCP 透传,
9
+ > 网关自己终止 TLS,证书有效期与更换要求见专门指南)。
10
+
5
11
  自建 FRP 适合已有 VPS、希望避开公共隧道带宽限制的用户。手机经 VPS 上的 Caddy 进入加密 FRP 隧道,再到达电脑上的 DSH;FRP 只负责传输,仍需完成 DSH 配对才能进入。
6
12
 
7
13
  ```text
@@ -18,7 +24,7 @@
18
24
  - **域名模式**:公开地址是自己的域名,Caddy 自动申请并续期证书。
19
25
  - **公网 IPv4 模式**:公开地址直接是 VPS 公网 IPv4(例如你自己的 VPS 地址),插件用 Certbot 申请约 6 天有效的 Let's Encrypt IP 证书,并安装每日自动续期定时器。注意:文档示例地址(如 `203.0.113.10`)和内网、保留地址会被拒绝,必须填写真实可路由的公网 IP。
20
26
 
21
- 需要 Android App 0.3.3 或更高版本(远程自定义入口);旧版 App 仍可用局域网、cpolar 和 Tailscale。
27
+ 本页的公开证书入口需要 Android App 0.3.3 或更高版本(远程自定义入口)。自签穿透档需要 0.4.6 Android App 固定远程 CA;旧版 App 不支持该档,仍可使用局域网、cpolar 和 Tailscale。
22
28
 
23
29
  ## 手动部署 vs 自动部署
24
30
 
package/lib/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir, networkInterfaces } from "node:os";
4
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
5
5
  import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
6
6
  import { execFile } from "node:child_process";
7
7
  import { generate } from "selfsigned";
@@ -34,8 +34,13 @@ function execFileText(file, args, options = {}) {
34
34
  //#endregion
35
35
  //#region src/private-file.ts
36
36
  let userSidTask;
37
+ function windowsSystemTool(name) {
38
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
39
+ if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw new Error("Windows system root is unavailable");
40
+ return win32.join(systemRoot, "System32", name);
41
+ }
37
42
  async function currentWindowsUserSid() {
38
- userSidTask ??= execFileText("whoami.exe", [
43
+ userSidTask ??= execFileText(windowsSystemTool("whoami.exe"), [
39
44
  "/user",
40
45
  "/fo",
41
46
  "csv",
@@ -58,11 +63,12 @@ async function currentWindowsUserSid() {
58
63
  async function restrictPrivateFile(file, mode = 384) {
59
64
  await chmod(file, mode);
60
65
  if (process.platform !== "win32") return;
61
- await execFileText("icacls.exe", [
66
+ const userSid = await currentWindowsUserSid();
67
+ await execFileText(windowsSystemTool("icacls.exe"), [
62
68
  file,
63
69
  "/inheritance:r",
64
70
  "/grant:r",
65
- `*${await currentWindowsUserSid()}:(F)`,
71
+ `*${userSid}:(F)`,
66
72
  "*S-1-5-18:(F)",
67
73
  "*S-1-5-32-544:(F)",
68
74
  "/remove:g",
@@ -279,17 +285,38 @@ async function ensureManagedCa(setup, legacy) {
279
285
  await Promise.all([restrictPrivateFile(setup.caCertFile), restrictPrivateFile(setup.caKeyFile)]);
280
286
  return assertMatchingCa(certPem, keyPem);
281
287
  }
282
- /** Sign and atomically install a server leaf for the interface's current address. */
283
- async function refreshManagedServerCertificate(setup, address) {
284
- await Promise.all([restrictPrivateFile(setup.tls.caCertFile), restrictPrivateFile(setup.tls.caKeyFile)]);
285
- const [caCert, caKey] = await Promise.all([readFile(setup.tls.caCertFile, "utf8"), readFile(setup.tls.caKeyFile, "utf8")]);
288
+ function subjectAltNames(target) {
289
+ const names = [];
290
+ for (const ip of target.ipAddresses ?? []) names.push({
291
+ type: 7,
292
+ ip
293
+ });
294
+ for (const dns of target.dnsNames ?? []) names.push({
295
+ type: 2,
296
+ value: dns
297
+ });
298
+ if (names.length === 0) throw new Error("managed TLS leaf needs at least one server name");
299
+ return names;
300
+ }
301
+ /**
302
+ * Sign one server leaf with an existing self-signed CA and install it privately.
303
+ *
304
+ * Both the LAN listener and the self-signed FRP ingress use this one path, so a
305
+ * certificate can never be produced by two divergent code paths. IP entries are
306
+ * required for console addresses (`type: 7`), DNS entries (`type: 2`) cover named
307
+ * hosts; the Android client accepts either through its pinned CA.
308
+ */
309
+ async function issueServerCertificate(authority, target, output, lifetimeDays = 397) {
310
+ const altNames = subjectAltNames(target);
311
+ await Promise.all([restrictPrivateFile(authority.caCertFile), restrictPrivateFile(authority.caKeyFile)]);
312
+ const [caCert, caKey] = await Promise.all([readFile(authority.caCertFile, "utf8"), readFile(authority.caKeyFile, "utf8")]);
286
313
  assertMatchingCa(caCert, caKey);
287
314
  const now = /* @__PURE__ */ new Date();
288
315
  const notAfter = new Date(now);
289
- notAfter.setDate(notAfter.getDate() + 397);
316
+ notAfter.setDate(notAfter.getDate() + lifetimeDays);
290
317
  const server = await generate([{
291
318
  name: "commonName",
292
- value: "DeepSeek Harness Mobile"
319
+ value: target.commonName
293
320
  }], {
294
321
  keyType: "ec",
295
322
  curve: "P-256",
@@ -317,14 +344,24 @@ async function refreshManagedServerCertificate(setup, address) {
317
344
  },
318
345
  {
319
346
  name: "subjectAltName",
320
- altNames: [{
321
- type: 7,
322
- ip: address
323
- }]
347
+ altNames
324
348
  }
325
349
  ]
326
350
  });
327
- await Promise.all([atomicWrite(setup.tls.certFile, server.cert), atomicWrite(setup.tls.keyFile, server.private)]);
351
+ await Promise.all([atomicWrite(output.certFile, server.cert), atomicWrite(output.keyFile, server.private)]);
352
+ }
353
+ /** Sign and atomically install a server leaf for the interface's current address. */
354
+ async function refreshManagedServerCertificate(setup, address) {
355
+ await issueServerCertificate({
356
+ caCertFile: setup.tls.caCertFile,
357
+ caKeyFile: setup.tls.caKeyFile
358
+ }, {
359
+ commonName: "DeepSeek Harness Mobile",
360
+ ipAddresses: [address]
361
+ }, {
362
+ certFile: setup.tls.certFile,
363
+ keyFile: setup.tls.keyFile
364
+ });
328
365
  }
329
366
  //#endregion
330
367
  //#region src/lan-setup.ts