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/CHANGELOG.md +23 -0
- package/CONTRIBUTORS.md +11 -3
- package/README.en.md +38 -11
- package/README.md +38 -11
- package/SECURITY.md +7 -5
- package/docs/ATTACH_EXISTING_FRPS.en.md +44 -0
- package/docs/ATTACH_EXISTING_FRPS.md +98 -0
- package/docs/README.md +3 -0
- package/docs/SELF_HOSTED_FRP.en.md +3 -1
- package/docs/SELF_HOSTED_FRP.md +7 -1
- package/lib/cli.js +52 -15
- package/lib/cli.js.map +1 -1
- package/lib/client.js +1298 -156
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +390 -12
- package/lib/index.mjs +2268 -581
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js.map +1 -1
- package/lib/mobile-layout.js +75 -20
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +18 -14
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
|
|
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
|
|
package/docs/SELF_HOSTED_FRP.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
66
|
+
const userSid = await currentWindowsUserSid();
|
|
67
|
+
await execFileText(windowsSystemTool("icacls.exe"), [
|
|
62
68
|
file,
|
|
63
69
|
"/inheritance:r",
|
|
64
70
|
"/grant:r",
|
|
65
|
-
`*${
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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() +
|
|
316
|
+
notAfter.setDate(notAfter.getDate() + lifetimeDays);
|
|
290
317
|
const server = await generate([{
|
|
291
318
|
name: "commonName",
|
|
292
|
-
value:
|
|
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(
|
|
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
|