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/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
6
|
import { connect, createServer } from "node:net";
|
|
7
7
|
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
8
9
|
import { execFile, spawn } from "node:child_process";
|
|
9
10
|
import { createSocket } from "node:dgram";
|
|
10
11
|
import { homedir, hostname, networkInterfaces, tmpdir } from "node:os";
|
|
@@ -1151,24 +1152,75 @@ function execFileText(file, args, options = {}) {
|
|
|
1151
1152
|
//#endregion
|
|
1152
1153
|
//#region src/private-file.ts
|
|
1153
1154
|
let userSidTask;
|
|
1155
|
+
function commandFailureDetails(error) {
|
|
1156
|
+
if (error === null || typeof error !== "object") return {};
|
|
1157
|
+
let descriptors;
|
|
1158
|
+
try {
|
|
1159
|
+
descriptors = Object.getOwnPropertyDescriptors(error);
|
|
1160
|
+
} catch (_error) {
|
|
1161
|
+
return {};
|
|
1162
|
+
}
|
|
1163
|
+
const code = descriptors.code?.value;
|
|
1164
|
+
const signal = descriptors.signal?.value;
|
|
1165
|
+
const killed = descriptors.killed?.value;
|
|
1166
|
+
return {
|
|
1167
|
+
...code === null || typeof code === "number" && Number.isSafeInteger(code) || typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/u.test(code) ? { code } : {},
|
|
1168
|
+
...signal === null || typeof signal === "string" && /^SIG[A-Z0-9]{1,16}$/u.test(signal) ? { signal } : {},
|
|
1169
|
+
...typeof killed === "boolean" ? { killed } : {}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/** Phase and full await duration, including subprocess creation, without arguments, output or private paths. */
|
|
1173
|
+
var PrivateFilePermissionError = class extends Error {
|
|
1174
|
+
stage;
|
|
1175
|
+
elapsedMs;
|
|
1176
|
+
code;
|
|
1177
|
+
signal;
|
|
1178
|
+
killed;
|
|
1179
|
+
constructor(stage, elapsedMs, error) {
|
|
1180
|
+
const details = commandFailureDetails(error);
|
|
1181
|
+
const fields = [
|
|
1182
|
+
...details.code === void 0 ? [] : [`code=${String(details.code)}`],
|
|
1183
|
+
...details.signal === void 0 ? [] : [`signal=${String(details.signal)}`],
|
|
1184
|
+
...details.killed === void 0 ? [] : [`killed=${String(details.killed)}`]
|
|
1185
|
+
];
|
|
1186
|
+
const summary = `Windows private-file ${stage} failed after ${String(elapsedMs)} ms${fields.length === 0 ? "" : ` (${fields.join(", ")})`}`;
|
|
1187
|
+
const cause = Object.assign(/* @__PURE__ */ new Error("Windows permission operation failed"), details);
|
|
1188
|
+
super(summary, { cause });
|
|
1189
|
+
this.stage = stage;
|
|
1190
|
+
this.elapsedMs = elapsedMs;
|
|
1191
|
+
this.name = "PrivateFilePermissionError";
|
|
1192
|
+
this.code = details.code;
|
|
1193
|
+
this.signal = details.signal;
|
|
1194
|
+
this.killed = details.killed;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
async function permissionOperation(stage, operation) {
|
|
1198
|
+
const started = performance$1.now();
|
|
1199
|
+
try {
|
|
1200
|
+
return await operation();
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
throw new PrivateFilePermissionError(stage, Math.max(0, Math.round(performance$1.now() - started)), error);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1154
1205
|
function windowsSystemTool(name) {
|
|
1155
1206
|
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
1156
|
-
if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw new Error("Windows system root is unavailable");
|
|
1207
|
+
if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw Object.assign(/* @__PURE__ */ new Error("Windows system root is unavailable"), { code: "WINDOWS_SYSTEM_ROOT_UNAVAILABLE" });
|
|
1157
1208
|
return win32.join(systemRoot, "System32", name);
|
|
1158
1209
|
}
|
|
1159
1210
|
async function currentWindowsUserSid() {
|
|
1160
|
-
userSidTask ??=
|
|
1161
|
-
"
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1211
|
+
userSidTask ??= permissionOperation("sid-query", async () => {
|
|
1212
|
+
const { stdout } = await execFileText(windowsSystemTool("whoami.exe"), [
|
|
1213
|
+
"/user",
|
|
1214
|
+
"/fo",
|
|
1215
|
+
"csv",
|
|
1216
|
+
"/nh"
|
|
1217
|
+
], {
|
|
1218
|
+
encoding: "utf8",
|
|
1219
|
+
windowsHide: true,
|
|
1220
|
+
timeout: 1e4
|
|
1221
|
+
});
|
|
1170
1222
|
const match = /,"(S-\d(?:-\d+)+)"\s*$/u.exec(stdout.trim());
|
|
1171
|
-
if (match?.[1] === void 0) throw new Error("unable to resolve the current Windows user SID");
|
|
1223
|
+
if (match?.[1] === void 0) throw Object.assign(/* @__PURE__ */ new Error("unable to resolve the current Windows user SID"), { code: "WINDOWS_USER_SID_INVALID" });
|
|
1172
1224
|
return match[1];
|
|
1173
1225
|
}).catch((error) => {
|
|
1174
1226
|
userSidTask = void 0;
|
|
@@ -1176,12 +1228,12 @@ async function currentWindowsUserSid() {
|
|
|
1176
1228
|
});
|
|
1177
1229
|
return userSidTask;
|
|
1178
1230
|
}
|
|
1179
|
-
/** Restrict a sensitive regular file to the current user and Windows administrators. */
|
|
1231
|
+
/** Restrict a sensitive regular file to the current user and Windows administrators; reject Windows permission failures with redacted phase and duration. */
|
|
1180
1232
|
async function restrictPrivateFile(file, mode = 384) {
|
|
1181
1233
|
await chmod(file, mode);
|
|
1182
1234
|
if (process.platform !== "win32") return;
|
|
1183
1235
|
const userSid = await currentWindowsUserSid();
|
|
1184
|
-
await execFileText(windowsSystemTool("icacls.exe"), [
|
|
1236
|
+
await permissionOperation("acl-apply", () => execFileText(windowsSystemTool("icacls.exe"), [
|
|
1185
1237
|
file,
|
|
1186
1238
|
"/inheritance:r",
|
|
1187
1239
|
"/grant:r",
|
|
@@ -1196,7 +1248,7 @@ async function restrictPrivateFile(file, mode = 384) {
|
|
|
1196
1248
|
encoding: "utf8",
|
|
1197
1249
|
windowsHide: true,
|
|
1198
1250
|
timeout: 1e4
|
|
1199
|
-
});
|
|
1251
|
+
}));
|
|
1200
1252
|
}
|
|
1201
1253
|
//#endregion
|
|
1202
1254
|
//#region src/client-module-preferences.ts
|
|
@@ -1457,24 +1509,23 @@ var JsonMobileAccessControlStore = class {
|
|
|
1457
1509
|
this.file = file;
|
|
1458
1510
|
this.initiallyEnabled = initiallyEnabled;
|
|
1459
1511
|
}
|
|
1460
|
-
/**
|
|
1512
|
+
/** Disable an existing preference, returning the state only after private-file preparation completes. */
|
|
1461
1513
|
async disableIfPresent() {
|
|
1514
|
+
const disabled = Object.freeze({
|
|
1515
|
+
version: 1,
|
|
1516
|
+
enabled: false
|
|
1517
|
+
});
|
|
1462
1518
|
try {
|
|
1463
1519
|
await lstat(this.file);
|
|
1464
1520
|
} catch (error) {
|
|
1465
1521
|
if (error.code === "ENOENT") {
|
|
1466
|
-
if (this.initiallyEnabled) await this.save(
|
|
1467
|
-
|
|
1468
|
-
enabled: false
|
|
1469
|
-
});
|
|
1470
|
-
return;
|
|
1522
|
+
if (this.initiallyEnabled) await this.save(disabled);
|
|
1523
|
+
return disabled;
|
|
1471
1524
|
}
|
|
1472
1525
|
throw error;
|
|
1473
1526
|
}
|
|
1474
|
-
await this.save(
|
|
1475
|
-
|
|
1476
|
-
enabled: false
|
|
1477
|
-
});
|
|
1527
|
+
await this.save(disabled);
|
|
1528
|
+
return disabled;
|
|
1478
1529
|
}
|
|
1479
1530
|
async load() {
|
|
1480
1531
|
let stat;
|
|
@@ -1517,8 +1568,8 @@ var JsonMobileAccessControlStore = class {
|
|
|
1517
1568
|
flag: "wx",
|
|
1518
1569
|
mode: 384
|
|
1519
1570
|
});
|
|
1571
|
+
await restrictPrivateFile(temporary);
|
|
1520
1572
|
await rename(temporary, this.file);
|
|
1521
|
-
await restrictPrivateFile(this.file);
|
|
1522
1573
|
} catch (error) {
|
|
1523
1574
|
try {
|
|
1524
1575
|
await rm(temporary, { force: true });
|
|
@@ -1960,6 +2011,44 @@ function ensureMobileCompatibility(html) {
|
|
|
1960
2011
|
return `${html.slice(0, first.index)}${script}${html.slice(first.index)}`;
|
|
1961
2012
|
}
|
|
1962
2013
|
//#endregion
|
|
2014
|
+
//#region src/http-cache.ts
|
|
2015
|
+
/** Entity-tag handling for authenticated GET and HEAD cache validation. */
|
|
2016
|
+
const ENTITY_TAG = /^[\t ]*(?:W\/)?("[\x21\x23-\x7e\x80-\xff]*")[\t ]*$/u;
|
|
2017
|
+
const ENTITY_TAG_LIST_ITEM = /(?:W\/)?"[\x21\x23-\x7e\x80-\xff]*"/gu;
|
|
2018
|
+
/**
|
|
2019
|
+
* Compare a GET or HEAD If-None-Match field with an existing representation.
|
|
2020
|
+
* Invalid fields do not match; empty list elements are ignored per HTTP list syntax.
|
|
2021
|
+
* @param field - The received conditional request field.
|
|
2022
|
+
* @param etag - The selected representation's valid entity tag.
|
|
2023
|
+
* @returns Whether the representation is unchanged under weak comparison.
|
|
2024
|
+
*/
|
|
2025
|
+
function matchesIfNoneMatch(field, etag) {
|
|
2026
|
+
if (field === void 0) return false;
|
|
2027
|
+
const current = ENTITY_TAG.exec(etag)?.[1];
|
|
2028
|
+
if (current === void 0) return false;
|
|
2029
|
+
if (/^[\t ]*\*[\t ]*$/u.test(field)) return true;
|
|
2030
|
+
let cursor = 0;
|
|
2031
|
+
let count = 0;
|
|
2032
|
+
let matches = false;
|
|
2033
|
+
for (const item of field.matchAll(ENTITY_TAG_LIST_ITEM)) {
|
|
2034
|
+
const gap = field.slice(cursor, item.index);
|
|
2035
|
+
if (!(count === 0 ? /^[\t ,]*$/u : /^[\t ]*,[\t ,]*$/u).test(gap)) return false;
|
|
2036
|
+
if ((item[0].startsWith("W/") ? item[0].slice(2) : item[0]) === current) matches = true;
|
|
2037
|
+
cursor = item.index + item[0].length;
|
|
2038
|
+
count++;
|
|
2039
|
+
}
|
|
2040
|
+
return count > 0 && /^[\t ,]*$/u.test(field.slice(cursor)) && matches;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Keep an upstream validator usable after lossless content encoding changes.
|
|
2044
|
+
* @param etag - An upstream entity tag, if supplied.
|
|
2045
|
+
* @returns A weak quoted entity tag, or undefined for an absent or invalid tag.
|
|
2046
|
+
*/
|
|
2047
|
+
function weakenEntityTag(etag) {
|
|
2048
|
+
const opaque = etag === void 0 ? void 0 : ENTITY_TAG.exec(etag)?.[1];
|
|
2049
|
+
return opaque === void 0 ? void 0 : `W/${opaque}`;
|
|
2050
|
+
}
|
|
2051
|
+
//#endregion
|
|
1963
2052
|
//#region src/browser-auth-cookies.ts
|
|
1964
2053
|
const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
|
|
1965
2054
|
const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
|
|
@@ -2993,15 +3082,15 @@ const HOST_ACTIVATION_TIMEOUT_MS = 5e3;
|
|
|
2993
3082
|
const RETIRED_GENERATION_TTL_MS = 6e5;
|
|
2994
3083
|
/** Extension teardown is advisory and must never stop watcher progress. */
|
|
2995
3084
|
const HOST_TEARDOWN_TIMEOUT_MS = 2e3;
|
|
2996
|
-
async function withActivationTimeout(promise, id, signal) {
|
|
3085
|
+
async function withActivationTimeout(promise, id, signal, timeoutMs) {
|
|
2997
3086
|
let timer;
|
|
2998
3087
|
let onAbort;
|
|
2999
3088
|
try {
|
|
3000
3089
|
return await Promise.race([
|
|
3001
3090
|
promise,
|
|
3002
|
-
new Promise((_, reject) => {
|
|
3003
|
-
timer = setTimeout(() => reject(new MobileExtensionError("host_load_timeout", `extension ${id} activation timed out`, 500)),
|
|
3004
|
-
}),
|
|
3091
|
+
...timeoutMs === void 0 ? [] : [new Promise((_, reject) => {
|
|
3092
|
+
timer = setTimeout(() => reject(new MobileExtensionError("host_load_timeout", `extension ${id} activation timed out`, 500)), timeoutMs);
|
|
3093
|
+
})],
|
|
3005
3094
|
new Promise((_, reject) => {
|
|
3006
3095
|
const abort = () => {
|
|
3007
3096
|
reject(new MobileExtensionError("host_activation_closed", `extension ${id} activation is closed`, 409));
|
|
@@ -3554,7 +3643,7 @@ var MobileAccessService = class extends Service {
|
|
|
3554
3643
|
if (extension === void 0) throw new MobileExtensionError("extension_not_found", "extension not found", 404);
|
|
3555
3644
|
const route = ("host" in extension ? extension.host : extension).routes?.find((candidate) => {
|
|
3556
3645
|
if (candidate.method !== method) return false;
|
|
3557
|
-
return (candidate.kind ?? "exact") === "exact" ? candidate.path === pathname : pathname === candidate.path || pathname.startsWith(`${candidate.path}/`);
|
|
3646
|
+
return (candidate.kind ?? "exact") === "exact" ? candidate.path === pathname : candidate.path === "/" || pathname === candidate.path || pathname.startsWith(`${candidate.path}/`);
|
|
3558
3647
|
});
|
|
3559
3648
|
if (route === void 0) throw new MobileExtensionError("route_not_found", "route not found", 404);
|
|
3560
3649
|
if ("host" in extension) extension.controller.signal.throwIfAborted();
|
|
@@ -3963,7 +4052,7 @@ async function loadLocalExtension(directory, context, known, parentSignal, execu
|
|
|
3963
4052
|
let host;
|
|
3964
4053
|
if (worker !== void 0) {
|
|
3965
4054
|
try {
|
|
3966
|
-
await withActivationTimeout(worker.activate(), manifest.id, controller.signal);
|
|
4055
|
+
await withActivationTimeout(worker.activate(), manifest.id, controller.signal, void 0);
|
|
3967
4056
|
} catch (error) {
|
|
3968
4057
|
await worker.terminate("activation-failed");
|
|
3969
4058
|
throw error;
|
|
@@ -4007,7 +4096,7 @@ async function loadLocalExtension(directory, context, known, parentSignal, execu
|
|
|
4007
4096
|
}
|
|
4008
4097
|
await Promise.all(pendingEffects);
|
|
4009
4098
|
};
|
|
4010
|
-
await withActivationTimeout(activate(), manifest.id, controller.signal);
|
|
4099
|
+
await withActivationTimeout(activate(), manifest.id, controller.signal, HOST_ACTIVATION_TIMEOUT_MS);
|
|
4011
4100
|
host = validateDefinition({
|
|
4012
4101
|
...manifest,
|
|
4013
4102
|
actions,
|
|
@@ -4070,8 +4159,14 @@ const COPY = {
|
|
|
4070
4159
|
deviceName: "Device name",
|
|
4071
4160
|
pair: "Pair",
|
|
4072
4161
|
pairing: "Pairing…",
|
|
4073
|
-
|
|
4074
|
-
|
|
4162
|
+
pairingCodeHint: "Use the 43-character pairing code shown on the computer. Keys starting with dsh1. or dsh2. are for the Android app.",
|
|
4163
|
+
invalidPairingCode: "Enter the 43-character pairing code from the current pairing window on the computer.",
|
|
4164
|
+
appKeyNotCode: "This is an Android app key. Use it in the app, or copy the browser pairing code from the computer.",
|
|
4165
|
+
pairFailed: "Pairing failed. Check the code or generate a new one on the computer.",
|
|
4166
|
+
pairingExpired: "The pairing code is invalid or has expired. Generate a new code on the computer and try again.",
|
|
4167
|
+
pairingLimited: "Too many pairing attempts. Wait a moment and try again.",
|
|
4168
|
+
deviceLimit: "The paired-device limit has been reached. Remove an unused device and generate a new pairing code on the computer.",
|
|
4169
|
+
unavailable: "The computer is unavailable. Check the connection and try again.",
|
|
4075
4170
|
reconnectTitle: "Reconnect DSH mobile access",
|
|
4076
4171
|
reconnectHeading: "Reconnect this device",
|
|
4077
4172
|
restoring: "Restoring the secure Session…",
|
|
@@ -4086,8 +4181,14 @@ const COPY = {
|
|
|
4086
4181
|
deviceName: "设备名称",
|
|
4087
4182
|
pair: "配对",
|
|
4088
4183
|
pairing: "正在配对…",
|
|
4089
|
-
|
|
4090
|
-
|
|
4184
|
+
pairingCodeHint: "请输入电脑端显示的 43 位配对码。以 dsh1. 或 dsh2. 开头的密钥用于 Android App。",
|
|
4185
|
+
invalidPairingCode: "请输入电脑端当前配对窗口中的 43 位配对码。",
|
|
4186
|
+
appKeyNotCode: "这是 Android App 密钥。请在 App 中使用,或回到电脑端复制浏览器配对码。",
|
|
4187
|
+
pairFailed: "配对失败,请检查输入或在电脑端重新生成配对码。",
|
|
4188
|
+
pairingExpired: "配对码无效或已过期,请在电脑端重新生成后重试。",
|
|
4189
|
+
pairingLimited: "配对尝试过于频繁,请稍后再试。",
|
|
4190
|
+
deviceLimit: "已达到配对设备上限,请在电脑端移除不需要的设备后重新生成配对码。",
|
|
4191
|
+
unavailable: "电脑当前不可用,请检查连接后重试。",
|
|
4091
4192
|
reconnectTitle: "重新连接 DSH 移动访问",
|
|
4092
4193
|
reconnectHeading: "重新连接此设备",
|
|
4093
4194
|
restoring: "正在恢复安全会话…",
|
|
@@ -4102,8 +4203,14 @@ const COPY = {
|
|
|
4102
4203
|
deviceName: "Nome dispositivo",
|
|
4103
4204
|
pair: "Abbina",
|
|
4104
4205
|
pairing: "Abbinamento…",
|
|
4105
|
-
|
|
4106
|
-
|
|
4206
|
+
pairingCodeHint: "Usa il codice di abbinamento di 43 caratteri mostrato sul computer. Le chiavi che iniziano con dsh1. o dsh2. sono per l’app Android.",
|
|
4207
|
+
invalidPairingCode: "Inserisci il codice di 43 caratteri dalla finestra di abbinamento attuale sul computer.",
|
|
4208
|
+
appKeyNotCode: "Questa è una chiave per l’app Android. Usala nell’app oppure copia il codice di abbinamento per il browser dal computer.",
|
|
4209
|
+
pairFailed: "Abbinamento non riuscito. Controlla il codice o generane uno nuovo sul computer.",
|
|
4210
|
+
pairingExpired: "Il codice non è valido o è scaduto. Genera un nuovo codice sul computer e riprova.",
|
|
4211
|
+
pairingLimited: "Troppi tentativi di abbinamento. Attendi un momento e riprova.",
|
|
4212
|
+
deviceLimit: "È stato raggiunto il limite di dispositivi abbinati. Rimuovi un dispositivo inutilizzato e genera un nuovo codice sul computer.",
|
|
4213
|
+
unavailable: "Il computer non è disponibile. Controlla la connessione e riprova.",
|
|
4107
4214
|
reconnectTitle: "Riconnetti accesso mobile DSH",
|
|
4108
4215
|
reconnectHeading: "Riconnetti questo dispositivo",
|
|
4109
4216
|
restoring: "Ripristino della sessione sicura…",
|
|
@@ -4142,7 +4249,8 @@ function renderPairPage(locale) {
|
|
|
4142
4249
|
<main>
|
|
4143
4250
|
<h1>${copy.pairHeading}</h1>
|
|
4144
4251
|
<form id="pair-form">
|
|
4145
|
-
<label>${copy.pairingCode} <input id="pair-token" autocomplete="one-time-code" required></label>
|
|
4252
|
+
<label>${copy.pairingCode} <input id="pair-token" autocomplete="one-time-code" autocapitalize="off" spellcheck="false" aria-describedby="pair-token-hint pair-status" required></label>
|
|
4253
|
+
<p id="pair-token-hint">${copy.pairingCodeHint}</p>
|
|
4146
4254
|
<label>${copy.deviceName} <input id="device-label" maxlength="64" autocomplete="off"></label>
|
|
4147
4255
|
<button type="submit">${copy.pair}</button>
|
|
4148
4256
|
<output id="pair-status" aria-live="polite"></output>
|
|
@@ -4160,27 +4268,54 @@ function renderPairScript(locale) {
|
|
|
4160
4268
|
const token = document.getElementById('pair-token')
|
|
4161
4269
|
const label = document.getElementById('device-label')
|
|
4162
4270
|
const status = document.getElementById('pair-status')
|
|
4271
|
+
const submit = form.querySelector('button[type="submit"]')
|
|
4272
|
+
let submitting = false
|
|
4163
4273
|
const fragment = new URLSearchParams(location.hash.slice(1))
|
|
4164
4274
|
const supplied = fragment.get('token')
|
|
4165
4275
|
history.replaceState(null, '', location.pathname)
|
|
4166
4276
|
if (supplied) token.value = supplied
|
|
4277
|
+
token.addEventListener('input', () => {
|
|
4278
|
+
token.removeAttribute('aria-invalid')
|
|
4279
|
+
if (!submitting) status.value = ''
|
|
4280
|
+
})
|
|
4167
4281
|
form.addEventListener('submit', async (event) => {
|
|
4168
4282
|
event.preventDefault()
|
|
4283
|
+
if (submitting) return
|
|
4284
|
+
const code = token.value.trim()
|
|
4285
|
+
token.removeAttribute('aria-invalid')
|
|
4286
|
+
if (/^dsh[12]\\./u.test(code) || !/^[A-Za-z0-9_-]{43}$/u.test(code)) {
|
|
4287
|
+
status.value = /^dsh[12]\\./u.test(code)
|
|
4288
|
+
? ${JSON.stringify(copy.appKeyNotCode)} : ${JSON.stringify(copy.invalidPairingCode)}
|
|
4289
|
+
token.setAttribute('aria-invalid', 'true')
|
|
4290
|
+
token.focus()
|
|
4291
|
+
return
|
|
4292
|
+
}
|
|
4293
|
+
submitting = true
|
|
4294
|
+
submit.disabled = true
|
|
4169
4295
|
status.value = ${JSON.stringify(copy.pairing)}
|
|
4170
4296
|
try {
|
|
4171
4297
|
const response = await fetch('/mobile-access/auth/pair', {
|
|
4172
4298
|
method: 'POST',
|
|
4173
4299
|
credentials: 'same-origin',
|
|
4174
4300
|
headers: { 'content-type': 'application/json' },
|
|
4175
|
-
body: JSON.stringify({ token:
|
|
4301
|
+
body: JSON.stringify({ token: code, label: label.value || undefined }),
|
|
4176
4302
|
})
|
|
4177
4303
|
if (!response.ok) {
|
|
4178
|
-
status.value =
|
|
4304
|
+
if (token.value.trim() !== code) { status.value = ''; return }
|
|
4305
|
+
status.value = response.status === 401 ? ${JSON.stringify(copy.pairingExpired)}
|
|
4306
|
+
: response.status === 429 ? ${JSON.stringify(copy.pairingLimited)}
|
|
4307
|
+
: response.status === 409 ? ${JSON.stringify(copy.deviceLimit)}
|
|
4308
|
+
: response.status >= 500 ? ${JSON.stringify(copy.unavailable)}
|
|
4309
|
+
: ${JSON.stringify(copy.pairFailed)}
|
|
4310
|
+
if (response.status === 401) token.setAttribute('aria-invalid', 'true')
|
|
4179
4311
|
return
|
|
4180
4312
|
}
|
|
4181
4313
|
location.replace('/')
|
|
4182
4314
|
} catch {
|
|
4183
|
-
status.value = ${JSON.stringify(copy.unavailable)}
|
|
4315
|
+
status.value = token.value.trim() === code ? ${JSON.stringify(copy.unavailable)} : ''
|
|
4316
|
+
} finally {
|
|
4317
|
+
submitting = false
|
|
4318
|
+
submit.disabled = false
|
|
4184
4319
|
}
|
|
4185
4320
|
})
|
|
4186
4321
|
})()
|
|
@@ -4793,6 +4928,7 @@ function sanitizeRequestHeaders(request, upstream) {
|
|
|
4793
4928
|
"if-match",
|
|
4794
4929
|
"if-modified-since",
|
|
4795
4930
|
"if-none-match",
|
|
4931
|
+
"if-range",
|
|
4796
4932
|
"if-unmodified-since",
|
|
4797
4933
|
"range",
|
|
4798
4934
|
"user-agent"
|
|
@@ -4872,13 +5008,13 @@ function isCompressibleContentType(value) {
|
|
|
4872
5008
|
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
4873
5009
|
return mediaType.startsWith("text/") || /^(?:application\/(?:javascript|json|xml|x-javascript)|image\/svg\+xml)$/u.test(mediaType);
|
|
4874
5010
|
}
|
|
4875
|
-
function
|
|
5011
|
+
function canCompressResponse(request, response) {
|
|
4876
5012
|
const pathname = request.url?.split("?", 1)[0] ?? "";
|
|
4877
|
-
return request.method === "GET" && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) && response.statusCode === 200 && request.headers.range === void 0 && response.headers["content-range"] === void 0 && response.headers["content-encoding"] === void 0 &&
|
|
5013
|
+
return (request.method === "GET" || request.method === "HEAD") && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) && response.statusCode === 200 && request.headers.range === void 0 && response.headers["content-range"] === void 0 && response.headers["content-encoding"] === void 0 && isCompressibleContentType(response.headers["content-type"]);
|
|
4878
5014
|
}
|
|
4879
5015
|
function revisionedStaticCacheControl(request, statusCode) {
|
|
4880
5016
|
if (request.method !== "GET" && request.method !== "HEAD") return void 0;
|
|
4881
|
-
if (statusCode !== 200) return void 0;
|
|
5017
|
+
if (statusCode !== 200 && statusCode !== 304) return void 0;
|
|
4882
5018
|
let target;
|
|
4883
5019
|
try {
|
|
4884
5020
|
target = new URL(request.url ?? "/", "https://dsh-mobile.invalid");
|
|
@@ -5689,7 +5825,7 @@ var MobileAccessGateway = class {
|
|
|
5689
5825
|
const clientModules = target.decodedPathname === `${AUTH_PREFIX}/client-modules`;
|
|
5690
5826
|
const requestedExtension = extensionTarget(target.decodedPathname);
|
|
5691
5827
|
const requestedMobileBootBatch = mobileBootBatchKey(target.decodedPathname);
|
|
5692
|
-
const customAsset = request.method === "GET" ? target.decodedPathname === `/mobile-access/custom.css` ? {
|
|
5828
|
+
const customAsset = request.method === "GET" || request.method === "HEAD" ? target.decodedPathname === `/mobile-access/custom.css` ? {
|
|
5693
5829
|
file: this.config.customCssFile,
|
|
5694
5830
|
contentType: "text/css; charset=utf-8",
|
|
5695
5831
|
fallback: CUSTOM_STYLE_FALLBACK
|
|
@@ -5798,23 +5934,23 @@ var MobileAccessGateway = class {
|
|
|
5798
5934
|
body = Buffer.from(customAsset.fallback);
|
|
5799
5935
|
}
|
|
5800
5936
|
if (body.byteLength > 262144) throw new HttpError(413, "payload_too_large");
|
|
5801
|
-
const etag = createHash("sha256").update(body).digest("hex")
|
|
5802
|
-
const
|
|
5803
|
-
if (
|
|
5937
|
+
const etag = `"${createHash("sha256").update(body).digest("hex")}"`;
|
|
5938
|
+
const responseHeaders = { ETag: etag };
|
|
5939
|
+
if (mtime !== void 0) responseHeaders["Last-Modified"] = mtime.toUTCString();
|
|
5940
|
+
if (matchesIfNoneMatch(headerValue(request.headers, "if-none-match"), etag)) {
|
|
5804
5941
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
5805
|
-
response.writeHead(304);
|
|
5942
|
+
response.writeHead(304, responseHeaders);
|
|
5806
5943
|
response.end();
|
|
5807
5944
|
return;
|
|
5808
5945
|
}
|
|
5809
5946
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
5810
|
-
|
|
5947
|
+
response.writeHead(200, {
|
|
5948
|
+
...responseHeaders,
|
|
5811
5949
|
"Content-Type": customAsset.contentType,
|
|
5812
|
-
"Content-Length": body.byteLength
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
response.writeHead(200, responseHeaders);
|
|
5817
|
-
response.end(body);
|
|
5950
|
+
"Content-Length": body.byteLength
|
|
5951
|
+
});
|
|
5952
|
+
if (request.method === "HEAD") response.end();
|
|
5953
|
+
else response.end(body);
|
|
5818
5954
|
return;
|
|
5819
5955
|
} finally {
|
|
5820
5956
|
operation.release();
|
|
@@ -5890,10 +6026,10 @@ var MobileAccessGateway = class {
|
|
|
5890
6026
|
styleRevision
|
|
5891
6027
|
}
|
|
5892
6028
|
}));
|
|
5893
|
-
const etag = createHash("sha256").update(body).update(extensions.contentDigest()).digest("hex")
|
|
5894
|
-
if (headerValue(request.headers, "if-none-match")
|
|
6029
|
+
const etag = `"${createHash("sha256").update(body).update(extensions.contentDigest()).digest("hex")}"`;
|
|
6030
|
+
if (matchesIfNoneMatch(headerValue(request.headers, "if-none-match"), etag)) {
|
|
5895
6031
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
5896
|
-
response.writeHead(304);
|
|
6032
|
+
response.writeHead(304, { ETag: etag });
|
|
5897
6033
|
response.end();
|
|
5898
6034
|
return;
|
|
5899
6035
|
}
|
|
@@ -5916,9 +6052,10 @@ var MobileAccessGateway = class {
|
|
|
5916
6052
|
const operation = this.allocateRequest(authorization, response, {});
|
|
5917
6053
|
try {
|
|
5918
6054
|
const file = targetInfo.kind === "script" ? await extensions.readClientFile(targetInfo.id, "script", operation.signal, generation) : targetInfo.kind === "style" ? await extensions.readClientFile(targetInfo.id, "style", operation.signal, generation) : await extensions.readAsset(targetInfo.id, targetInfo.path ?? "", operation.signal, generation);
|
|
5919
|
-
|
|
6055
|
+
const etag = `"${file.digest}"`;
|
|
6056
|
+
if (matchesIfNoneMatch(headerValue(request.headers, "if-none-match"), etag)) {
|
|
5920
6057
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
5921
|
-
response.writeHead(304);
|
|
6058
|
+
response.writeHead(304, { ETag: etag });
|
|
5922
6059
|
response.end();
|
|
5923
6060
|
return;
|
|
5924
6061
|
}
|
|
@@ -5927,7 +6064,7 @@ var MobileAccessGateway = class {
|
|
|
5927
6064
|
response.writeHead(200, {
|
|
5928
6065
|
"Content-Type": contentType,
|
|
5929
6066
|
"Content-Length": file.body.byteLength,
|
|
5930
|
-
ETag:
|
|
6067
|
+
ETag: etag
|
|
5931
6068
|
});
|
|
5932
6069
|
if (request.method === "HEAD") response.end();
|
|
5933
6070
|
else response.end(file.body);
|
|
@@ -6034,36 +6171,41 @@ var MobileAccessGateway = class {
|
|
|
6034
6171
|
}
|
|
6035
6172
|
}
|
|
6036
6173
|
async sendExtensionResponse(response, result, head) {
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6174
|
+
try {
|
|
6175
|
+
const status = result.status ?? 200;
|
|
6176
|
+
if (!Number.isSafeInteger(status) || status < 200 || status > 599) throw new MobileExtensionError("invalid_route_response", "extension returned an invalid HTTP status", 500);
|
|
6177
|
+
const contentType = result.contentType ?? "application/octet-stream";
|
|
6178
|
+
if (contentType.length > 1024 || !/^[\x20-\x7e]+$/u.test(contentType) || !/^[\w!#$&+.^-]+\/[\w!#$&+.^-]+(?:;[\x20-\x7e]*)?$/u.test(contentType)) throw new MobileExtensionError("invalid_route_response", "extension returned an invalid content type", 500);
|
|
6179
|
+
const safeHeaders = {};
|
|
6180
|
+
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
6181
|
+
if (!/^(?:content-disposition|cache-control|etag)$/iu.test(name) || /[\r\n]/u.test(value)) continue;
|
|
6182
|
+
safeHeaders[name] = value;
|
|
6183
|
+
}
|
|
6184
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
6185
|
+
if (typeof result.body === "string" || result.body instanceof Uint8Array) {
|
|
6186
|
+
const body = typeof result.body === "string" ? Buffer.from(result.body) : Buffer.from(result.body);
|
|
6187
|
+
if (body.byteLength > 4194304) throw new MobileExtensionError("extension_result_too_large", "extension response is too large", 500);
|
|
6188
|
+
response.writeHead(status, {
|
|
6189
|
+
...safeHeaders,
|
|
6190
|
+
"Content-Type": contentType,
|
|
6191
|
+
"Content-Length": body.byteLength
|
|
6192
|
+
});
|
|
6193
|
+
if (head) response.end();
|
|
6194
|
+
else response.end(body);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6050
6197
|
response.writeHead(status, {
|
|
6051
6198
|
...safeHeaders,
|
|
6052
|
-
"Content-Type": contentType
|
|
6053
|
-
"Content-Length": body.byteLength
|
|
6199
|
+
"Content-Type": contentType
|
|
6054
6200
|
});
|
|
6055
|
-
if (head)
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
if (head) {
|
|
6064
|
-
result.body.destroy();
|
|
6065
|
-
response.end();
|
|
6066
|
-
return;
|
|
6201
|
+
if (head) {
|
|
6202
|
+
result.body.destroy();
|
|
6203
|
+
response.end();
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
} catch (error) {
|
|
6207
|
+
disposeLateExtensionResponse(result);
|
|
6208
|
+
throw error;
|
|
6067
6209
|
}
|
|
6068
6210
|
await pipeline(result.body, new ByteLimitTransform(4194304), response);
|
|
6069
6211
|
}
|
|
@@ -6479,7 +6621,7 @@ var MobileAccessGateway = class {
|
|
|
6479
6621
|
const assembled = stored.body;
|
|
6480
6622
|
if (assembled === void 0) throw new HttpError(502, "upstream_unavailable");
|
|
6481
6623
|
const body = compressed ? stored.gzipBody ??= await gzipBuffer(assembled) : assembled;
|
|
6482
|
-
const etag = compressed ? `${stored.etag}-gzip` : stored.etag
|
|
6624
|
+
const etag = `"${compressed ? `${stored.etag}-gzip` : stored.etag}"`;
|
|
6483
6625
|
const headers = {
|
|
6484
6626
|
"Content-Type": "text/javascript; charset=utf-8",
|
|
6485
6627
|
"Content-Length": body.byteLength,
|
|
@@ -6488,7 +6630,7 @@ var MobileAccessGateway = class {
|
|
|
6488
6630
|
};
|
|
6489
6631
|
if (compressed) headers["Content-Encoding"] = "gzip";
|
|
6490
6632
|
addVaryAcceptEncoding(headers);
|
|
6491
|
-
if (headerValue(request.headers, "if-none-match")
|
|
6633
|
+
if (matchesIfNoneMatch(headerValue(request.headers, "if-none-match"), etag)) {
|
|
6492
6634
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
6493
6635
|
response.writeHead(304, {
|
|
6494
6636
|
ETag: etag,
|
|
@@ -6735,16 +6877,28 @@ var MobileAccessGateway = class {
|
|
|
6735
6877
|
const statusCode = proxied.statusCode ?? 502;
|
|
6736
6878
|
const cacheControl = revisionedStaticCacheControl(request$2, statusCode);
|
|
6737
6879
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
6738
|
-
const
|
|
6880
|
+
const compressible = canCompressResponse(request$2, proxied);
|
|
6881
|
+
const compressed = compressible && acceptsGzip(request$2.headers["accept-encoding"]);
|
|
6882
|
+
if (compressible) addVaryAcceptEncoding(headers);
|
|
6739
6883
|
if (compressed) {
|
|
6740
6884
|
delete headers["accept-ranges"];
|
|
6741
6885
|
delete headers["content-length"];
|
|
6742
|
-
|
|
6886
|
+
const etag = weakenEntityTag(proxied.headers.etag);
|
|
6887
|
+
if (etag === void 0) delete headers.etag;
|
|
6888
|
+
else headers.etag = etag;
|
|
6743
6889
|
headers["content-encoding"] = "gzip";
|
|
6890
|
+
} else if (statusCode === 304 && (request$2.method === "GET" || request$2.method === "HEAD") && request$2.headers.range === void 0 && (request$2.url?.split("?", 1)[0]?.startsWith("/plugins/") === true || request$2.url?.split("?", 1)[0]?.startsWith("/assets/") === true)) {
|
|
6891
|
+
if (proxied.headers["content-encoding"] === void 0 && acceptsGzip(request$2.headers["accept-encoding"])) {
|
|
6892
|
+
delete headers["content-length"];
|
|
6893
|
+
delete headers["accept-ranges"];
|
|
6894
|
+
const etag = weakenEntityTag(proxied.headers.etag);
|
|
6895
|
+
if (etag === void 0) delete headers.etag;
|
|
6896
|
+
else headers.etag = etag;
|
|
6897
|
+
}
|
|
6744
6898
|
addVaryAcceptEncoding(headers);
|
|
6745
6899
|
}
|
|
6746
6900
|
response.writeHead(statusCode, headers);
|
|
6747
|
-
await Promise.all([bodyDone, compressed ? pipeline(proxied, createGzip(), response) : pipeline(proxied, response)]);
|
|
6901
|
+
await Promise.all([bodyDone, compressed && request$2.method !== "HEAD" ? pipeline(proxied, createGzip(), response) : pipeline(proxied, response)]);
|
|
6748
6902
|
} catch (error) {
|
|
6749
6903
|
holder.request?.destroy();
|
|
6750
6904
|
await bodyDone?.catch(() => void 0);
|
|
@@ -7475,6 +7629,118 @@ async function replaceManagedDirectory(root, target, candidate, prefix, operatio
|
|
|
7475
7629
|
}
|
|
7476
7630
|
}
|
|
7477
7631
|
//#endregion
|
|
7632
|
+
//#region src/component-download.ts
|
|
7633
|
+
/**
|
|
7634
|
+
* Hosts a pinned GitHub release download may redirect to.
|
|
7635
|
+
*
|
|
7636
|
+
* `github.com/<owner>/<repo>/releases/download/<tag>/<asset>` does not serve the bytes itself: it
|
|
7637
|
+
* answers 302 with a signed URL on a release-asset host. A fetch configured with
|
|
7638
|
+
* `redirect: 'error'` fails that hop as `TypeError: fetch failed` (cause: `unexpected redirect`)
|
|
7639
|
+
* before a single byte is transferred, which is exactly how the cloudflared component could never
|
|
7640
|
+
* install. Following one validated hop keeps the origin restriction while allowing the shape
|
|
7641
|
+
* every GitHub release download uses.
|
|
7642
|
+
*/
|
|
7643
|
+
const RELEASE_ASSET_HOSTS = Object.freeze(["release-assets.githubusercontent.com", "objects.githubusercontent.com"]);
|
|
7644
|
+
/** Release an unused body without replacing a download or validation failure. */
|
|
7645
|
+
async function discardBody(body) {
|
|
7646
|
+
if (body === null) return;
|
|
7647
|
+
try {
|
|
7648
|
+
await body.cancel();
|
|
7649
|
+
} catch (_error) {}
|
|
7650
|
+
}
|
|
7651
|
+
/** Follow one redirect hop after checking its scheme and host. A second hop is refused. */
|
|
7652
|
+
async function followValidatedRedirect(response, request) {
|
|
7653
|
+
let target;
|
|
7654
|
+
try {
|
|
7655
|
+
const location = response.headers.get("location");
|
|
7656
|
+
if (location === null) throw new Error(`${request.errorPrefix}_download_redirect_missing`);
|
|
7657
|
+
try {
|
|
7658
|
+
target = new URL(location, request.url);
|
|
7659
|
+
} catch {
|
|
7660
|
+
throw new Error(`${request.errorPrefix}_download_redirect_invalid`);
|
|
7661
|
+
}
|
|
7662
|
+
const hosts = request.redirectHosts ?? RELEASE_ASSET_HOSTS;
|
|
7663
|
+
if (target.protocol !== "https:" || !hosts.includes(target.hostname)) throw new Error(`${request.errorPrefix}_download_redirect_rejected`);
|
|
7664
|
+
} finally {
|
|
7665
|
+
await discardBody(response.body);
|
|
7666
|
+
}
|
|
7667
|
+
return fetch(target, {
|
|
7668
|
+
redirect: "error",
|
|
7669
|
+
signal: request.signal
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
/** Attempts per install. A 55 MB transfer through a TUN proxy can reset mid-stream: the first
|
|
7673
|
+
* real run in the development environment failed after 1.5 MB and succeeded on the next attempt,
|
|
7674
|
+
* which is exactly the confusing "nothing happened" the user saw. */
|
|
7675
|
+
const DOWNLOAD_ATTEMPTS = 2;
|
|
7676
|
+
const RETRY_DELAY_MS = 750;
|
|
7677
|
+
/** Whether a failure is a transport problem worth a second attempt rather than a verdict. */
|
|
7678
|
+
function isRetryable(error) {
|
|
7679
|
+
if (error instanceof TypeError) return true;
|
|
7680
|
+
if (error instanceof Error && /_download_http_5\d\d$/u.test(error.message)) return true;
|
|
7681
|
+
return false;
|
|
7682
|
+
}
|
|
7683
|
+
function delay(ms) {
|
|
7684
|
+
return new Promise((resolve) => {
|
|
7685
|
+
setTimeout(resolve, ms);
|
|
7686
|
+
});
|
|
7687
|
+
}
|
|
7688
|
+
async function attemptDownload(request) {
|
|
7689
|
+
const first = await fetch(request.url, {
|
|
7690
|
+
redirect: "manual",
|
|
7691
|
+
signal: request.signal
|
|
7692
|
+
});
|
|
7693
|
+
const response = first.status >= 300 && first.status < 400 ? await followValidatedRedirect(first, request) : first;
|
|
7694
|
+
let reader;
|
|
7695
|
+
let ended = false;
|
|
7696
|
+
try {
|
|
7697
|
+
if (!response.ok) throw new Error(`${request.errorPrefix}_download_http_${String(response.status)}`);
|
|
7698
|
+
const contentLength = response.headers.get("content-length");
|
|
7699
|
+
if (contentLength !== null && (!/^\d+$/u.test(contentLength) || Number(contentLength) !== request.expectedBytes)) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
7700
|
+
if (response.body === null) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
7701
|
+
const bytes = new Uint8Array(request.expectedBytes);
|
|
7702
|
+
reader = response.body.getReader();
|
|
7703
|
+
let received = 0;
|
|
7704
|
+
while (true) {
|
|
7705
|
+
const chunk = await reader.read();
|
|
7706
|
+
if (chunk.done) {
|
|
7707
|
+
ended = true;
|
|
7708
|
+
break;
|
|
7709
|
+
}
|
|
7710
|
+
if (chunk.value.byteLength > bytes.byteLength - received) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
7711
|
+
bytes.set(chunk.value, received);
|
|
7712
|
+
received += chunk.value.byteLength;
|
|
7713
|
+
}
|
|
7714
|
+
if (received !== request.expectedBytes) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
7715
|
+
return bytes;
|
|
7716
|
+
} finally {
|
|
7717
|
+
if (reader === void 0) await discardBody(response.body);
|
|
7718
|
+
else try {
|
|
7719
|
+
if (!ended) await discardBody(reader);
|
|
7720
|
+
} finally {
|
|
7721
|
+
reader.releaseLock();
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7725
|
+
/**
|
|
7726
|
+
* Download a pinned artifact, following at most one validated redirect, and check its exact length.
|
|
7727
|
+
* A transport failure is retried once; a length or redirect verdict is not. The caller still
|
|
7728
|
+
* verifies the SHA-256 before publishing anything.
|
|
7729
|
+
* @param request - the pinned URL, its exact byte length and the caller's error prefix.
|
|
7730
|
+
* @returns the artifact bytes.
|
|
7731
|
+
*/
|
|
7732
|
+
async function downloadPinnedArtifact(request) {
|
|
7733
|
+
let lastError;
|
|
7734
|
+
for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt += 1) try {
|
|
7735
|
+
return await attemptDownload(request);
|
|
7736
|
+
} catch (error) {
|
|
7737
|
+
lastError = error;
|
|
7738
|
+
if (attempt === DOWNLOAD_ATTEMPTS || !isRetryable(error)) throw error;
|
|
7739
|
+
await delay(RETRY_DELAY_MS);
|
|
7740
|
+
}
|
|
7741
|
+
throw lastError;
|
|
7742
|
+
}
|
|
7743
|
+
//#endregion
|
|
7478
7744
|
//#region src/frp-component.ts
|
|
7479
7745
|
const FRP_VERSION$1 = "0.70.1";
|
|
7480
7746
|
const MAX_ARCHIVE_ENTRIES$1 = 128;
|
|
@@ -7605,39 +7871,12 @@ async function defaultExtractArtifact$2(archive, destination, executableName) {
|
|
|
7605
7871
|
await copyFile(extracted, join(destination, executableName));
|
|
7606
7872
|
}
|
|
7607
7873
|
async function defaultFetchArtifact$2(artifact, signal) {
|
|
7608
|
-
|
|
7609
|
-
|
|
7874
|
+
return downloadPinnedArtifact({
|
|
7875
|
+
url: artifact.downloadUrl,
|
|
7876
|
+
expectedBytes: artifact.downloadBytes,
|
|
7877
|
+
errorPrefix: "frp",
|
|
7610
7878
|
signal
|
|
7611
7879
|
});
|
|
7612
|
-
if (!response.ok) throw new Error(`frp_download_http_${String(response.status)}`);
|
|
7613
|
-
const finalUrl = new URL(response.url);
|
|
7614
|
-
const officialHost = finalUrl.hostname === "github.com" || finalUrl.hostname.endsWith(".githubusercontent.com");
|
|
7615
|
-
if (finalUrl.protocol !== "https:" || !officialHost) throw new Error("frp_download_origin_invalid");
|
|
7616
|
-
const lengthHeader = response.headers.get("content-length");
|
|
7617
|
-
const declaredLength = lengthHeader === null ? void 0 : Number(lengthHeader);
|
|
7618
|
-
if (declaredLength !== void 0 && (!Number.isFinite(declaredLength) || declaredLength !== artifact.downloadBytes)) throw new Error("frp_download_size_mismatch");
|
|
7619
|
-
if (response.body === null) throw new Error("frp_download_empty");
|
|
7620
|
-
const chunks = [];
|
|
7621
|
-
let received = 0;
|
|
7622
|
-
const reader = response.body.getReader();
|
|
7623
|
-
while (true) {
|
|
7624
|
-
const result = await reader.read();
|
|
7625
|
-
if (result.done) break;
|
|
7626
|
-
received += result.value.byteLength;
|
|
7627
|
-
if (received > artifact.downloadBytes) {
|
|
7628
|
-
await reader.cancel();
|
|
7629
|
-
throw new Error("frp_download_size_mismatch");
|
|
7630
|
-
}
|
|
7631
|
-
chunks.push(result.value);
|
|
7632
|
-
}
|
|
7633
|
-
if (received !== artifact.downloadBytes) throw new Error("frp_download_size_mismatch");
|
|
7634
|
-
const bytes = new Uint8Array(received);
|
|
7635
|
-
let offset = 0;
|
|
7636
|
-
for (const chunk of chunks) {
|
|
7637
|
-
bytes.set(chunk, offset);
|
|
7638
|
-
offset += chunk.byteLength;
|
|
7639
|
-
}
|
|
7640
|
-
return bytes;
|
|
7641
7880
|
}
|
|
7642
7881
|
async function defaultInspectExecutable(executable) {
|
|
7643
7882
|
return (await runCapture$1(executable, ["--version"])).trim();
|
|
@@ -8984,7 +9223,7 @@ async function readManagedCa(setup) {
|
|
|
8984
9223
|
const [certPem, keyPem] = await Promise.all([readFile(setup.caCertFile, "utf8"), readFile(setup.caKeyFile, "utf8")]);
|
|
8985
9224
|
return assertMatchingCa(certPem, keyPem);
|
|
8986
9225
|
}
|
|
8987
|
-
async function
|
|
9226
|
+
async function stagePrivateFile(file, contents) {
|
|
8988
9227
|
const directory = dirname(file);
|
|
8989
9228
|
await mkdir(directory, {
|
|
8990
9229
|
recursive: true,
|
|
@@ -8992,17 +9231,85 @@ async function atomicWrite(file, contents) {
|
|
|
8992
9231
|
});
|
|
8993
9232
|
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
8994
9233
|
try {
|
|
8995
|
-
await writeFile(temporary,
|
|
9234
|
+
await writeFile(temporary, "", {
|
|
8996
9235
|
mode: 384,
|
|
8997
9236
|
flag: "wx"
|
|
8998
9237
|
});
|
|
8999
9238
|
await restrictPrivateFile(temporary);
|
|
9239
|
+
await writeFile(temporary, contents);
|
|
9240
|
+
return temporary;
|
|
9241
|
+
} catch (error) {
|
|
9242
|
+
await rm(temporary, { force: true });
|
|
9243
|
+
throw error;
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
async function atomicWrite(file, contents) {
|
|
9247
|
+
const temporary = await stagePrivateFile(file, contents);
|
|
9248
|
+
try {
|
|
9000
9249
|
await rename(temporary, file);
|
|
9001
9250
|
await restrictPrivateFile(file);
|
|
9002
9251
|
} finally {
|
|
9003
9252
|
await rm(temporary, { force: true });
|
|
9004
9253
|
}
|
|
9005
9254
|
}
|
|
9255
|
+
const certificatePublications = /* @__PURE__ */ new Map();
|
|
9256
|
+
/** Stage both files before promotion and retain a private rollback copy until the pair is installed. */
|
|
9257
|
+
async function installServerCertificatePair(output, cert, key) {
|
|
9258
|
+
const identity = `${output.certFile}\0${output.keyFile}`;
|
|
9259
|
+
const publication = (certificatePublications.get(identity) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
9260
|
+
const files = [output.certFile, output.keyFile];
|
|
9261
|
+
const contents = [cert, key];
|
|
9262
|
+
const staged = [];
|
|
9263
|
+
const backups = [];
|
|
9264
|
+
let promoted = 0;
|
|
9265
|
+
let restored = true;
|
|
9266
|
+
const failures = [];
|
|
9267
|
+
try {
|
|
9268
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
9269
|
+
const file = files[index];
|
|
9270
|
+
staged.push(await stagePrivateFile(file, contents[index]));
|
|
9271
|
+
let old;
|
|
9272
|
+
try {
|
|
9273
|
+
const metadata = await lstat(file);
|
|
9274
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error("managed TLS output is not a regular file");
|
|
9275
|
+
old = await readFile(file);
|
|
9276
|
+
} catch (error) {
|
|
9277
|
+
if (error.code !== "ENOENT") throw error;
|
|
9278
|
+
}
|
|
9279
|
+
backups.push(old === void 0 ? void 0 : await stagePrivateFile(file, old));
|
|
9280
|
+
}
|
|
9281
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
9282
|
+
await rename(staged[index], files[index]);
|
|
9283
|
+
promoted += 1;
|
|
9284
|
+
}
|
|
9285
|
+
} catch (error) {
|
|
9286
|
+
failures.push(error);
|
|
9287
|
+
for (let index = promoted - 1; index >= 0; index -= 1) try {
|
|
9288
|
+
const backup = backups[index];
|
|
9289
|
+
if (backup === void 0) await rm(files[index], { force: true });
|
|
9290
|
+
else await rename(backup, files[index]);
|
|
9291
|
+
} catch (restoreError) {
|
|
9292
|
+
restored = false;
|
|
9293
|
+
failures.push(restoreError);
|
|
9294
|
+
}
|
|
9295
|
+
} finally {
|
|
9296
|
+
const cleanup = await Promise.allSettled(staged.map((file) => rm(file, { force: true })));
|
|
9297
|
+
for (const result of cleanup) if (result.status === "rejected") failures.push(result.reason);
|
|
9298
|
+
if (restored) {
|
|
9299
|
+
const backupCleanup = await Promise.allSettled(backups.filter((file) => file !== void 0).map((file) => rm(file, { force: true })));
|
|
9300
|
+
for (const result of backupCleanup) if (result.status === "rejected") failures.push(result.reason);
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
if (failures.length > 1) throw new AggregateError(failures, "managed TLS certificate replacement, rollback or cleanup failed");
|
|
9304
|
+
if (failures.length === 1) throw failures[0];
|
|
9305
|
+
});
|
|
9306
|
+
certificatePublications.set(identity, publication);
|
|
9307
|
+
try {
|
|
9308
|
+
await publication;
|
|
9309
|
+
} finally {
|
|
9310
|
+
if (certificatePublications.get(identity) === publication) certificatePublications.delete(identity);
|
|
9311
|
+
}
|
|
9312
|
+
}
|
|
9006
9313
|
/** Create a long-lived CA or migrate the legacy self-signed server certificate as that CA. */
|
|
9007
9314
|
async function ensureManagedCa(setup, legacy) {
|
|
9008
9315
|
let certPem;
|
|
@@ -9117,7 +9424,7 @@ async function issueServerCertificate(authority, target, output, lifetimeDays =
|
|
|
9117
9424
|
}
|
|
9118
9425
|
]
|
|
9119
9426
|
});
|
|
9120
|
-
await
|
|
9427
|
+
await installServerCertificatePair(output, server.cert, server.private);
|
|
9121
9428
|
}
|
|
9122
9429
|
/** Sign and atomically install a server leaf for the interface's current address. */
|
|
9123
9430
|
async function refreshManagedServerCertificate(setup, address) {
|
|
@@ -9254,12 +9561,12 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
9254
9561
|
if (previousFingerprint === void 0) await writeIngressIdentity(paths.statusFile, caFingerprint, ownedRoot);
|
|
9255
9562
|
const publicHost = new URL(settings.publicOrigin).hostname;
|
|
9256
9563
|
let leaf;
|
|
9564
|
+
let matchingKey = false;
|
|
9257
9565
|
try {
|
|
9258
9566
|
leaf = new X509Certificate(await readFile(paths.certFile));
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
if (!(leaf !== void 0 && Date.parse(leaf.validTo) > now + 864e5 && coversPublicIp(leaf, publicHost) && leaf.verify(ca.publicKey))) {
|
|
9567
|
+
if (leafKeyExists) matchingKey = leaf.checkPrivateKey(createPrivateKey(await readFile(paths.keyFile)));
|
|
9568
|
+
} catch (_error) {}
|
|
9569
|
+
if (!(leaf !== void 0 && matchingKey && Date.parse(leaf.validFrom) <= now && Date.parse(leaf.validTo) > now + 864e5 && coversPublicIp(leaf, publicHost) && leaf.verify(ca.publicKey))) {
|
|
9263
9570
|
await assertManagedParents(ownedRoot, paths.certFile, "frp_ingress");
|
|
9264
9571
|
await issueServerCertificate({
|
|
9265
9572
|
caCertFile: paths.caCertFile,
|
|
@@ -9385,6 +9692,18 @@ function aggregateErrors(errors, message) {
|
|
|
9385
9692
|
if (errors.length === 1 && errors[0] instanceof Error) return errors[0];
|
|
9386
9693
|
return new AggregateError(errors, message);
|
|
9387
9694
|
}
|
|
9695
|
+
/**
|
|
9696
|
+
* Prepare each inactive provider's disabled state and settle every write before reporting a failure.
|
|
9697
|
+
* @param selected - Provider whose startup must still read its durable preference.
|
|
9698
|
+
* @param stores - The same stores supplied to the corresponding provider controllers.
|
|
9699
|
+
* @returns Fresh disabled states for this initialization only; no permission-success cache is retained.
|
|
9700
|
+
*/
|
|
9701
|
+
async function prepareDisabledRemoteProviders(selected, stores) {
|
|
9702
|
+
const results = await Promise.allSettled(REMOTE_PROVIDERS.filter((provider) => provider !== selected).map(async (provider) => [provider, await stores[provider].disableIfPresent()]));
|
|
9703
|
+
const failure = aggregateErrors(results.filter((result) => result.status === "rejected").map((result) => result.reason), "remote provider control preparation failed");
|
|
9704
|
+
if (failure !== void 0) throw failure;
|
|
9705
|
+
return new Map(results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []));
|
|
9706
|
+
}
|
|
9388
9707
|
/** Settle independent remote cleanup work before reporting any collected failure. */
|
|
9389
9708
|
async function settleRemoteResources(steps, message = "remote resource cleanup failed") {
|
|
9390
9709
|
const failure = aggregateErrors((await Promise.allSettled(steps.map(async (step) => step()))).filter((result) => result.status === "rejected").map((result) => result.reason), message);
|
|
@@ -9837,8 +10156,8 @@ var FrpController = class {
|
|
|
9837
10156
|
if (options.ingressCertificateCheckMs !== void 0 && (!Number.isSafeInteger(options.ingressCertificateCheckMs) || options.ingressCertificateCheckMs < 1 || options.ingressCertificateCheckMs > 2147483647)) throw new Error("frp_ingress_check_interval_invalid");
|
|
9838
10157
|
}
|
|
9839
10158
|
/** Restore the remembered FRP switch without changing LAN or other providers. */
|
|
9840
|
-
async initialize() {
|
|
9841
|
-
const state = await this.options.store.load();
|
|
10159
|
+
async initialize(initialState) {
|
|
10160
|
+
const state = initialState ?? await this.options.store.load();
|
|
9842
10161
|
this.enabled = state.enabled;
|
|
9843
10162
|
this.initialized = true;
|
|
9844
10163
|
if (this.enabled) await this.start();
|
|
@@ -9860,12 +10179,12 @@ var FrpController = class {
|
|
|
9860
10179
|
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
9861
10180
|
await this.enqueue(async () => {
|
|
9862
10181
|
if (this.enabled === enabled && (enabled === false || this.child !== void 0)) return;
|
|
9863
|
-
if (!enabled) await this.stop();
|
|
9864
|
-
this.enabled = enabled;
|
|
9865
10182
|
await this.options.store.save({
|
|
9866
10183
|
version: 1,
|
|
9867
10184
|
enabled
|
|
9868
10185
|
});
|
|
10186
|
+
this.enabled = enabled;
|
|
10187
|
+
if (!enabled) await this.stop();
|
|
9869
10188
|
if (enabled) await this.start();
|
|
9870
10189
|
else this.publish({
|
|
9871
10190
|
enabled: false,
|
|
@@ -9879,11 +10198,11 @@ var FrpController = class {
|
|
|
9879
10198
|
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
9880
10199
|
await this.enqueue(async () => {
|
|
9881
10200
|
if (!this.enabled) {
|
|
9882
|
-
this.enabled = true;
|
|
9883
10201
|
await this.options.store.save({
|
|
9884
10202
|
version: 1,
|
|
9885
10203
|
enabled: true
|
|
9886
10204
|
});
|
|
10205
|
+
this.enabled = true;
|
|
9887
10206
|
}
|
|
9888
10207
|
await this.stop();
|
|
9889
10208
|
await this.start();
|
|
@@ -9894,12 +10213,12 @@ var FrpController = class {
|
|
|
9894
10213
|
async reset() {
|
|
9895
10214
|
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
9896
10215
|
await this.enqueue(async () => {
|
|
9897
|
-
await this.stop();
|
|
9898
|
-
this.enabled = false;
|
|
9899
10216
|
await this.options.store.save({
|
|
9900
10217
|
version: 1,
|
|
9901
10218
|
enabled: false
|
|
9902
10219
|
});
|
|
10220
|
+
this.enabled = false;
|
|
10221
|
+
await this.stop();
|
|
9903
10222
|
this.publish({
|
|
9904
10223
|
enabled: false,
|
|
9905
10224
|
state: "off"
|
|
@@ -10509,96 +10828,6 @@ var OriginController = class {
|
|
|
10509
10828
|
}
|
|
10510
10829
|
};
|
|
10511
10830
|
//#endregion
|
|
10512
|
-
//#region src/component-download.ts
|
|
10513
|
-
/**
|
|
10514
|
-
* Hosts a pinned GitHub release download may redirect to.
|
|
10515
|
-
*
|
|
10516
|
-
* `github.com/<owner>/<repo>/releases/download/<tag>/<asset>` does not serve the bytes itself: it
|
|
10517
|
-
* answers 302 with a signed URL on a release-asset host. A fetch configured with
|
|
10518
|
-
* `redirect: 'error'` fails that hop as `TypeError: fetch failed` (cause: `unexpected redirect`)
|
|
10519
|
-
* before a single byte is transferred, which is exactly how the cloudflared component could never
|
|
10520
|
-
* install. Following one validated hop keeps the origin restriction while allowing the shape
|
|
10521
|
-
* every GitHub release download uses.
|
|
10522
|
-
*/
|
|
10523
|
-
const RELEASE_ASSET_HOSTS = Object.freeze(["release-assets.githubusercontent.com", "objects.githubusercontent.com"]);
|
|
10524
|
-
/** Follow one redirect hop after checking its scheme and host. A second hop is refused. */
|
|
10525
|
-
async function followValidatedRedirect(response, request) {
|
|
10526
|
-
const location = response.headers.get("location");
|
|
10527
|
-
if (location === null) throw new Error(`${request.errorPrefix}_download_redirect_missing`);
|
|
10528
|
-
let target;
|
|
10529
|
-
try {
|
|
10530
|
-
target = new URL(location, request.url);
|
|
10531
|
-
} catch {
|
|
10532
|
-
throw new Error(`${request.errorPrefix}_download_redirect_invalid`);
|
|
10533
|
-
}
|
|
10534
|
-
const hosts = request.redirectHosts ?? RELEASE_ASSET_HOSTS;
|
|
10535
|
-
if (target.protocol !== "https:" || !hosts.includes(target.hostname)) throw new Error(`${request.errorPrefix}_download_redirect_rejected`);
|
|
10536
|
-
return fetch(target, {
|
|
10537
|
-
redirect: "error",
|
|
10538
|
-
signal: request.signal
|
|
10539
|
-
});
|
|
10540
|
-
}
|
|
10541
|
-
/** Attempts per install. A 55 MB transfer through a TUN proxy can reset mid-stream: the first
|
|
10542
|
-
* real run in the development environment failed after 1.5 MB and succeeded on the next attempt,
|
|
10543
|
-
* which is exactly the confusing "nothing happened" the user saw. */
|
|
10544
|
-
const DOWNLOAD_ATTEMPTS = 2;
|
|
10545
|
-
const RETRY_DELAY_MS = 750;
|
|
10546
|
-
/** Whether a failure is a transport problem worth a second attempt rather than a verdict. */
|
|
10547
|
-
function isRetryable(error) {
|
|
10548
|
-
if (error instanceof TypeError) return true;
|
|
10549
|
-
if (error instanceof Error && /_download_http_5\d\d$/u.test(error.message)) return true;
|
|
10550
|
-
return false;
|
|
10551
|
-
}
|
|
10552
|
-
function delay(ms) {
|
|
10553
|
-
return new Promise((resolve) => {
|
|
10554
|
-
setTimeout(resolve, ms);
|
|
10555
|
-
});
|
|
10556
|
-
}
|
|
10557
|
-
async function attemptDownload(request) {
|
|
10558
|
-
const first = await fetch(request.url, {
|
|
10559
|
-
redirect: "manual",
|
|
10560
|
-
signal: request.signal
|
|
10561
|
-
});
|
|
10562
|
-
const response = first.status >= 300 && first.status < 400 ? await followValidatedRedirect(first, request) : first;
|
|
10563
|
-
if (!response.ok) throw new Error(`${request.errorPrefix}_download_http_${String(response.status)}`);
|
|
10564
|
-
const contentLength = response.headers.get("content-length");
|
|
10565
|
-
if (contentLength !== null && (!/^\d+$/u.test(contentLength) || Number(contentLength) !== request.expectedBytes)) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
10566
|
-
if (response.body === null) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
10567
|
-
const bytes = new Uint8Array(request.expectedBytes);
|
|
10568
|
-
const reader = response.body.getReader();
|
|
10569
|
-
let received = 0;
|
|
10570
|
-
while (true) {
|
|
10571
|
-
const chunk = await reader.read();
|
|
10572
|
-
if (chunk.done) break;
|
|
10573
|
-
if (chunk.value.byteLength > bytes.byteLength - received) {
|
|
10574
|
-
await reader.cancel();
|
|
10575
|
-
throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
10576
|
-
}
|
|
10577
|
-
bytes.set(chunk.value, received);
|
|
10578
|
-
received += chunk.value.byteLength;
|
|
10579
|
-
}
|
|
10580
|
-
if (received !== request.expectedBytes) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
10581
|
-
return bytes;
|
|
10582
|
-
}
|
|
10583
|
-
/**
|
|
10584
|
-
* Download a pinned artifact, following at most one validated redirect, and check its exact length.
|
|
10585
|
-
* A transport failure is retried once; a length or redirect verdict is not. The caller still
|
|
10586
|
-
* verifies the SHA-256 before publishing anything.
|
|
10587
|
-
* @param request - the pinned URL, its exact byte length and the caller's error prefix.
|
|
10588
|
-
* @returns the artifact bytes.
|
|
10589
|
-
*/
|
|
10590
|
-
async function downloadPinnedArtifact(request) {
|
|
10591
|
-
let lastError;
|
|
10592
|
-
for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt += 1) try {
|
|
10593
|
-
return await attemptDownload(request);
|
|
10594
|
-
} catch (error) {
|
|
10595
|
-
lastError = error;
|
|
10596
|
-
if (attempt === DOWNLOAD_ATTEMPTS || !isRetryable(error)) throw error;
|
|
10597
|
-
await delay(RETRY_DELAY_MS);
|
|
10598
|
-
}
|
|
10599
|
-
throw lastError;
|
|
10600
|
-
}
|
|
10601
|
-
//#endregion
|
|
10602
10831
|
//#region src/cloudflared-component.ts
|
|
10603
10832
|
/** Upper bound for `tar -t` output; a pinned release listing is a few lines. */
|
|
10604
10833
|
const MAX_ARCHIVE_LIST_BYTES = 1048576;
|
|
@@ -11091,8 +11320,8 @@ var CloudflaredController = class {
|
|
|
11091
11320
|
if (!isAbsolute(options.executable)) throw new Error("cloudflared executable path must be absolute");
|
|
11092
11321
|
}
|
|
11093
11322
|
/** Restore the remembered cloudflared switch independently from LAN and other providers. */
|
|
11094
|
-
async initialize() {
|
|
11095
|
-
const state = await this.options.store.load();
|
|
11323
|
+
async initialize(initialState) {
|
|
11324
|
+
const state = initialState ?? await this.options.store.load();
|
|
11096
11325
|
this.enabled = state.enabled;
|
|
11097
11326
|
this.initialized = true;
|
|
11098
11327
|
if (this.enabled) await this.start();
|
|
@@ -11114,12 +11343,12 @@ var CloudflaredController = class {
|
|
|
11114
11343
|
if (!this.initialized || this.disposed) throw new Error("cloudflared controller is unavailable");
|
|
11115
11344
|
await this.enqueue(async () => {
|
|
11116
11345
|
if (this.enabled === enabled && (enabled === false || this.child !== void 0)) return;
|
|
11117
|
-
if (!enabled) await this.stop();
|
|
11118
|
-
this.enabled = enabled;
|
|
11119
11346
|
await this.options.store.save({
|
|
11120
11347
|
version: 1,
|
|
11121
11348
|
enabled
|
|
11122
11349
|
});
|
|
11350
|
+
this.enabled = enabled;
|
|
11351
|
+
if (!enabled) await this.stop();
|
|
11123
11352
|
if (enabled) await this.start();
|
|
11124
11353
|
else this.publish({
|
|
11125
11354
|
enabled: false,
|
|
@@ -11133,11 +11362,11 @@ var CloudflaredController = class {
|
|
|
11133
11362
|
if (!this.initialized || this.disposed) throw new Error("cloudflared controller is unavailable");
|
|
11134
11363
|
await this.enqueue(async () => {
|
|
11135
11364
|
if (!this.enabled) {
|
|
11136
|
-
this.enabled = true;
|
|
11137
11365
|
await this.options.store.save({
|
|
11138
11366
|
version: 1,
|
|
11139
11367
|
enabled: true
|
|
11140
11368
|
});
|
|
11369
|
+
this.enabled = true;
|
|
11141
11370
|
}
|
|
11142
11371
|
await this.stop();
|
|
11143
11372
|
await this.start();
|
|
@@ -11148,12 +11377,12 @@ var CloudflaredController = class {
|
|
|
11148
11377
|
async reset() {
|
|
11149
11378
|
if (!this.initialized || this.disposed) throw new Error("cloudflared controller is unavailable");
|
|
11150
11379
|
await this.enqueue(async () => {
|
|
11151
|
-
await this.stop();
|
|
11152
|
-
this.enabled = false;
|
|
11153
11380
|
await this.options.store.save({
|
|
11154
11381
|
version: 1,
|
|
11155
11382
|
enabled: false
|
|
11156
11383
|
});
|
|
11384
|
+
this.enabled = false;
|
|
11385
|
+
await this.stop();
|
|
11157
11386
|
this.publish({
|
|
11158
11387
|
enabled: false,
|
|
11159
11388
|
state: "off"
|
|
@@ -12695,17 +12924,6 @@ else
|
|
|
12695
12924
|
printf '%s\n' "$caddy_import" > /etc/caddy/Caddyfile
|
|
12696
12925
|
chmod 0644 /etc/caddy/Caddyfile
|
|
12697
12926
|
caddyfile_ready=true
|
|
12698
|
-
elif grep -q '^# Managed by DSH Mobile$' /etc/caddy/Caddyfile; then
|
|
12699
|
-
# Legacy whole-file layout: the entire file is ours by construction.
|
|
12700
|
-
printf '%s\n' "$caddy_import" > /etc/caddy/Caddyfile
|
|
12701
|
-
chmod 0644 /etc/caddy/Caddyfile
|
|
12702
|
-
caddyfile_ready=true
|
|
12703
|
-
elif grep -q '^:80[[:space:]]*{' /etc/caddy/Caddyfile \
|
|
12704
|
-
&& grep -q 'root [*] /usr/share/caddy' /etc/caddy/Caddyfile \
|
|
12705
|
-
&& grep -q '^[[:space:]]*file_server[[:space:]]*$' /etc/caddy/Caddyfile; then
|
|
12706
|
-
printf '%s\n' "$caddy_import" > /etc/caddy/Caddyfile
|
|
12707
|
-
chmod 0644 /etc/caddy/Caddyfile
|
|
12708
|
-
caddyfile_ready=true
|
|
12709
12927
|
fi
|
|
12710
12928
|
fi
|
|
12711
12929
|
if [ "$caddyfile_ready" != true ]; then
|
|
@@ -13126,13 +13344,8 @@ if grep -Eq '^[[:space:]]*import[[:space:]]+/etc/caddy/dsh-mobile-dsh\.caddy([[:
|
|
|
13126
13344
|
fi
|
|
13127
13345
|
check caddy ok "已移除 DSH Mobile 站点引入;其余 Caddy 配置保持原样。"
|
|
13128
13346
|
elif [ -f /etc/caddy/Caddyfile ] && grep -q '^# Managed by DSH Mobile$' /etc/caddy/Caddyfile; then
|
|
13129
|
-
#
|
|
13130
|
-
|
|
13131
|
-
chmod 0644 /etc/caddy/Caddyfile
|
|
13132
|
-
if command -v caddy >/dev/null 2>&1; then
|
|
13133
|
-
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy.service || systemctl restart caddy.service || true
|
|
13134
|
-
fi
|
|
13135
|
-
check caddy ok "已清空旧版 DSH Mobile 管理的 Caddy 站点;请按需恢复自己的配置。"
|
|
13347
|
+
# A legacy marker does not establish ownership of subsequently added sites.
|
|
13348
|
+
check caddy warning "已保留旧版 Caddyfile;请手动移除 DSH Mobile 站点,不要删除其他站点。"
|
|
13136
13349
|
else
|
|
13137
13350
|
check caddy ok "Caddyfile 非 DSH Mobile 管理,保持原样。"
|
|
13138
13351
|
fi
|
|
@@ -13339,6 +13552,7 @@ var FunnelController = class {
|
|
|
13339
13552
|
state: "off"
|
|
13340
13553
|
});
|
|
13341
13554
|
queue = Promise.resolve();
|
|
13555
|
+
closeTask;
|
|
13342
13556
|
startTimer;
|
|
13343
13557
|
ownedRoot;
|
|
13344
13558
|
constructor(options) {
|
|
@@ -13348,7 +13562,7 @@ var FunnelController = class {
|
|
|
13348
13562
|
if (!isAbsolute(this.ownedRoot) || resolve(this.ownedRoot) === resolve(options.stateDirectory)) throw new Error("Funnel state must be a child of its owned root");
|
|
13349
13563
|
}
|
|
13350
13564
|
/** Restore the remote switch without coupling it to LAN availability. */
|
|
13351
|
-
async initialize() {
|
|
13565
|
+
async initialize(initialState) {
|
|
13352
13566
|
try {
|
|
13353
13567
|
await this.assertStateParents();
|
|
13354
13568
|
} catch (_error) {
|
|
@@ -13361,7 +13575,7 @@ var FunnelController = class {
|
|
|
13361
13575
|
});
|
|
13362
13576
|
return;
|
|
13363
13577
|
}
|
|
13364
|
-
const state = await this.options.store.load();
|
|
13578
|
+
const state = initialState ?? await this.options.store.load();
|
|
13365
13579
|
this.enabled = state.enabled;
|
|
13366
13580
|
this.initialized = true;
|
|
13367
13581
|
if (this.enabled) await this.start();
|
|
@@ -13382,29 +13596,67 @@ var FunnelController = class {
|
|
|
13382
13596
|
async setEnabled(enabled) {
|
|
13383
13597
|
if (!this.initialized || this.disposed) throw new Error("Funnel controller is unavailable");
|
|
13384
13598
|
await this.enqueue(async () => {
|
|
13385
|
-
|
|
13386
|
-
if (
|
|
13599
|
+
const ownsResources = this.child !== void 0 || this.gatewayValue !== void 0;
|
|
13600
|
+
if (this.enabled === enabled && (enabled ? this.child !== void 0 : !ownsResources && this.latest.state === "off")) return;
|
|
13601
|
+
if (!enabled) {
|
|
13602
|
+
try {
|
|
13603
|
+
await this.assertStateParents();
|
|
13604
|
+
} catch (error) {
|
|
13605
|
+
try {
|
|
13606
|
+
await this.stop();
|
|
13607
|
+
} catch (cleanupError) {
|
|
13608
|
+
this.publish({
|
|
13609
|
+
enabled: this.enabled,
|
|
13610
|
+
state: "error",
|
|
13611
|
+
errorCode: "funnel_cleanup_failed"
|
|
13612
|
+
});
|
|
13613
|
+
throw new AggregateError([error, cleanupError], "Funnel state validation and cleanup failed");
|
|
13614
|
+
}
|
|
13615
|
+
this.publish({
|
|
13616
|
+
enabled: this.enabled,
|
|
13617
|
+
state: "unavailable",
|
|
13618
|
+
errorCode: "funnel_state_invalid"
|
|
13619
|
+
});
|
|
13620
|
+
throw error;
|
|
13621
|
+
}
|
|
13622
|
+
await this.options.store.save({
|
|
13623
|
+
version: 1,
|
|
13624
|
+
enabled: false
|
|
13625
|
+
});
|
|
13626
|
+
this.enabled = false;
|
|
13627
|
+
try {
|
|
13628
|
+
await this.stop();
|
|
13629
|
+
} catch (error) {
|
|
13630
|
+
this.publish({
|
|
13631
|
+
enabled: false,
|
|
13632
|
+
state: "error",
|
|
13633
|
+
errorCode: "funnel_cleanup_failed"
|
|
13634
|
+
});
|
|
13635
|
+
throw error;
|
|
13636
|
+
}
|
|
13637
|
+
this.publish({
|
|
13638
|
+
enabled: false,
|
|
13639
|
+
state: "off"
|
|
13640
|
+
});
|
|
13641
|
+
return;
|
|
13642
|
+
}
|
|
13387
13643
|
try {
|
|
13388
13644
|
await this.assertStateParents();
|
|
13389
13645
|
} catch (error) {
|
|
13390
|
-
this.enabled = false;
|
|
13391
13646
|
this.publish({
|
|
13392
|
-
enabled:
|
|
13647
|
+
enabled: this.enabled,
|
|
13393
13648
|
state: "unavailable",
|
|
13394
13649
|
errorCode: "funnel_state_invalid"
|
|
13395
13650
|
});
|
|
13396
13651
|
throw error;
|
|
13397
13652
|
}
|
|
13398
|
-
this.enabled = enabled;
|
|
13399
13653
|
await this.options.store.save({
|
|
13400
13654
|
version: 1,
|
|
13401
|
-
enabled
|
|
13402
|
-
});
|
|
13403
|
-
if (enabled) await this.start();
|
|
13404
|
-
else this.publish({
|
|
13405
|
-
enabled: false,
|
|
13406
|
-
state: "off"
|
|
13655
|
+
enabled: true
|
|
13407
13656
|
});
|
|
13657
|
+
this.enabled = true;
|
|
13658
|
+
if (ownsResources) await this.stop();
|
|
13659
|
+
await this.start();
|
|
13408
13660
|
});
|
|
13409
13661
|
return this.status();
|
|
13410
13662
|
}
|
|
@@ -13412,7 +13664,6 @@ var FunnelController = class {
|
|
|
13412
13664
|
async reconnect() {
|
|
13413
13665
|
if (!this.initialized || this.disposed) throw new Error("Funnel controller is unavailable");
|
|
13414
13666
|
await this.enqueue(async () => {
|
|
13415
|
-
await this.stop();
|
|
13416
13667
|
try {
|
|
13417
13668
|
await this.assertStateParents();
|
|
13418
13669
|
} catch (error) {
|
|
@@ -13424,12 +13675,13 @@ var FunnelController = class {
|
|
|
13424
13675
|
throw error;
|
|
13425
13676
|
}
|
|
13426
13677
|
if (!this.enabled) {
|
|
13427
|
-
this.enabled = true;
|
|
13428
13678
|
await this.options.store.save({
|
|
13429
13679
|
version: 1,
|
|
13430
13680
|
enabled: true
|
|
13431
13681
|
});
|
|
13682
|
+
this.enabled = true;
|
|
13432
13683
|
}
|
|
13684
|
+
await this.stop();
|
|
13433
13685
|
await this.start();
|
|
13434
13686
|
});
|
|
13435
13687
|
return this.status();
|
|
@@ -13438,8 +13690,26 @@ var FunnelController = class {
|
|
|
13438
13690
|
async reset() {
|
|
13439
13691
|
if (!this.initialized || this.disposed) throw new Error("Funnel controller is unavailable");
|
|
13440
13692
|
await this.enqueue(async () => {
|
|
13441
|
-
|
|
13693
|
+
try {
|
|
13694
|
+
await assertManagedParents(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13695
|
+
const selected = await lstat(this.options.stateDirectory).catch((error) => {
|
|
13696
|
+
if (error.code !== "ENOENT") throw error;
|
|
13697
|
+
});
|
|
13698
|
+
if (selected !== void 0 && (selected.isSymbolicLink() || !selected.isDirectory())) await removeManagedTree(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13699
|
+
} catch (error) {
|
|
13700
|
+
this.publish({
|
|
13701
|
+
enabled: this.enabled,
|
|
13702
|
+
state: "off",
|
|
13703
|
+
errorCode: "funnel_state_invalid"
|
|
13704
|
+
});
|
|
13705
|
+
throw error;
|
|
13706
|
+
}
|
|
13707
|
+
await this.options.store.save({
|
|
13708
|
+
version: 1,
|
|
13709
|
+
enabled: false
|
|
13710
|
+
});
|
|
13442
13711
|
this.enabled = false;
|
|
13712
|
+
await this.stop();
|
|
13443
13713
|
try {
|
|
13444
13714
|
await removeManagedTree(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13445
13715
|
await this.assertStateParents();
|
|
@@ -13463,10 +13733,15 @@ var FunnelController = class {
|
|
|
13463
13733
|
return this.status();
|
|
13464
13734
|
}
|
|
13465
13735
|
/** Stop all remote resources without modifying the remembered switch. */
|
|
13466
|
-
|
|
13467
|
-
if (this.
|
|
13736
|
+
close() {
|
|
13737
|
+
if (this.closeTask !== void 0) return this.closeTask;
|
|
13468
13738
|
this.disposed = true;
|
|
13469
|
-
|
|
13739
|
+
const task = this.enqueue(() => this.stop());
|
|
13740
|
+
this.closeTask = task.catch((error) => {
|
|
13741
|
+
this.closeTask = void 0;
|
|
13742
|
+
throw error;
|
|
13743
|
+
});
|
|
13744
|
+
return this.closeTask;
|
|
13470
13745
|
}
|
|
13471
13746
|
enqueue(operation) {
|
|
13472
13747
|
const task = this.queue.then(operation, operation);
|
|
@@ -13656,13 +13931,17 @@ var FunnelController = class {
|
|
|
13656
13931
|
async stopProcessAndGateway() {
|
|
13657
13932
|
this.clearStartTimer();
|
|
13658
13933
|
const child = this.child;
|
|
13659
|
-
this.child = void 0;
|
|
13660
13934
|
const gateway = this.gatewayValue;
|
|
13661
|
-
this.gatewayValue = void 0;
|
|
13662
13935
|
await settleRemoteResources([async () => {
|
|
13663
13936
|
child?.stdin.end();
|
|
13664
13937
|
if (child !== void 0 && child.exitCode === null) await terminateRemoteProcess(child);
|
|
13665
|
-
|
|
13938
|
+
if (this.child === child) this.child = void 0;
|
|
13939
|
+
}, async () => {
|
|
13940
|
+
if (gateway !== void 0) {
|
|
13941
|
+
await gateway.close();
|
|
13942
|
+
if (this.gatewayValue === gateway) this.gatewayValue = void 0;
|
|
13943
|
+
}
|
|
13944
|
+
}], "Funnel resource cleanup failed");
|
|
13666
13945
|
}
|
|
13667
13946
|
clearStartTimer() {
|
|
13668
13947
|
if (this.startTimer === void 0) return;
|
|
@@ -13788,8 +14067,8 @@ var CpolarController = class {
|
|
|
13788
14067
|
if (options.region !== void 0 && !/^[a-z][a-z0-9_]{0,31}$/u.test(options.region)) throw new Error("cpolar region is invalid");
|
|
13789
14068
|
}
|
|
13790
14069
|
/** Restore the remembered cpolar switch independently from LAN and Funnel state. */
|
|
13791
|
-
async initialize() {
|
|
13792
|
-
const state = await this.options.store.load();
|
|
14070
|
+
async initialize(initialState) {
|
|
14071
|
+
const state = initialState ?? await this.options.store.load();
|
|
13793
14072
|
this.enabled = state.enabled;
|
|
13794
14073
|
this.initialized = true;
|
|
13795
14074
|
if (this.enabled) await this.start();
|
|
@@ -13811,12 +14090,12 @@ var CpolarController = class {
|
|
|
13811
14090
|
if (!this.initialized || this.disposed) throw new Error("cpolar controller is unavailable");
|
|
13812
14091
|
await this.enqueue(async () => {
|
|
13813
14092
|
if (this.enabled === enabled && (enabled === false || this.child !== void 0)) return;
|
|
13814
|
-
if (!enabled) await this.stop();
|
|
13815
|
-
this.enabled = enabled;
|
|
13816
14093
|
await this.options.store.save({
|
|
13817
14094
|
version: 1,
|
|
13818
14095
|
enabled
|
|
13819
14096
|
});
|
|
14097
|
+
this.enabled = enabled;
|
|
14098
|
+
if (!enabled) await this.stop();
|
|
13820
14099
|
if (enabled) await this.start();
|
|
13821
14100
|
else this.publish({
|
|
13822
14101
|
enabled: false,
|
|
@@ -13830,11 +14109,11 @@ var CpolarController = class {
|
|
|
13830
14109
|
if (!this.initialized || this.disposed) throw new Error("cpolar controller is unavailable");
|
|
13831
14110
|
await this.enqueue(async () => {
|
|
13832
14111
|
if (!this.enabled) {
|
|
13833
|
-
this.enabled = true;
|
|
13834
14112
|
await this.options.store.save({
|
|
13835
14113
|
version: 1,
|
|
13836
14114
|
enabled: true
|
|
13837
14115
|
});
|
|
14116
|
+
this.enabled = true;
|
|
13838
14117
|
}
|
|
13839
14118
|
await this.stop();
|
|
13840
14119
|
await this.start();
|
|
@@ -13845,12 +14124,12 @@ var CpolarController = class {
|
|
|
13845
14124
|
async reset() {
|
|
13846
14125
|
if (!this.initialized || this.disposed) throw new Error("cpolar controller is unavailable");
|
|
13847
14126
|
await this.enqueue(async () => {
|
|
13848
|
-
await this.stop();
|
|
13849
|
-
this.enabled = false;
|
|
13850
14127
|
await this.options.store.save({
|
|
13851
14128
|
version: 1,
|
|
13852
14129
|
enabled: false
|
|
13853
14130
|
});
|
|
14131
|
+
this.enabled = false;
|
|
14132
|
+
await this.stop();
|
|
13854
14133
|
this.publish({
|
|
13855
14134
|
enabled: false,
|
|
13856
14135
|
state: "off"
|
|
@@ -15292,12 +15571,12 @@ var ManagedOriginController = class {
|
|
|
15292
15571
|
gateway() {
|
|
15293
15572
|
return this.enabled ? this.controller().gateway() : void 0;
|
|
15294
15573
|
}
|
|
15295
|
-
initialize() {
|
|
15574
|
+
initialize(initialState) {
|
|
15296
15575
|
return this.enqueue(async () => {
|
|
15297
15576
|
if (this.initialized) return;
|
|
15298
15577
|
if (this.disposed) throw new Error("origin_controller_unavailable");
|
|
15299
15578
|
this.selected = (await this.options.modeStore.load()).mode;
|
|
15300
|
-
this.enabled = (await this.options.store.load()).enabled;
|
|
15579
|
+
this.enabled = (initialState ?? await this.options.store.load()).enabled;
|
|
15301
15580
|
await this.options.external.initialize();
|
|
15302
15581
|
await this.options.managed.initialize();
|
|
15303
15582
|
await this.options.external.setEnabled(false);
|
|
@@ -15336,11 +15615,11 @@ var ManagedOriginController = class {
|
|
|
15336
15615
|
if (!enabled) this.options.managed.cancelPendingStart?.();
|
|
15337
15616
|
return this.enqueue(async () => {
|
|
15338
15617
|
this.assertAvailable();
|
|
15339
|
-
if (!enabled) await this.stopBoth();
|
|
15340
15618
|
await this.options.store.save({
|
|
15341
15619
|
version: 1,
|
|
15342
15620
|
enabled
|
|
15343
15621
|
});
|
|
15622
|
+
if (!enabled) await this.stopBoth();
|
|
15344
15623
|
this.enabled = enabled;
|
|
15345
15624
|
if (enabled && !this.disposed) try {
|
|
15346
15625
|
await this.controller().setEnabled(true);
|
|
@@ -15354,11 +15633,11 @@ var ManagedOriginController = class {
|
|
|
15354
15633
|
this.options.managed.cancelPendingStart?.();
|
|
15355
15634
|
return this.enqueue(async () => {
|
|
15356
15635
|
this.assertAvailable();
|
|
15357
|
-
await this.stopBoth();
|
|
15358
15636
|
await this.options.store.save({
|
|
15359
15637
|
version: 1,
|
|
15360
15638
|
enabled: true
|
|
15361
15639
|
});
|
|
15640
|
+
await this.stopBoth();
|
|
15362
15641
|
this.enabled = true;
|
|
15363
15642
|
if (!this.disposed) try {
|
|
15364
15643
|
await this.controller().reconnect();
|
|
@@ -17229,8 +17508,9 @@ async function apply(ctx, config) {
|
|
|
17229
17508
|
frp: frpStore,
|
|
17230
17509
|
origin: originStore
|
|
17231
17510
|
};
|
|
17232
|
-
|
|
17233
|
-
for (const provider of REMOTE_PROVIDERS) await remoteControllers[provider].initialize();
|
|
17511
|
+
const prepared = await prepareDisabledRemoteProviders(remoteProviders.selected, stores);
|
|
17512
|
+
for (const provider of REMOTE_PROVIDERS) if (provider === remoteProviders.selected) await remoteControllers[provider].initialize();
|
|
17513
|
+
else await remoteControllers[provider].initialize(prepared.get(provider));
|
|
17234
17514
|
const lanDiscovery = lanGateway?.discoveryStatus();
|
|
17235
17515
|
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");
|
|
17236
17516
|
if (lanDiscovery?.mobileAssetsErrorCode !== void 0) logger.warn("a bundled mobile asset is missing (%s); the phone frontend will serve without it", lanDiscovery.mobileAssetsErrorCode);
|