dsh-mobile 0.6.3 → 0.6.4
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 +22 -0
- package/CONTRIBUTORS.md +10 -2
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/THIRD_PARTY_NOTICES.md +29 -1
- package/apps/mobile/README.md +11 -3
- package/cordis.patch.yml +5 -2
- package/docs/ATTACH_EXISTING_FRPS.en.md +1 -1
- package/docs/ATTACH_EXISTING_FRPS.md +1 -1
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +8 -0
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +8 -0
- package/docs/SELF_HOSTED_FRP.en.md +3 -1
- package/docs/SELF_HOSTED_FRP.md +3 -1
- package/docs/SLOW_CONNECTIONS.en.md +6 -0
- package/docs/SLOW_CONNECTIONS.md +6 -0
- package/lib/cli.js +139 -19
- package/lib/cli.js.map +1 -1
- package/lib/client.js +400 -9
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +10 -5
- package/lib/index.mjs +571 -291
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, networkInterfaces } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
5
5
|
import { X509Certificate, createPrivateKey, createPublicKey, randomBytes } from "node:crypto";
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { generate } from "selfsigned";
|
|
8
|
+
import { performance } from "node:perf_hooks";
|
|
8
9
|
import "@deepseek-ai/cordis";
|
|
9
10
|
import "@deepseek-ai/schemastery";
|
|
10
11
|
//#region src/exec-file.ts
|
|
@@ -34,24 +35,75 @@ function execFileText(file, args, options = {}) {
|
|
|
34
35
|
//#endregion
|
|
35
36
|
//#region src/private-file.ts
|
|
36
37
|
let userSidTask;
|
|
38
|
+
function commandFailureDetails(error) {
|
|
39
|
+
if (error === null || typeof error !== "object") return {};
|
|
40
|
+
let descriptors;
|
|
41
|
+
try {
|
|
42
|
+
descriptors = Object.getOwnPropertyDescriptors(error);
|
|
43
|
+
} catch (_error) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const code = descriptors.code?.value;
|
|
47
|
+
const signal = descriptors.signal?.value;
|
|
48
|
+
const killed = descriptors.killed?.value;
|
|
49
|
+
return {
|
|
50
|
+
...code === null || typeof code === "number" && Number.isSafeInteger(code) || typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/u.test(code) ? { code } : {},
|
|
51
|
+
...signal === null || typeof signal === "string" && /^SIG[A-Z0-9]{1,16}$/u.test(signal) ? { signal } : {},
|
|
52
|
+
...typeof killed === "boolean" ? { killed } : {}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Phase and full await duration, including subprocess creation, without arguments, output or private paths. */
|
|
56
|
+
var PrivateFilePermissionError = class extends Error {
|
|
57
|
+
stage;
|
|
58
|
+
elapsedMs;
|
|
59
|
+
code;
|
|
60
|
+
signal;
|
|
61
|
+
killed;
|
|
62
|
+
constructor(stage, elapsedMs, error) {
|
|
63
|
+
const details = commandFailureDetails(error);
|
|
64
|
+
const fields = [
|
|
65
|
+
...details.code === void 0 ? [] : [`code=${String(details.code)}`],
|
|
66
|
+
...details.signal === void 0 ? [] : [`signal=${String(details.signal)}`],
|
|
67
|
+
...details.killed === void 0 ? [] : [`killed=${String(details.killed)}`]
|
|
68
|
+
];
|
|
69
|
+
const summary = `Windows private-file ${stage} failed after ${String(elapsedMs)} ms${fields.length === 0 ? "" : ` (${fields.join(", ")})`}`;
|
|
70
|
+
const cause = Object.assign(/* @__PURE__ */ new Error("Windows permission operation failed"), details);
|
|
71
|
+
super(summary, { cause });
|
|
72
|
+
this.stage = stage;
|
|
73
|
+
this.elapsedMs = elapsedMs;
|
|
74
|
+
this.name = "PrivateFilePermissionError";
|
|
75
|
+
this.code = details.code;
|
|
76
|
+
this.signal = details.signal;
|
|
77
|
+
this.killed = details.killed;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
async function permissionOperation(stage, operation) {
|
|
81
|
+
const started = performance.now();
|
|
82
|
+
try {
|
|
83
|
+
return await operation();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new PrivateFilePermissionError(stage, Math.max(0, Math.round(performance.now() - started)), error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
37
88
|
function windowsSystemTool(name) {
|
|
38
89
|
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
39
|
-
if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw new Error("Windows system root is unavailable");
|
|
90
|
+
if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw Object.assign(/* @__PURE__ */ new Error("Windows system root is unavailable"), { code: "WINDOWS_SYSTEM_ROOT_UNAVAILABLE" });
|
|
40
91
|
return win32.join(systemRoot, "System32", name);
|
|
41
92
|
}
|
|
42
93
|
async function currentWindowsUserSid() {
|
|
43
|
-
userSidTask ??=
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
userSidTask ??= permissionOperation("sid-query", async () => {
|
|
95
|
+
const { stdout } = await execFileText(windowsSystemTool("whoami.exe"), [
|
|
96
|
+
"/user",
|
|
97
|
+
"/fo",
|
|
98
|
+
"csv",
|
|
99
|
+
"/nh"
|
|
100
|
+
], {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
timeout: 1e4
|
|
104
|
+
});
|
|
53
105
|
const match = /,"(S-\d(?:-\d+)+)"\s*$/u.exec(stdout.trim());
|
|
54
|
-
if (match?.[1] === void 0) throw new Error("unable to resolve the current Windows user SID");
|
|
106
|
+
if (match?.[1] === void 0) throw Object.assign(/* @__PURE__ */ new Error("unable to resolve the current Windows user SID"), { code: "WINDOWS_USER_SID_INVALID" });
|
|
55
107
|
return match[1];
|
|
56
108
|
}).catch((error) => {
|
|
57
109
|
userSidTask = void 0;
|
|
@@ -59,12 +111,12 @@ async function currentWindowsUserSid() {
|
|
|
59
111
|
});
|
|
60
112
|
return userSidTask;
|
|
61
113
|
}
|
|
62
|
-
/** Restrict a sensitive regular file to the current user and Windows administrators. */
|
|
114
|
+
/** Restrict a sensitive regular file to the current user and Windows administrators; reject Windows permission failures with redacted phase and duration. */
|
|
63
115
|
async function restrictPrivateFile(file, mode = 384) {
|
|
64
116
|
await chmod(file, mode);
|
|
65
117
|
if (process.platform !== "win32") return;
|
|
66
118
|
const userSid = await currentWindowsUserSid();
|
|
67
|
-
await execFileText(windowsSystemTool("icacls.exe"), [
|
|
119
|
+
await permissionOperation("acl-apply", () => execFileText(windowsSystemTool("icacls.exe"), [
|
|
68
120
|
file,
|
|
69
121
|
"/inheritance:r",
|
|
70
122
|
"/grant:r",
|
|
@@ -79,7 +131,7 @@ async function restrictPrivateFile(file, mode = 384) {
|
|
|
79
131
|
encoding: "utf8",
|
|
80
132
|
windowsHide: true,
|
|
81
133
|
timeout: 1e4
|
|
82
|
-
});
|
|
134
|
+
}));
|
|
83
135
|
}
|
|
84
136
|
Object.freeze([
|
|
85
137
|
[0n, 8],
|
|
@@ -243,7 +295,7 @@ function assertMatchingCa(certPem, keyPem) {
|
|
|
243
295
|
if (Date.parse(certificate.validFrom) > Date.now() || Date.parse(certificate.validTo) <= Date.now()) throw new Error("managed TLS CA certificate is not currently valid");
|
|
244
296
|
return certificate;
|
|
245
297
|
}
|
|
246
|
-
async function
|
|
298
|
+
async function stagePrivateFile(file, contents) {
|
|
247
299
|
const directory = dirname(file);
|
|
248
300
|
await mkdir(directory, {
|
|
249
301
|
recursive: true,
|
|
@@ -251,17 +303,85 @@ async function atomicWrite(file, contents) {
|
|
|
251
303
|
});
|
|
252
304
|
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
253
305
|
try {
|
|
254
|
-
await writeFile(temporary,
|
|
306
|
+
await writeFile(temporary, "", {
|
|
255
307
|
mode: 384,
|
|
256
308
|
flag: "wx"
|
|
257
309
|
});
|
|
258
310
|
await restrictPrivateFile(temporary);
|
|
311
|
+
await writeFile(temporary, contents);
|
|
312
|
+
return temporary;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
await rm(temporary, { force: true });
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function atomicWrite(file, contents) {
|
|
319
|
+
const temporary = await stagePrivateFile(file, contents);
|
|
320
|
+
try {
|
|
259
321
|
await rename(temporary, file);
|
|
260
322
|
await restrictPrivateFile(file);
|
|
261
323
|
} finally {
|
|
262
324
|
await rm(temporary, { force: true });
|
|
263
325
|
}
|
|
264
326
|
}
|
|
327
|
+
const certificatePublications = /* @__PURE__ */ new Map();
|
|
328
|
+
/** Stage both files before promotion and retain a private rollback copy until the pair is installed. */
|
|
329
|
+
async function installServerCertificatePair(output, cert, key) {
|
|
330
|
+
const identity = `${output.certFile}\0${output.keyFile}`;
|
|
331
|
+
const publication = (certificatePublications.get(identity) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
332
|
+
const files = [output.certFile, output.keyFile];
|
|
333
|
+
const contents = [cert, key];
|
|
334
|
+
const staged = [];
|
|
335
|
+
const backups = [];
|
|
336
|
+
let promoted = 0;
|
|
337
|
+
let restored = true;
|
|
338
|
+
const failures = [];
|
|
339
|
+
try {
|
|
340
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
341
|
+
const file = files[index];
|
|
342
|
+
staged.push(await stagePrivateFile(file, contents[index]));
|
|
343
|
+
let old;
|
|
344
|
+
try {
|
|
345
|
+
const metadata = await lstat(file);
|
|
346
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error("managed TLS output is not a regular file");
|
|
347
|
+
old = await readFile(file);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error.code !== "ENOENT") throw error;
|
|
350
|
+
}
|
|
351
|
+
backups.push(old === void 0 ? void 0 : await stagePrivateFile(file, old));
|
|
352
|
+
}
|
|
353
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
354
|
+
await rename(staged[index], files[index]);
|
|
355
|
+
promoted += 1;
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
failures.push(error);
|
|
359
|
+
for (let index = promoted - 1; index >= 0; index -= 1) try {
|
|
360
|
+
const backup = backups[index];
|
|
361
|
+
if (backup === void 0) await rm(files[index], { force: true });
|
|
362
|
+
else await rename(backup, files[index]);
|
|
363
|
+
} catch (restoreError) {
|
|
364
|
+
restored = false;
|
|
365
|
+
failures.push(restoreError);
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
const cleanup = await Promise.allSettled(staged.map((file) => rm(file, { force: true })));
|
|
369
|
+
for (const result of cleanup) if (result.status === "rejected") failures.push(result.reason);
|
|
370
|
+
if (restored) {
|
|
371
|
+
const backupCleanup = await Promise.allSettled(backups.filter((file) => file !== void 0).map((file) => rm(file, { force: true })));
|
|
372
|
+
for (const result of backupCleanup) if (result.status === "rejected") failures.push(result.reason);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (failures.length > 1) throw new AggregateError(failures, "managed TLS certificate replacement, rollback or cleanup failed");
|
|
376
|
+
if (failures.length === 1) throw failures[0];
|
|
377
|
+
});
|
|
378
|
+
certificatePublications.set(identity, publication);
|
|
379
|
+
try {
|
|
380
|
+
await publication;
|
|
381
|
+
} finally {
|
|
382
|
+
if (certificatePublications.get(identity) === publication) certificatePublications.delete(identity);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
265
385
|
/** Create a long-lived CA or migrate the legacy self-signed server certificate as that CA. */
|
|
266
386
|
async function ensureManagedCa(setup, legacy) {
|
|
267
387
|
let certPem;
|
|
@@ -376,7 +496,7 @@ async function issueServerCertificate(authority, target, output, lifetimeDays =
|
|
|
376
496
|
}
|
|
377
497
|
]
|
|
378
498
|
});
|
|
379
|
-
await
|
|
499
|
+
await installServerCertificatePair(output, server.cert, server.private);
|
|
380
500
|
}
|
|
381
501
|
/** Sign and atomically install a server leaf for the interface's current address. */
|
|
382
502
|
async function refreshManagedServerCertificate(setup, address) {
|