dsh-mobile 0.4.7 → 0.5.1
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 +20 -0
- package/CONTRIBUTORS.md +10 -3
- package/README.en.md +33 -30
- package/README.md +37 -33
- package/SECURITY.md +3 -1
- package/THIRD_PARTY_NOTICES.md +4 -4
- package/docs/ATTACH_EXISTING_FRPS.en.md +2 -0
- package/docs/ATTACH_EXISTING_FRPS.md +4 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +15 -20
- package/docs/CLOUDFLARE_TUNNEL.md +15 -20
- package/docs/DSH_0.1.5_LAN.md +1 -1
- package/docs/README.md +2 -2
- package/docs/SELF_HOSTED_FRP.en.md +7 -10
- package/docs/SELF_HOSTED_FRP.md +7 -10
- package/docs/SELF_HOSTED_ORIGIN.en.md +1 -1
- package/docs/SELF_HOSTED_ORIGIN.md +1 -1
- package/lib/client.js +459 -37
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +4 -1
- package/lib/index.mjs +162 -45
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +73 -9
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +5 -5
package/lib/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { X509Certificate } from "node:crypto";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
4
|
+
import { IncomingMessage } from "node:http";
|
|
4
5
|
import { Readable } from "node:stream";
|
|
5
6
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
6
7
|
import { WebRoute } from "@deepseek-ai/dsh-host-webserver";
|
|
@@ -355,6 +356,8 @@ declare class JsonMobileAccessControlStore implements MobileAccessControlStore {
|
|
|
355
356
|
private readonly file;
|
|
356
357
|
private readonly initiallyEnabled;
|
|
357
358
|
constructor(file: string, initiallyEnabled: boolean);
|
|
359
|
+
/** Avoid writing an absent already-disabled state without changing recovery of existing files. */
|
|
360
|
+
disableIfPresent(): Promise<void>;
|
|
358
361
|
load(): Promise<MobileAccessControlState>;
|
|
359
362
|
save(state: MobileAccessControlState): Promise<void>;
|
|
360
363
|
}
|
|
@@ -756,7 +759,7 @@ declare class MobileAccessGateway {
|
|
|
756
759
|
blockedUpgradePathReport(): BlockedUpgradePathEntry[];
|
|
757
760
|
private handleUpgrade;
|
|
758
761
|
/** Loopback-only DSH WebServer route for opening pairing and managing devices. */
|
|
759
|
-
localAdminRoute(prefix?: string): WebRoute;
|
|
762
|
+
localAdminRoute(prefix?: string, authenticateDesktop?: (request: IncomingMessage) => boolean): WebRoute;
|
|
760
763
|
/** Close listeners and abort all accepted work before resolving teardown. */
|
|
761
764
|
close(): Promise<void>;
|
|
762
765
|
private performClose;
|
package/lib/index.mjs
CHANGED
|
@@ -1078,6 +1078,25 @@ var JsonMobileAccessControlStore = class {
|
|
|
1078
1078
|
this.file = file;
|
|
1079
1079
|
this.initiallyEnabled = initiallyEnabled;
|
|
1080
1080
|
}
|
|
1081
|
+
/** Avoid writing an absent already-disabled state without changing recovery of existing files. */
|
|
1082
|
+
async disableIfPresent() {
|
|
1083
|
+
try {
|
|
1084
|
+
await lstat(this.file);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error.code === "ENOENT") {
|
|
1087
|
+
if (this.initiallyEnabled) await this.save({
|
|
1088
|
+
version: 1,
|
|
1089
|
+
enabled: false
|
|
1090
|
+
});
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
await this.save({
|
|
1096
|
+
version: 1,
|
|
1097
|
+
enabled: false
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1081
1100
|
async load() {
|
|
1082
1101
|
let stat;
|
|
1083
1102
|
try {
|
|
@@ -1222,9 +1241,6 @@ var MobileAccessGatewayController = class {
|
|
|
1222
1241
|
return run;
|
|
1223
1242
|
}
|
|
1224
1243
|
};
|
|
1225
|
-
//#endregion
|
|
1226
|
-
//#region src/local-admin-host.ts
|
|
1227
|
-
/** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
|
|
1228
1244
|
function unwrapBrackets(hostname) {
|
|
1229
1245
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1230
1246
|
}
|
|
@@ -1339,7 +1355,8 @@ function setSecurityHeaders(response, tls, framing = "gateway") {
|
|
|
1339
1355
|
"worker-src 'self' blob:",
|
|
1340
1356
|
...proxied ? [PROXIED_FRAME_SRC] : []
|
|
1341
1357
|
].join("; "));
|
|
1342
|
-
|
|
1358
|
+
const microphone = proxied ? "microphone=(self)" : "microphone=()";
|
|
1359
|
+
response.setHeader("Permissions-Policy", `camera=(), ${microphone}, geolocation=(), payment=(), usb=()`);
|
|
1343
1360
|
response.setHeader("Referrer-Policy", "no-referrer");
|
|
1344
1361
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1345
1362
|
response.setHeader("X-Frame-Options", proxied ? "SAMEORIGIN" : "DENY");
|
|
@@ -1436,7 +1453,7 @@ function localAuthority(header) {
|
|
|
1436
1453
|
}
|
|
1437
1454
|
}
|
|
1438
1455
|
/** Protect the inner management route from non-loopback and DNS-rebinding callers. */
|
|
1439
|
-
function assertLocalAdminTrust(request, requireBrowserOrigin) {
|
|
1456
|
+
function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDesktop = () => false) {
|
|
1440
1457
|
if (request.socket.remoteAddress === void 0 || !isLoopbackAddress(request.socket.remoteAddress)) throw new HttpError(403, "forbidden");
|
|
1441
1458
|
const host = localAuthority(request.headers.host);
|
|
1442
1459
|
if (host === void 0 || !isLocalAdminHostname(host.hostname)) throw new HttpError(403, "forbidden");
|
|
@@ -1450,7 +1467,8 @@ function assertLocalAdminTrust(request, requireBrowserOrigin) {
|
|
|
1450
1467
|
if (error instanceof HttpError) throw error;
|
|
1451
1468
|
throw new HttpError(403, "forbidden");
|
|
1452
1469
|
}
|
|
1453
|
-
|
|
1470
|
+
const desktopForward = site === void 0 && request.headers["x-dsh-mobile-desktop-admin"] === "dsh-app://app";
|
|
1471
|
+
if (requireBrowserOrigin && origin === void 0 && !(desktopForward && authenticateDesktop())) throw new HttpError(403, "forbidden");
|
|
1454
1472
|
if (requireBrowserOrigin && site !== void 0 && site !== "same-origin") throw new HttpError(403, "forbidden");
|
|
1455
1473
|
}
|
|
1456
1474
|
//#endregion
|
|
@@ -2647,7 +2665,7 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
|
2647
2665
|
])
|
|
2648
2666
|
})]);
|
|
2649
2667
|
const MOBILE_CSRF_FETCH_BOOTSTRAP = `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${CSRF_COOKIE}=`)};const token=document.cookie.split(';').map(value=>value.trim()).find(value=>value.startsWith(prefix))?.slice(prefix.length);if(token!==undefined)headers.set(${JSON.stringify(CSRF_HEADER)},token)}return nativeFetch(input,{...init,headers})};})();`;
|
|
2650
|
-
const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true}})();`;
|
|
2668
|
+
const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true},window.__DSH_FILE_UPLOAD__={fetch:(input,init)=>window.fetch(input,init)}})();`;
|
|
2651
2669
|
/** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
|
|
2652
2670
|
const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
|
|
2653
2671
|
const view=globalThis;
|
|
@@ -5085,14 +5103,14 @@ var MobileAccessGateway = class {
|
|
|
5085
5103
|
}
|
|
5086
5104
|
}
|
|
5087
5105
|
/** Loopback-only DSH WebServer route for opening pairing and managing devices. */
|
|
5088
|
-
localAdminRoute(prefix = LOCAL_ADMIN_PREFIX) {
|
|
5106
|
+
localAdminRoute(prefix = LOCAL_ADMIN_PREFIX, authenticateDesktop) {
|
|
5089
5107
|
return {
|
|
5090
5108
|
kind: "prefix",
|
|
5091
5109
|
path: prefix,
|
|
5092
5110
|
handler: async (request, response) => {
|
|
5093
5111
|
try {
|
|
5094
5112
|
const target = parseRequestTarget(request.url);
|
|
5095
|
-
assertLocalAdminTrust(request, request.method === "POST");
|
|
5113
|
+
assertLocalAdminTrust(request, request.method === "POST", () => authenticateDesktop?.(request) === true);
|
|
5096
5114
|
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
5097
5115
|
if (request.method === "GET" && target.decodedPathname === `${prefix}/status`) {
|
|
5098
5116
|
sendJson(response, 200, {
|
|
@@ -9509,6 +9527,10 @@ var TaskEventHub = class {
|
|
|
9509
9527
|
};
|
|
9510
9528
|
//#endregion
|
|
9511
9529
|
//#region src/diagnostics.ts
|
|
9530
|
+
/** Recognize the active remote-web-ui boot hook without evaluating or modifying it. */
|
|
9531
|
+
function hasCompetingRemoteChannelBoot(rows) {
|
|
9532
|
+
return rows.some((row) => row.kind === "script" && row.text?.includes("__DSH_REMOTE_CHANNEL_BOOT__") === true);
|
|
9533
|
+
}
|
|
9512
9534
|
const REMOTE_ERROR_GUIDANCE = Object.freeze({
|
|
9513
9535
|
component_missing: "重新安装完整插件包。",
|
|
9514
9536
|
funnel_permission_required: "继续完成 Tailscale Funnel 授权。",
|
|
@@ -9903,6 +9925,7 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
|
9903
9925
|
const remoteProbe = snapshot.remote.provider !== "origin" && snapshot.remote.running && snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0 ? (probes.remote ?? defaultRemoteProbe)(snapshot.remote.origin) : Promise.resolve({ state: "not-applicable" });
|
|
9904
9926
|
const [firewall, remoteObservation] = await Promise.all([(probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port), remoteProbe]);
|
|
9905
9927
|
checks.push(check("versions", "ok", "versions-current", "版本兼容", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
|
|
9928
|
+
if (snapshot.competingRemoteChannelBoot === true) checks.push(check("remote-plugin-conflict", "warning", "competing-remote-channel", "第三方远程插件", "检测到另一套远程请求通道脚本;手机页面可能进入该插件自己的配对流程。", "在电脑端关闭 dsh-remote-web-ui 的远程访问后刷新,再使用 DSH Mobile 当前二维码配对;不要混用两种配对码。"));
|
|
9906
9929
|
if (snapshot.lan.configured === false) checks.push(check("network", "error", "lan-setup-required", "局域网配置", "尚未完成局域网初始化。", "返回局域网页选择网卡并完成配置。"));
|
|
9907
9930
|
else if (snapshot.lan.networkError !== void 0) checks.push(check("network", "error", "network-unavailable", "局域网网卡", "已保存的网卡当前不可用。", "重新运行 dsh-mobile setup。"));
|
|
9908
9931
|
else if (snapshot.lan.configuredInterface !== void 0) {
|
|
@@ -9973,7 +9996,7 @@ function buildMobileGuide(state) {
|
|
|
9973
9996
|
const scriptLine = state.hasCustomJs ? "存在(当前生效的自定义脚本)" : "不存在(无自定义脚本)";
|
|
9974
9997
|
const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version})`).join("\n");
|
|
9975
9998
|
const failureLine = state.failedExtensionCount > 0 ? `注意:${state.failedExtensionCount} 个扩展的电脑端 host 激活失败,如改动相关扩展请先检查其 host.mjs 与 extension.json。` : "";
|
|
9976
|
-
return `${`##
|
|
9999
|
+
return `${`## 手机端当前状态(改动前必读,避免覆盖已有定制)
|
|
9977
10000
|
|
|
9978
10001
|
- 定制目录:${state.directory}(所有改动只允许在这里进行)
|
|
9979
10002
|
- mobile.css:${styleLine}
|
|
@@ -9999,6 +10022,8 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
9999
10022
|
- $DSH_HOME/mobile-access/mobile.js:手机端脚本,用 window.dshMobile.register(({ root }) => { ... }) 把内容挂载到 root,返回清理函数
|
|
10000
10023
|
- 保存后手机端几秒内自动应用,无需重启
|
|
10001
10024
|
|
|
10025
|
+
动手前先读已有的定制文件。若要调整 DSH 自带的输入框、侧栏或设置等现有界面,先确认当前 DSH 版本与手机页面模式(专用移动页面或 ?frontend=stock),只读核对运行页面的 DOM、属性和样式;本机有同版本前端源码时可参考相应组件,没有源码时可查看已安装的客户端产物,不必下载源码。优先使用扩展 API、插槽和稳定的 data-* 标记,不凭猜测依赖构建类名。只读查看不改变前述写入范围。
|
|
10026
|
+
|
|
10002
10027
|
2. 电脑端能力 —— 手机需要读电脑文件、执行命令或访问硬件时,创建扩展:
|
|
10003
10028
|
- 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
|
|
10004
10029
|
- extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
|
|
@@ -10015,6 +10040,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
10015
10040
|
|
|
10016
10041
|
完成前请自检:
|
|
10017
10042
|
- 改动涉及 mobile.js 或扩展的 mobile.js / host.mjs 时,先做语法检查再保存(如 node --check <file>),确保没有语法错误
|
|
10043
|
+
- 改动 DSH 现有界面后尽可能在实际手机页面核对效果;无法观察运行页面时,明确告诉用户尚未实测
|
|
10018
10044
|
- 创建或修改扩展后,确认 extension.json 的 schemaVersion 为 1、id 与目录名一致、且 id 只含小写字母数字和连字符
|
|
10019
10045
|
- 扩展的 host.mjs 若在完成前无法激活,先修正而不是留下损坏的扩展
|
|
10020
10046
|
- 完成后检查自己实际写入了哪些文件,向用户简要说明改了什么、手机端会有什么变化`;
|
|
@@ -11851,6 +11877,87 @@ async function regularFile(file, expectedBytes) {
|
|
|
11851
11877
|
throw error;
|
|
11852
11878
|
}
|
|
11853
11879
|
}
|
|
11880
|
+
async function installStep(code, operation) {
|
|
11881
|
+
try {
|
|
11882
|
+
return await operation();
|
|
11883
|
+
} catch (error) {
|
|
11884
|
+
if (error instanceof Error && error.message.startsWith("cpolar_")) throw error;
|
|
11885
|
+
throw new Error(code, { cause: error });
|
|
11886
|
+
}
|
|
11887
|
+
}
|
|
11888
|
+
async function pathExists(path) {
|
|
11889
|
+
try {
|
|
11890
|
+
await lstat(path);
|
|
11891
|
+
return true;
|
|
11892
|
+
} catch (error) {
|
|
11893
|
+
if (error.code === "ENOENT") return false;
|
|
11894
|
+
throw error;
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11897
|
+
/** Replace only this version's verified binary, retaining the previous directory until promotion succeeds. */
|
|
11898
|
+
async function publishVerifiedCpolarExecutable(extracted, componentRoot, componentStorage, executableName, operations = {}) {
|
|
11899
|
+
if (dirname(componentStorage) !== componentRoot || !inside(componentRoot, componentStorage) || basename(executableName) !== executableName || executableName === "." || executableName === "..") throw new Error("cpolar component storage escaped its private root");
|
|
11900
|
+
const move = operations.move ?? rename;
|
|
11901
|
+
const remove = operations.remove ?? ((path) => rm(path, {
|
|
11902
|
+
recursive: true,
|
|
11903
|
+
force: true
|
|
11904
|
+
}));
|
|
11905
|
+
const candidate = join(componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
11906
|
+
const backup = join(componentRoot, `.previous-${randomBytes(12).toString("hex")}`);
|
|
11907
|
+
let backupContainsPrevious = false;
|
|
11908
|
+
let replacementInstalled = false;
|
|
11909
|
+
try {
|
|
11910
|
+
await mkdir(candidate, {
|
|
11911
|
+
recursive: true,
|
|
11912
|
+
mode: 448
|
|
11913
|
+
});
|
|
11914
|
+
await copyFile(extracted, join(candidate, executableName));
|
|
11915
|
+
await chmod(join(candidate, executableName), 448);
|
|
11916
|
+
if (await pathExists(componentStorage)) {
|
|
11917
|
+
await move(componentStorage, backup);
|
|
11918
|
+
backupContainsPrevious = true;
|
|
11919
|
+
}
|
|
11920
|
+
try {
|
|
11921
|
+
await move(candidate, componentStorage);
|
|
11922
|
+
replacementInstalled = true;
|
|
11923
|
+
} catch (error) {
|
|
11924
|
+
if (backupContainsPrevious) try {
|
|
11925
|
+
await move(backup, componentStorage);
|
|
11926
|
+
backupContainsPrevious = false;
|
|
11927
|
+
} catch (rollbackError) {
|
|
11928
|
+
process.emitWarning(`cpolar previous component remains at ${backup}; automatic rollback failed`, { code: "DSH_MOBILE_CPOLAR_ROLLBACK_FAILED" });
|
|
11929
|
+
throw new Error("cpolar_storage_failed", { cause: new AggregateError([error, rollbackError]) });
|
|
11930
|
+
}
|
|
11931
|
+
throw error;
|
|
11932
|
+
}
|
|
11933
|
+
} finally {
|
|
11934
|
+
try {
|
|
11935
|
+
await remove(candidate);
|
|
11936
|
+
} catch {
|
|
11937
|
+
process.emitWarning(`cpolar temporary component remains at ${candidate}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
|
|
11938
|
+
}
|
|
11939
|
+
if (replacementInstalled && backupContainsPrevious) try {
|
|
11940
|
+
await remove(backup);
|
|
11941
|
+
} catch {
|
|
11942
|
+
process.emitWarning(`cpolar previous component remains at ${backup}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
|
|
11943
|
+
}
|
|
11944
|
+
}
|
|
11945
|
+
}
|
|
11946
|
+
/** Remove only this install's staging directory without replacing a preceding failure. */
|
|
11947
|
+
async function cleanupCpolarInstallStaging(staging, installFailed, remove = (path) => rm(path, {
|
|
11948
|
+
recursive: true,
|
|
11949
|
+
force: true
|
|
11950
|
+
})) {
|
|
11951
|
+
try {
|
|
11952
|
+
await remove(staging);
|
|
11953
|
+
} catch (error) {
|
|
11954
|
+
if (installFailed) {
|
|
11955
|
+
process.emitWarning(`cpolar staging directory remains at ${staging}; cleanup failed`, { code: "DSH_MOBILE_CPOLAR_CLEANUP_FAILED" });
|
|
11956
|
+
return;
|
|
11957
|
+
}
|
|
11958
|
+
throw new Error("cpolar_storage_failed", { cause: error });
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11854
11961
|
async function run(file, args) {
|
|
11855
11962
|
await new Promise((resolveRun, reject) => {
|
|
11856
11963
|
execFile(file, [...args], {
|
|
@@ -12005,11 +12112,14 @@ var CpolarComponentManager = class {
|
|
|
12005
12112
|
return this.enqueue(async () => {
|
|
12006
12113
|
const release = this.release;
|
|
12007
12114
|
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
12008
|
-
await
|
|
12009
|
-
|
|
12010
|
-
|
|
12115
|
+
const staging = await installStep("cpolar_storage_failed", async () => {
|
|
12116
|
+
await mkdir(this.stagingRoot, {
|
|
12117
|
+
recursive: true,
|
|
12118
|
+
mode: 448
|
|
12119
|
+
});
|
|
12120
|
+
return mkdtemp(join(this.stagingRoot, "install-"));
|
|
12011
12121
|
});
|
|
12012
|
-
|
|
12122
|
+
let installFailed = false;
|
|
12013
12123
|
try {
|
|
12014
12124
|
const controller = new AbortController();
|
|
12015
12125
|
const timeout = setTimeout(() => {
|
|
@@ -12018,38 +12128,35 @@ var CpolarComponentManager = class {
|
|
|
12018
12128
|
timeout.unref();
|
|
12019
12129
|
let bytes;
|
|
12020
12130
|
try {
|
|
12021
|
-
bytes = await this.fetchArtifact(release.downloadUrl, controller.signal);
|
|
12131
|
+
bytes = await installStep("cpolar_download_failed", () => this.fetchArtifact(release.downloadUrl, controller.signal));
|
|
12132
|
+
} catch (error) {
|
|
12133
|
+
if (controller.signal.aborted) throw new Error("cpolar_download_timeout", { cause: error });
|
|
12134
|
+
throw error;
|
|
12022
12135
|
} finally {
|
|
12023
12136
|
clearTimeout(timeout);
|
|
12024
12137
|
}
|
|
12025
12138
|
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cpolar_download_hash_mismatch");
|
|
12026
12139
|
const archive = join(staging, release.archiveKind === "tar.gz" ? "cpolar.tar.gz" : "cpolar.zip");
|
|
12027
|
-
await writeFile(archive, bytes, {
|
|
12140
|
+
await installStep("cpolar_storage_failed", () => writeFile(archive, bytes, {
|
|
12028
12141
|
flag: "wx",
|
|
12029
12142
|
mode: 384
|
|
12030
|
-
});
|
|
12031
|
-
await this.extractArtifact(archive, staging);
|
|
12143
|
+
}));
|
|
12144
|
+
await installStep("cpolar_extract_failed", () => this.extractArtifact(archive, staging));
|
|
12032
12145
|
const extracted = join(staging, release.executableName);
|
|
12033
|
-
if (!await regularFile(extracted, release.executableBytes)
|
|
12034
|
-
|
|
12035
|
-
await mkdir(candidate, {
|
|
12036
|
-
recursive: true,
|
|
12037
|
-
mode: 448
|
|
12038
|
-
});
|
|
12039
|
-
await copyFile(extracted, join(candidate, release.executableName));
|
|
12040
|
-
await chmod(join(candidate, release.executableName), 448);
|
|
12041
|
-
await rm(this.componentStorage, {
|
|
12042
|
-
recursive: true,
|
|
12043
|
-
force: true
|
|
12044
|
-
});
|
|
12045
|
-
await rename(candidate, this.componentStorage);
|
|
12146
|
+
if (!await installStep("cpolar_extract_failed", async () => await regularFile(extracted, release.executableBytes) && await sha256(extracted) === release.executableSha256)) throw new Error("cpolar_executable_hash_mismatch");
|
|
12147
|
+
await installStep("cpolar_storage_failed", () => publishVerifiedCpolarExecutable(extracted, this.componentRoot, this.componentStorage, release.executableName));
|
|
12046
12148
|
this.installed = true;
|
|
12047
12149
|
this.errorCode = void 0;
|
|
12150
|
+
} catch (error) {
|
|
12151
|
+
installFailed = true;
|
|
12152
|
+
try {
|
|
12153
|
+
this.installed = await regularFile(this.executable, release.executableBytes) && await sha256(this.executable) === release.executableSha256;
|
|
12154
|
+
} catch (_inspectionError) {
|
|
12155
|
+
this.installed = false;
|
|
12156
|
+
}
|
|
12157
|
+
throw error;
|
|
12048
12158
|
} finally {
|
|
12049
|
-
await
|
|
12050
|
-
recursive: true,
|
|
12051
|
-
force: true
|
|
12052
|
-
});
|
|
12159
|
+
await cleanupCpolarInstallStaging(staging, installFailed);
|
|
12053
12160
|
}
|
|
12054
12161
|
});
|
|
12055
12162
|
}
|
|
@@ -13223,6 +13330,7 @@ async function apply(ctx, config) {
|
|
|
13223
13330
|
} : void 0;
|
|
13224
13331
|
return collectConnectionDiagnostics({
|
|
13225
13332
|
dshVersion,
|
|
13333
|
+
competingRemoteChannelBoot: hasCompetingRemoteChannelBoot(ctx.webServer.collectIndexInjections?.() ?? []),
|
|
13226
13334
|
lan: {
|
|
13227
13335
|
configured: loaded.kind !== "unconfigured" || preparedLanSetup !== void 0,
|
|
13228
13336
|
running: lanController.isRunning(),
|
|
@@ -13246,13 +13354,17 @@ async function apply(ctx, config) {
|
|
|
13246
13354
|
}
|
|
13247
13355
|
}, pinnedFrpProbe === void 0 ? {} : { remote: pinnedFrpProbe });
|
|
13248
13356
|
};
|
|
13357
|
+
const authenticateDesktopAdmin = (request) => {
|
|
13358
|
+
const connection = ctx.connection;
|
|
13359
|
+
return typeof connection?.requestRejection === "function" && connection.requestRejection(request) === void 0;
|
|
13360
|
+
};
|
|
13249
13361
|
const adminRoute = {
|
|
13250
13362
|
kind: "prefix",
|
|
13251
13363
|
path: LOCAL_ADMIN_PREFIX,
|
|
13252
13364
|
handler: async (request, response) => {
|
|
13253
13365
|
try {
|
|
13254
13366
|
const target = parseRequestTarget(request.url);
|
|
13255
|
-
assertLocalAdminTrust(request, request.method === "POST");
|
|
13367
|
+
assertLocalAdminTrust(request, request.method === "POST", () => authenticateDesktopAdmin(request));
|
|
13256
13368
|
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
13257
13369
|
const lanControl = target.decodedPathname === `/api/mobile-access/control` || target.decodedPathname === `/api/mobile-access/lan/control`;
|
|
13258
13370
|
if (request.method === "GET" && lanControl) {
|
|
@@ -13341,7 +13453,15 @@ async function apply(ctx, config) {
|
|
|
13341
13453
|
}
|
|
13342
13454
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cpolar/component/install`) {
|
|
13343
13455
|
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
13344
|
-
|
|
13456
|
+
logger.info("cpolar component install started");
|
|
13457
|
+
try {
|
|
13458
|
+
await remoteProviders.mutate(async () => cpolarComponent.install());
|
|
13459
|
+
logger.info("cpolar component install completed");
|
|
13460
|
+
} catch (error) {
|
|
13461
|
+
const cause = error instanceof Error && error.cause instanceof Error ? error.cause : void 0;
|
|
13462
|
+
logger.error("cpolar component install failed: %s%s", error instanceof Error ? error.stack ?? error.message : String(error), cause === void 0 ? "" : `; cause: ${cause.message}`);
|
|
13463
|
+
throw error;
|
|
13464
|
+
}
|
|
13345
13465
|
sendJson(response, 200, remotePayload(), false);
|
|
13346
13466
|
return;
|
|
13347
13467
|
}
|
|
@@ -13584,18 +13704,18 @@ async function apply(ctx, config) {
|
|
|
13584
13704
|
if (target.decodedPathname.startsWith(`/api/mobile-access/remote/`)) {
|
|
13585
13705
|
const active = remoteController().gateway();
|
|
13586
13706
|
if (active === void 0) throw new HttpError(409, "gateway_stopped");
|
|
13587
|
-
await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/remote
|
|
13707
|
+
await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/remote`, authenticateDesktopAdmin).handler(request, response);
|
|
13588
13708
|
return;
|
|
13589
13709
|
}
|
|
13590
13710
|
if (target.decodedPathname.startsWith(`/api/mobile-access/lan/`)) {
|
|
13591
13711
|
const active = lanGateway;
|
|
13592
13712
|
if (active === void 0) throw new HttpError(409, "gateway_stopped");
|
|
13593
|
-
await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/lan
|
|
13713
|
+
await active.localAdminRoute(`${LOCAL_ADMIN_PREFIX}/lan`, authenticateDesktopAdmin).handler(request, response);
|
|
13594
13714
|
return;
|
|
13595
13715
|
}
|
|
13596
13716
|
const active = lanGateway;
|
|
13597
13717
|
if (active === void 0) throw new HttpError(409, "gateway_stopped");
|
|
13598
|
-
await active.localAdminRoute().handler(request, response);
|
|
13718
|
+
await active.localAdminRoute(LOCAL_ADMIN_PREFIX, authenticateDesktopAdmin).handler(request, response);
|
|
13599
13719
|
} catch (error) {
|
|
13600
13720
|
const mapped = mapAdminError(error);
|
|
13601
13721
|
if (response.headersSent) response.destroy();
|
|
@@ -13607,7 +13727,7 @@ async function apply(ctx, config) {
|
|
|
13607
13727
|
const unregister = ctx.webServer.register(adminRoute);
|
|
13608
13728
|
const disposeMobileCommand = ctx.commands.register({
|
|
13609
13729
|
name: "mobile",
|
|
13610
|
-
description: "
|
|
13730
|
+
description: "定制手机界面或添加电脑能力",
|
|
13611
13731
|
input: { hint: "<要做什么>" },
|
|
13612
13732
|
handler: async ({ agent, rawInput }) => {
|
|
13613
13733
|
const task = rawInput.trim();
|
|
@@ -13653,10 +13773,7 @@ async function apply(ctx, config) {
|
|
|
13653
13773
|
frp: frpStore,
|
|
13654
13774
|
origin: originStore
|
|
13655
13775
|
};
|
|
13656
|
-
await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].
|
|
13657
|
-
version: 1,
|
|
13658
|
-
enabled: false
|
|
13659
|
-
})));
|
|
13776
|
+
await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].disableIfPresent()));
|
|
13660
13777
|
for (const provider of REMOTE_PROVIDERS) await remoteControllers[provider].initialize();
|
|
13661
13778
|
const lanDiscovery = lanGateway?.discoveryStatus();
|
|
13662
13779
|
if (lanDiscovery !== void 0 && !lanDiscovery.broadcast) logger.warn("broadcast discovery is unavailable (%s); mDNS is still published, so pair manually or free the UDP port", lanDiscovery.errorCode ?? "unknown_cause");
|