dsh-mobile 0.5.4 → 0.6.0
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 +48 -1
- package/CONTRIBUTORS.md +25 -6
- package/README.en.md +121 -41
- package/README.md +121 -43
- package/SECURITY.md +16 -5
- package/THIRD_PARTY_NOTICES.md +9 -1
- package/docs/ATTACH_EXISTING_FRPS.en.md +9 -8
- package/docs/ATTACH_EXISTING_FRPS.md +16 -13
- package/docs/CADDY_MANAGED.en.md +39 -0
- package/docs/CADDY_MANAGED.md +39 -0
- package/docs/CLIENT_MODULES.en.md +33 -0
- package/docs/CLIENT_MODULES.md +33 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +17 -9
- package/docs/CLOUDFLARE_TUNNEL.md +17 -9
- package/docs/README.md +30 -18
- package/docs/SELF_HOSTED_FRP.en.md +6 -6
- package/docs/SELF_HOSTED_FRP.md +6 -6
- package/docs/SELF_HOSTED_ORIGIN.en.md +67 -17
- package/docs/SELF_HOSTED_ORIGIN.md +1 -1
- package/docs/SLOW_CONNECTIONS.en.md +16 -0
- package/docs/SLOW_CONNECTIONS.md +16 -0
- package/docs/TRUSTED_NETWORKS.en.md +26 -0
- package/docs/TRUSTED_NETWORKS.md +26 -0
- package/lib/cli.js +33 -5
- package/lib/cli.js.map +1 -1
- package/lib/client.js +1214 -27
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +133 -20
- package/lib/index.mjs +2248 -295
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +1 -1
- package/lib/mobile-compat.js.map +1 -1
- package/lib/mobile-layout.js +60 -11
- package/lib/mobile-layout.js.map +1 -1
- package/node_modules/dsh-mobile-question-fixes/package.json +1 -1
- package/package.json +17 -3
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -70,14 +70,14 @@ var BoundedRateLimiter = class {
|
|
|
70
70
|
function opaqueToken() {
|
|
71
71
|
return randomBytes(32).toString("base64url");
|
|
72
72
|
}
|
|
73
|
-
function digest(value) {
|
|
73
|
+
function digest$1(value) {
|
|
74
74
|
return createHash("sha256").update(value, "utf8").digest();
|
|
75
75
|
}
|
|
76
76
|
function digestHex(value) {
|
|
77
|
-
return digest(value).toString("hex");
|
|
77
|
+
return digest$1(value).toString("hex");
|
|
78
78
|
}
|
|
79
79
|
function matchesDigest(value, expected) {
|
|
80
|
-
return timingSafeEqual(digest(value), expected);
|
|
80
|
+
return timingSafeEqual(digest$1(value), expected);
|
|
81
81
|
}
|
|
82
82
|
function normalizeLabel(value) {
|
|
83
83
|
const label = (value ?? "Mobile device").normalize("NFC").trim();
|
|
@@ -175,7 +175,7 @@ var AccessController = class {
|
|
|
175
175
|
const record = Object.freeze({
|
|
176
176
|
key,
|
|
177
177
|
deviceId,
|
|
178
|
-
csrfDigest: digest(csrfToken),
|
|
178
|
+
csrfDigest: digest$1(csrfToken),
|
|
179
179
|
createdAt: now,
|
|
180
180
|
expiresAt: Math.min(now + this.options.sessionTtlMs, deviceExpiresAt)
|
|
181
181
|
});
|
|
@@ -196,7 +196,7 @@ var AccessController = class {
|
|
|
196
196
|
const token = opaqueToken();
|
|
197
197
|
const expiresAt = this.now() + ttl;
|
|
198
198
|
this.pairingWindow = Object.freeze({
|
|
199
|
-
digest: digest(token),
|
|
199
|
+
digest: digest$1(token),
|
|
200
200
|
expiresAt
|
|
201
201
|
});
|
|
202
202
|
return Object.freeze({
|
|
@@ -289,7 +289,7 @@ var AccessController = class {
|
|
|
289
289
|
}
|
|
290
290
|
requireDevice(deviceToken, now) {
|
|
291
291
|
if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
|
|
292
|
-
const tokenDigest = digest(deviceToken);
|
|
292
|
+
const tokenDigest = digest$1(deviceToken);
|
|
293
293
|
const device = this.devices.find((candidate) => timingSafeEqual(Buffer.from(candidate.tokenDigest, "hex"), tokenDigest));
|
|
294
294
|
if (device === void 0) throw new AccessError(401, "authentication_failed");
|
|
295
295
|
if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
|
|
@@ -904,6 +904,7 @@ const Config = z.object({
|
|
|
904
904
|
maxWebSockets: z.natural(),
|
|
905
905
|
maxBodyBytes: z.natural(),
|
|
906
906
|
upstreamTimeoutMs: z.natural(),
|
|
907
|
+
upstreamApiTimeoutMs: z.natural(),
|
|
907
908
|
rateLimitWindowMs: z.natural(),
|
|
908
909
|
maxPairingAttempts: z.natural(),
|
|
909
910
|
maxRateLimitKeys: z.natural()
|
|
@@ -1052,6 +1053,7 @@ function parseGatewayConfig(raw) {
|
|
|
1052
1053
|
maxWebSockets: integer(value.maxWebSockets, "maxWebSockets", 16, 1, 256),
|
|
1053
1054
|
maxBodyBytes: integer(value.maxBodyBytes, "maxBodyBytes", 167772160, 1024, 268435456),
|
|
1054
1055
|
upstreamTimeoutMs: integer(value.upstreamTimeoutMs, "upstreamTimeoutMs", 3e4, 1e3, 3e5),
|
|
1056
|
+
upstreamApiTimeoutMs: integer(value.upstreamApiTimeoutMs, "upstreamApiTimeoutMs", 0, 0, 2147483647),
|
|
1055
1057
|
rateLimitWindowMs: integer(value.rateLimitWindowMs, "rateLimitWindowMs", 6e4, 1e3, 36e5),
|
|
1056
1058
|
maxPairingAttempts: integer(value.maxPairingAttempts, "maxPairingAttempts", 8, 1, 100),
|
|
1057
1059
|
maxRateLimitKeys: integer(value.maxRateLimitKeys, "maxRateLimitKeys", 256, 1, 4096)
|
|
@@ -1133,6 +1135,180 @@ async function restrictPrivateFile(file, mode = 384) {
|
|
|
1133
1135
|
});
|
|
1134
1136
|
}
|
|
1135
1137
|
//#endregion
|
|
1138
|
+
//#region src/client-module-preferences.ts
|
|
1139
|
+
const MAX_RECORD_BYTES = 1048576;
|
|
1140
|
+
/** Validate untrusted JSON selections; graph membership is checked by the Gateway. */
|
|
1141
|
+
function parseExcludedClientModules(value) {
|
|
1142
|
+
if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || id.length === 0 || id.trim() !== id || /[\u0000-\u001f\u007f]/u.test(id))) throw new Error("excluded_client_modules_invalid");
|
|
1143
|
+
const ids = value;
|
|
1144
|
+
if (new Set(ids).size !== ids.length) throw new Error("excluded_client_modules_invalid");
|
|
1145
|
+
return Object.freeze([...ids]);
|
|
1146
|
+
}
|
|
1147
|
+
function deviceKey(deviceId) {
|
|
1148
|
+
if (!/^[a-f\d]{32}$/u.test(deviceId)) throw new Error("client_module_device_invalid");
|
|
1149
|
+
}
|
|
1150
|
+
function parseRecord(value) {
|
|
1151
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("client_module_preferences_invalid");
|
|
1152
|
+
const record = value;
|
|
1153
|
+
if (record.version !== 1 || record.devices === null || typeof record.devices !== "object" || Array.isArray(record.devices) || Object.keys(record).some((key) => ![
|
|
1154
|
+
"version",
|
|
1155
|
+
"computer",
|
|
1156
|
+
"devices"
|
|
1157
|
+
].includes(key))) throw new Error("client_module_preferences_invalid");
|
|
1158
|
+
const devices = {};
|
|
1159
|
+
for (const [id, excluded] of Object.entries(record.devices)) {
|
|
1160
|
+
deviceKey(id);
|
|
1161
|
+
devices[id] = parseExcludedClientModules(excluded);
|
|
1162
|
+
}
|
|
1163
|
+
return Object.freeze({
|
|
1164
|
+
version: 1,
|
|
1165
|
+
devices: Object.freeze(devices),
|
|
1166
|
+
...record.computer === void 0 ? {} : { computer: parseExcludedClientModules(record.computer) }
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
/** Atomic, serialized preferences shared by the LAN and remote Gateway instances. */
|
|
1170
|
+
var ClientModulePreferenceStore = class {
|
|
1171
|
+
file;
|
|
1172
|
+
defaults;
|
|
1173
|
+
value = Object.freeze({
|
|
1174
|
+
version: 1,
|
|
1175
|
+
devices: Object.freeze({})
|
|
1176
|
+
});
|
|
1177
|
+
initialization;
|
|
1178
|
+
queue = Promise.resolve();
|
|
1179
|
+
constructor(file, defaults) {
|
|
1180
|
+
this.file = file;
|
|
1181
|
+
if (!isAbsolute(file)) throw new Error("client module preference path must be absolute");
|
|
1182
|
+
this.defaults = parseExcludedClientModules(defaults);
|
|
1183
|
+
}
|
|
1184
|
+
/** Read existing state once. Missing state is not written during initialization. */
|
|
1185
|
+
initialize() {
|
|
1186
|
+
this.initialization ??= this.load();
|
|
1187
|
+
return this.initialization;
|
|
1188
|
+
}
|
|
1189
|
+
async load() {
|
|
1190
|
+
let information;
|
|
1191
|
+
try {
|
|
1192
|
+
information = await lstat(this.file);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (error.code === "ENOENT") return;
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1197
|
+
if (!information.isFile() || information.isSymbolicLink() || information.size > MAX_RECORD_BYTES) throw new Error("client module preferences must be a regular file no larger than 1 MiB");
|
|
1198
|
+
this.value = parseRecord(JSON.parse(await readFile(this.file, "utf8")));
|
|
1199
|
+
}
|
|
1200
|
+
selection(deviceId) {
|
|
1201
|
+
if (deviceId !== void 0) deviceKey(deviceId);
|
|
1202
|
+
const computer = this.value.computer ?? this.defaults;
|
|
1203
|
+
const device = deviceId === void 0 ? void 0 : this.value.devices[deviceId];
|
|
1204
|
+
return Object.freeze({
|
|
1205
|
+
excludedClientModules: device ?? computer,
|
|
1206
|
+
defaultExcludedClientModules: computer,
|
|
1207
|
+
source: device !== void 0 ? "device" : this.value.computer !== void 0 ? "computer" : "plugin"
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
exclusive(action) {
|
|
1211
|
+
const operation = this.queue.then(action);
|
|
1212
|
+
this.queue = operation.then(() => void 0, () => void 0);
|
|
1213
|
+
return operation;
|
|
1214
|
+
}
|
|
1215
|
+
/** Resolve one device override, otherwise the computer or plugin default. */
|
|
1216
|
+
async read(deviceId) {
|
|
1217
|
+
await this.initialize();
|
|
1218
|
+
return this.exclusive(async () => this.selection(deviceId));
|
|
1219
|
+
}
|
|
1220
|
+
/** Revalidate against the current boot manifest inside the serialized mutation. */
|
|
1221
|
+
async configure(excluded, deviceId, validate) {
|
|
1222
|
+
const ids = parseExcludedClientModules(excluded);
|
|
1223
|
+
if (deviceId !== void 0) deviceKey(deviceId);
|
|
1224
|
+
await this.initialize();
|
|
1225
|
+
return this.exclusive(async () => {
|
|
1226
|
+
await validate(ids);
|
|
1227
|
+
const next = deviceId === void 0 ? {
|
|
1228
|
+
...this.value,
|
|
1229
|
+
computer: ids
|
|
1230
|
+
} : {
|
|
1231
|
+
...this.value,
|
|
1232
|
+
devices: {
|
|
1233
|
+
...this.value.devices,
|
|
1234
|
+
[deviceId]: ids
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
await this.save(next);
|
|
1238
|
+
return this.selection(deviceId);
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
/** Remove only the selected override; retain all other device selections. */
|
|
1242
|
+
async reset(deviceId, validate) {
|
|
1243
|
+
if (deviceId !== void 0) deviceKey(deviceId);
|
|
1244
|
+
await this.initialize();
|
|
1245
|
+
return this.exclusive(async () => {
|
|
1246
|
+
const devices = { ...this.value.devices };
|
|
1247
|
+
if (deviceId !== void 0) delete devices[deviceId];
|
|
1248
|
+
const next = {
|
|
1249
|
+
version: 1,
|
|
1250
|
+
devices,
|
|
1251
|
+
...deviceId === void 0 || this.value.computer === void 0 ? {} : { computer: this.value.computer }
|
|
1252
|
+
};
|
|
1253
|
+
await validate(deviceId === void 0 ? this.defaults : this.value.computer ?? this.defaults);
|
|
1254
|
+
await this.save(next);
|
|
1255
|
+
return this.selection(deviceId);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
/** Remove exactly the revoked device's record without evaluating its obsolete graph. */
|
|
1259
|
+
removeDevice(deviceId) {
|
|
1260
|
+
return this.removeDevices([deviceId]);
|
|
1261
|
+
}
|
|
1262
|
+
/** Remove an explicit set of revoked identities, not every record in the shared store. */
|
|
1263
|
+
async removeDevices(deviceIds) {
|
|
1264
|
+
for (const deviceId of deviceIds) deviceKey(deviceId);
|
|
1265
|
+
await this.initialize();
|
|
1266
|
+
await this.exclusive(async () => {
|
|
1267
|
+
if (!deviceIds.some((id) => this.value.devices[id] !== void 0)) return;
|
|
1268
|
+
const devices = { ...this.value.devices };
|
|
1269
|
+
for (const deviceId of deviceIds) delete devices[deviceId];
|
|
1270
|
+
await this.save({
|
|
1271
|
+
...this.value,
|
|
1272
|
+
devices
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
async save(record) {
|
|
1277
|
+
const valid = parseRecord(record);
|
|
1278
|
+
const body = `${JSON.stringify(valid)}\n`;
|
|
1279
|
+
if (Buffer.byteLength(body) > MAX_RECORD_BYTES) throw new Error("client_module_preferences_limit");
|
|
1280
|
+
const directory = dirname(this.file);
|
|
1281
|
+
if (await mkdir(directory, {
|
|
1282
|
+
recursive: true,
|
|
1283
|
+
mode: 448
|
|
1284
|
+
}) !== void 0) await restrictPrivateFile(directory, 448);
|
|
1285
|
+
try {
|
|
1286
|
+
const existing = await lstat(this.file);
|
|
1287
|
+
if (!existing.isFile() || existing.isSymbolicLink()) throw new Error("client module preference target must be a regular file");
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (error.code !== "ENOENT") throw error;
|
|
1290
|
+
}
|
|
1291
|
+
const temporary = join(directory, `.${basename(this.file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
1292
|
+
try {
|
|
1293
|
+
await writeFile(temporary, body, {
|
|
1294
|
+
encoding: "utf8",
|
|
1295
|
+
flag: "wx",
|
|
1296
|
+
mode: 384
|
|
1297
|
+
});
|
|
1298
|
+
await restrictPrivateFile(temporary);
|
|
1299
|
+
await rename(temporary, this.file);
|
|
1300
|
+
this.value = valid;
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
try {
|
|
1303
|
+
await rm(temporary, { force: true });
|
|
1304
|
+
} catch (cleanupError) {
|
|
1305
|
+
throw new AggregateError([error, cleanupError], "client module preference write and temporary cleanup failed");
|
|
1306
|
+
}
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
//#endregion
|
|
1136
1312
|
//#region src/control.ts
|
|
1137
1313
|
/** Keeps one runtime aligned with a changing network selection. */
|
|
1138
1314
|
var FollowingMobileAccessRuntime = class {
|
|
@@ -1380,6 +1556,88 @@ var MobileAccessGatewayController = class {
|
|
|
1380
1556
|
return run;
|
|
1381
1557
|
}
|
|
1382
1558
|
};
|
|
1559
|
+
//#endregion
|
|
1560
|
+
//#region src/client-module-recovery.ts
|
|
1561
|
+
const CLIENT_MODULE_RECOVERY_COPY = {
|
|
1562
|
+
zh: {
|
|
1563
|
+
title: "移动页面模块需要调整",
|
|
1564
|
+
explanation: "电脑上的插件或依赖关系已变化,之前保存的模块选择无法加载当前页面。",
|
|
1565
|
+
scope: "可为本设备加载全部模块并重新打开 DSH。不会修改电脑默认设置、其他设备或已安装的插件。",
|
|
1566
|
+
action: "为本设备加载全部模块并重试",
|
|
1567
|
+
working: "正在更新本设备的模块选择…",
|
|
1568
|
+
failed: "未能恢复。请确认电脑和远程通道仍在线,然后重试。"
|
|
1569
|
+
},
|
|
1570
|
+
en: {
|
|
1571
|
+
title: "Mobile page modules need attention",
|
|
1572
|
+
explanation: "The computer’s plugins or dependencies have changed. The saved module selection can no longer load this page.",
|
|
1573
|
+
scope: "Load all modules for this device and reopen DSH. Computer defaults, other devices, and installed plugins are unchanged.",
|
|
1574
|
+
action: "Load all modules for this device and retry",
|
|
1575
|
+
working: "Updating this device’s module selection…",
|
|
1576
|
+
failed: "Could not restore the page. Check that the computer and remote connection are online, then retry."
|
|
1577
|
+
},
|
|
1578
|
+
it: {
|
|
1579
|
+
title: "I moduli della pagina mobile richiedono attenzione",
|
|
1580
|
+
explanation: "I plugin o le dipendenze del computer sono cambiati. La selezione salvata non può più caricare questa pagina.",
|
|
1581
|
+
scope: "Carica tutti i moduli per questo dispositivo e riapri DSH. I predefiniti del computer, gli altri dispositivi e i plugin installati non cambiano.",
|
|
1582
|
+
action: "Carica tutti i moduli per questo dispositivo e riprova",
|
|
1583
|
+
working: "Aggiornamento dei moduli per questo dispositivo…",
|
|
1584
|
+
failed: "Ripristino non riuscito. Verifica che il computer e la connessione remota siano online, poi riprova."
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
/** Render an explicit device-only recovery action without interpolating graph diagnostics or credentials.
|
|
1588
|
+
* @param locale - Negotiated browser display language.
|
|
1589
|
+
* @returns An owned HTML page which changes no selection until the user activates its button.
|
|
1590
|
+
*/
|
|
1591
|
+
function renderClientModuleRecoveryPage(locale) {
|
|
1592
|
+
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
1593
|
+
return `<!doctype html>
|
|
1594
|
+
<html lang="${locale === "zh" ? "zh-CN" : locale}">
|
|
1595
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
1596
|
+
<meta name="color-scheme" content="light dark"><title>${copy.title}</title>
|
|
1597
|
+
<style>
|
|
1598
|
+
body{margin:0;background:Canvas;color:CanvasText;font:16px/1.5 system-ui,sans-serif}
|
|
1599
|
+
main{box-sizing:border-box;max-width:640px;margin:32px auto;padding:24px max(24px,env(safe-area-inset-left)) 24px max(24px,env(safe-area-inset-right));overflow-wrap:anywhere}
|
|
1600
|
+
h1{font-size:24px;line-height:1.3}button{min-height:48px;max-width:100%;padding:12px 20px;border:1px solid ButtonText;border-radius:12px;background:ButtonFace;color:ButtonText;font:inherit;cursor:pointer}
|
|
1601
|
+
button:focus-visible{outline:2px solid Highlight;outline-offset:3px}button:disabled{opacity:.6;cursor:wait}#recovery-status{min-height:1.5em}
|
|
1602
|
+
</style></head>
|
|
1603
|
+
<body><main><h1>${copy.title}</h1><p>${copy.explanation}</p><p>${copy.scope}</p>
|
|
1604
|
+
<button id="load-all-modules" type="button">${copy.action}</button><p id="recovery-status" role="status" aria-live="polite"></p>
|
|
1605
|
+
</main><script>${renderClientModuleRecoveryScript(locale)}<\/script></body></html>
|
|
1606
|
+
`;
|
|
1607
|
+
}
|
|
1608
|
+
/** Render the fixed same-origin action; CSRF values are read from the active browser cookie only at user activation.
|
|
1609
|
+
* @param locale - Negotiated browser display language.
|
|
1610
|
+
* @returns JavaScript that preserves selections on failure and reloads only after an authorized save succeeds.
|
|
1611
|
+
*/
|
|
1612
|
+
function renderClientModuleRecoveryScript(locale) {
|
|
1613
|
+
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
1614
|
+
return `(() => {
|
|
1615
|
+
const button = document.getElementById('load-all-modules')
|
|
1616
|
+
const status = document.getElementById('recovery-status')
|
|
1617
|
+
button.addEventListener('click', async () => {
|
|
1618
|
+
if (button.disabled) return
|
|
1619
|
+
button.disabled = true
|
|
1620
|
+
status.textContent = ${JSON.stringify(copy.working)}
|
|
1621
|
+
try {
|
|
1622
|
+
const values = document.cookie.split(';').map(value => value.trim())
|
|
1623
|
+
const host = values.filter(value => value.startsWith('__Host-dsh_ma_csrf='))
|
|
1624
|
+
const candidates = host.length === 0 ? values.filter(value => value.startsWith('dsh_ma_csrf=')) : host
|
|
1625
|
+
if (candidates.length !== 1) throw new Error('csrf_unavailable')
|
|
1626
|
+
const csrf = candidates[0].slice(candidates[0].indexOf('=') + 1)
|
|
1627
|
+
const response = await fetch('/mobile-access/client-modules', {
|
|
1628
|
+
method: 'POST', credentials: 'same-origin', redirect: 'error', cache: 'no-store',
|
|
1629
|
+
headers: { 'content-type': 'application/json', 'x-dsh-mobile-csrf': csrf },
|
|
1630
|
+
body: JSON.stringify({ excludedClientModules: [] }),
|
|
1631
|
+
})
|
|
1632
|
+
if (!response.ok) throw new Error('recovery_failed')
|
|
1633
|
+
location.reload()
|
|
1634
|
+
} catch {
|
|
1635
|
+
status.textContent = ${JSON.stringify(copy.failed)}
|
|
1636
|
+
button.disabled = false
|
|
1637
|
+
}
|
|
1638
|
+
})
|
|
1639
|
+
})()`;
|
|
1640
|
+
}
|
|
1383
1641
|
function unwrapBrackets(hostname) {
|
|
1384
1642
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1385
1643
|
}
|
|
@@ -1621,10 +1879,8 @@ function commentRanges(html) {
|
|
|
1621
1879
|
/**
|
|
1622
1880
|
* Load the compatibility bundle synchronously, before the first DSH script.
|
|
1623
1881
|
*
|
|
1624
|
-
* Returns the HTML unchanged when there is no script to anchor to.
|
|
1625
|
-
*
|
|
1626
|
-
* cannot place it in must still be served: throwing here took the entire dedicated
|
|
1627
|
-
* mobile frontend down with a 502.
|
|
1882
|
+
* Returns the HTML unchanged when there is no executable script to anchor to.
|
|
1883
|
+
* The bundle supplies missing browser APIs before the page's scripts execute.
|
|
1628
1884
|
*/
|
|
1629
1885
|
function ensureMobileCompatibility(html) {
|
|
1630
1886
|
const comments = commentRanges(html);
|
|
@@ -2233,6 +2489,12 @@ function normalizeRoutePath(value) {
|
|
|
2233
2489
|
if (normalizedInput.split("/").some((part) => part === ".." || part === ".")) throw new MobileExtensionError("invalid_route", "extension route path is invalid");
|
|
2234
2490
|
return normalizedInput === "/" ? "/" : normalizedInput.replace(/\/+$/u, "");
|
|
2235
2491
|
}
|
|
2492
|
+
/** Validate and snapshot an optional per-operation timeout override. */
|
|
2493
|
+
function operationTimeout(value, code) {
|
|
2494
|
+
if (value === void 0) return void 0;
|
|
2495
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 3e5) throw new MobileExtensionError(code, "operation timeoutMs is invalid");
|
|
2496
|
+
return value;
|
|
2497
|
+
}
|
|
2236
2498
|
function validateDefinition(definition) {
|
|
2237
2499
|
const manifest = parseExtensionManifest({
|
|
2238
2500
|
schemaVersion: definition.schemaVersion,
|
|
@@ -2242,9 +2504,17 @@ function validateDefinition(definition) {
|
|
|
2242
2504
|
...definition.description === void 0 ? {} : { description: definition.description }
|
|
2243
2505
|
});
|
|
2244
2506
|
const actionNames = /* @__PURE__ */ new Set();
|
|
2507
|
+
const actions = {};
|
|
2245
2508
|
for (const [name, action] of Object.entries(definition.actions ?? {})) {
|
|
2246
2509
|
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(name) || action === null || typeof action !== "object" || typeof action.run !== "function" || actionNames.has(name)) throw new MobileExtensionError("invalid_action", `invalid action ${name}`);
|
|
2247
2510
|
actionNames.add(name);
|
|
2511
|
+
const actionTimeout = operationTimeout(action.timeoutMs, "invalid_action");
|
|
2512
|
+
const actionInput = action.input;
|
|
2513
|
+
actions[name] = Object.freeze({
|
|
2514
|
+
...actionInput === void 0 ? {} : { input: actionInput },
|
|
2515
|
+
run: action.run.bind(action),
|
|
2516
|
+
...actionTimeout === void 0 ? {} : { timeoutMs: actionTimeout }
|
|
2517
|
+
});
|
|
2248
2518
|
}
|
|
2249
2519
|
const routeNames = /* @__PURE__ */ new Set();
|
|
2250
2520
|
const routes = (definition.routes ?? []).map((route) => {
|
|
@@ -2258,11 +2528,14 @@ function validateDefinition(definition) {
|
|
|
2258
2528
|
"PATCH",
|
|
2259
2529
|
"DELETE"
|
|
2260
2530
|
].includes(method)) throw new MobileExtensionError("invalid_route", "unsupported extension route method");
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2531
|
+
const routeTimeout = operationTimeout(route.timeoutMs, "invalid_route");
|
|
2532
|
+
const normalized = Object.freeze({
|
|
2263
2533
|
method,
|
|
2264
|
-
path: normalizeRoutePath(route.path)
|
|
2265
|
-
|
|
2534
|
+
path: normalizeRoutePath(route.path),
|
|
2535
|
+
...route.kind === void 0 ? {} : { kind: route.kind },
|
|
2536
|
+
handle: route.handle.bind(route),
|
|
2537
|
+
...routeTimeout === void 0 ? {} : { timeoutMs: routeTimeout }
|
|
2538
|
+
});
|
|
2266
2539
|
const key = routeKey(normalized);
|
|
2267
2540
|
if (routeNames.has(key)) throw new MobileExtensionError("duplicate_route", `duplicate route ${key}`);
|
|
2268
2541
|
routeNames.add(key);
|
|
@@ -2270,7 +2543,7 @@ function validateDefinition(definition) {
|
|
|
2270
2543
|
});
|
|
2271
2544
|
return Object.freeze({
|
|
2272
2545
|
...manifest,
|
|
2273
|
-
...definition.actions === void 0 ? {} : { actions: Object.freeze(
|
|
2546
|
+
...definition.actions === void 0 ? {} : { actions: Object.freeze(actions) },
|
|
2274
2547
|
...routes.length === 0 ? {} : { routes: Object.freeze(routes) }
|
|
2275
2548
|
});
|
|
2276
2549
|
}
|
|
@@ -2315,6 +2588,8 @@ var MobileAccessService = class extends Service {
|
|
|
2315
2588
|
local = /* @__PURE__ */ new Map();
|
|
2316
2589
|
retired = /* @__PURE__ */ new Map();
|
|
2317
2590
|
failures = /* @__PURE__ */ new Map();
|
|
2591
|
+
/** Extensions with caller-detached, still-unresolved operations. */
|
|
2592
|
+
abandoned = /* @__PURE__ */ new Map();
|
|
2318
2593
|
contentListeners = /* @__PURE__ */ new Set();
|
|
2319
2594
|
contentHash = createHash("sha256").update("").digest("hex");
|
|
2320
2595
|
localRoot;
|
|
@@ -2434,30 +2709,65 @@ var MobileAccessService = class extends Service {
|
|
|
2434
2709
|
name: asset.name
|
|
2435
2710
|
};
|
|
2436
2711
|
}
|
|
2712
|
+
/** Reject new dispatches to an extension whose abandoned work is unresolved. */
|
|
2713
|
+
requireAdmission(id) {
|
|
2714
|
+
if ((this.abandoned.get(id) ?? 0) > 0) throw new MobileExtensionError("extension_busy", `extension ${id} has unresolved abandoned work`, 503);
|
|
2715
|
+
}
|
|
2716
|
+
/** Account for one operation until its underlying work actually settles. */
|
|
2717
|
+
trackOperation(id, operation) {
|
|
2718
|
+
operation.settled.then(() => {
|
|
2719
|
+
if (operation.detached()) {
|
|
2720
|
+
const remaining = (this.abandoned.get(id) ?? 1) - 1;
|
|
2721
|
+
if (remaining <= 0) this.abandoned.delete(id);
|
|
2722
|
+
else this.abandoned.set(id, remaining);
|
|
2723
|
+
}
|
|
2724
|
+
}).catch(() => void 0);
|
|
2725
|
+
}
|
|
2437
2726
|
/** Invoke one action after parsing its input and binding the request lifetime. */
|
|
2438
2727
|
async invoke(id, actionName, input, context, generation) {
|
|
2439
2728
|
const extension = this.extension(id, generation);
|
|
2440
2729
|
if (extension === void 0) throw new MobileExtensionError("extension_not_found", "extension not found", 404);
|
|
2441
2730
|
const action = ("host" in extension ? extension.host : extension).actions?.[actionName];
|
|
2442
2731
|
if (action === void 0) throw new MobileExtensionError("action_not_found", "action not found", 404);
|
|
2443
|
-
|
|
2732
|
+
if ("host" in extension) extension.controller.signal.throwIfAborted();
|
|
2733
|
+
context.signal.throwIfAborted();
|
|
2734
|
+
this.requireAdmission(id);
|
|
2735
|
+
const external = "host" in extension ? combineSignalLifetime(extension.controller.signal, context.signal) : void 0;
|
|
2736
|
+
let operation;
|
|
2444
2737
|
try {
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2738
|
+
operation = startBoundedOperation((signal) => {
|
|
2739
|
+
let parsed;
|
|
2740
|
+
try {
|
|
2741
|
+
parsed = parseActionInput(action.input, input);
|
|
2742
|
+
} catch {
|
|
2743
|
+
throw new MobileExtensionError("invalid_action_input", "action input is invalid", 400);
|
|
2744
|
+
}
|
|
2745
|
+
return Promise.resolve(parsed).then((value) => {
|
|
2746
|
+
signal.throwIfAborted();
|
|
2747
|
+
return action.run({
|
|
2748
|
+
...context,
|
|
2749
|
+
signal
|
|
2750
|
+
}, value);
|
|
2751
|
+
}, () => {
|
|
2752
|
+
throw new MobileExtensionError("invalid_action_input", "action input is invalid", 400);
|
|
2753
|
+
});
|
|
2754
|
+
}, {
|
|
2755
|
+
external: external?.signal ?? context.signal,
|
|
2756
|
+
timeoutMs: action.timeoutMs ?? 3e4,
|
|
2757
|
+
timeoutError: () => new MobileExtensionError("extension_action_timeout", `extension ${id} action ${actionName} timed out`, 500),
|
|
2758
|
+
onDetach: () => {
|
|
2759
|
+
this.abandoned.set(id, (this.abandoned.get(id) ?? 0) + 1);
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
this.trackOperation(id, operation);
|
|
2763
|
+
return await operation.caller;
|
|
2456
2764
|
} catch (error) {
|
|
2765
|
+
if (error instanceof OperationCancelled) throw error.reason;
|
|
2457
2766
|
if (error instanceof MobileExtensionError) throw error;
|
|
2458
2767
|
throw new MobileExtensionError("extension_failed", "extension action failed", 500);
|
|
2459
2768
|
} finally {
|
|
2460
|
-
|
|
2769
|
+
operation?.release();
|
|
2770
|
+
external?.cleanup();
|
|
2461
2771
|
}
|
|
2462
2772
|
}
|
|
2463
2773
|
/** Match one route and invoke it with a generation-bound abort signal. */
|
|
@@ -2469,25 +2779,47 @@ var MobileAccessService = class extends Service {
|
|
|
2469
2779
|
return (candidate.kind ?? "exact") === "exact" ? candidate.path === pathname : pathname === candidate.path || pathname.startsWith(`${candidate.path}/`);
|
|
2470
2780
|
});
|
|
2471
2781
|
if (route === void 0) throw new MobileExtensionError("route_not_found", "route not found", 404);
|
|
2472
|
-
|
|
2473
|
-
|
|
2782
|
+
if ("host" in extension) extension.controller.signal.throwIfAborted();
|
|
2783
|
+
request.signal.throwIfAborted();
|
|
2784
|
+
this.requireAdmission(id);
|
|
2785
|
+
const external = "host" in extension ? combineSignalLifetime(extension.controller.signal, request.signal) : void 0;
|
|
2786
|
+
let operation;
|
|
2787
|
+
let holdForStream = false;
|
|
2474
2788
|
try {
|
|
2475
|
-
|
|
2789
|
+
operation = startBoundedOperation((signal) => route.handle({
|
|
2476
2790
|
...request,
|
|
2477
|
-
signal
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2791
|
+
signal
|
|
2792
|
+
}), {
|
|
2793
|
+
external: external?.signal ?? request.signal,
|
|
2794
|
+
timeoutMs: route.timeoutMs ?? 3e4,
|
|
2795
|
+
timeoutError: () => new MobileExtensionError("extension_route_timeout", `extension ${id} route ${method} ${pathname} timed out`, 500),
|
|
2796
|
+
onDetach: () => {
|
|
2797
|
+
this.abandoned.set(id, (this.abandoned.get(id) ?? 0) + 1);
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
this.trackOperation(id, operation);
|
|
2801
|
+
operation.settled.then(() => {
|
|
2802
|
+
disposeLateExtensionResponse(operation?.lateValue());
|
|
2803
|
+
}).catch(() => void 0);
|
|
2804
|
+
const result = await operation.caller;
|
|
2480
2805
|
if (result === null || typeof result !== "object" || typeof result.body !== "string" && !(result.body instanceof Uint8Array) && !isReadable(result.body)) throw new MobileExtensionError("invalid_route_response", "extension returned an invalid response", 500);
|
|
2481
|
-
if (
|
|
2482
|
-
|
|
2483
|
-
releaseSignalLifetimeWhenStreamSettles(result.body,
|
|
2806
|
+
if (isReadable(result.body)) {
|
|
2807
|
+
holdForStream = true;
|
|
2808
|
+
releaseSignalLifetimeWhenStreamSettles(result.body, () => {
|
|
2809
|
+
operation?.release();
|
|
2810
|
+
external?.cleanup();
|
|
2811
|
+
});
|
|
2484
2812
|
}
|
|
2485
2813
|
return result;
|
|
2486
2814
|
} catch (error) {
|
|
2815
|
+
if (error instanceof OperationCancelled) throw error.reason;
|
|
2487
2816
|
if (error instanceof MobileExtensionError) throw error;
|
|
2488
2817
|
throw new MobileExtensionError("extension_failed", "extension route failed", 500);
|
|
2489
2818
|
} finally {
|
|
2490
|
-
if (
|
|
2819
|
+
if (!holdForStream) {
|
|
2820
|
+
operation?.release();
|
|
2821
|
+
external?.cleanup();
|
|
2822
|
+
}
|
|
2491
2823
|
}
|
|
2492
2824
|
}
|
|
2493
2825
|
/** Start the local directory watcher; an absent directory is intentionally inert. */
|
|
@@ -2658,6 +2990,16 @@ var MobileAccessService = class extends Service {
|
|
|
2658
2990
|
function isReadable(value) {
|
|
2659
2991
|
return value !== null && typeof value === "object" && typeof value.pipe === "function";
|
|
2660
2992
|
}
|
|
2993
|
+
/** Destroy a route response that arrived after its caller detached; must never throw or emit unhandled stream errors. */
|
|
2994
|
+
function disposeLateExtensionResponse(value) {
|
|
2995
|
+
try {
|
|
2996
|
+
if (value === null || typeof value !== "object") return;
|
|
2997
|
+
const body = value.body;
|
|
2998
|
+
if (!isReadable(body) || typeof body.once !== "function" || typeof body.destroy !== "function") return;
|
|
2999
|
+
body.once("error", () => void 0);
|
|
3000
|
+
body.destroy();
|
|
3001
|
+
} catch {}
|
|
3002
|
+
}
|
|
2661
3003
|
function releaseSignalLifetimeWhenStreamSettles(stream, cleanup) {
|
|
2662
3004
|
let stopObserving;
|
|
2663
3005
|
stopObserving = finished(stream, () => {
|
|
@@ -2680,6 +3022,77 @@ async function settleBounded(pending, timeoutMs) {
|
|
|
2680
3022
|
})]);
|
|
2681
3023
|
if (timer !== void 0) clearTimeout(timer);
|
|
2682
3024
|
}
|
|
3025
|
+
/** Sentinel distinguishing caller detachment (deadline or external cancel) from a host error. */
|
|
3026
|
+
var OperationCancelled = class {
|
|
3027
|
+
reason;
|
|
3028
|
+
constructor(reason) {
|
|
3029
|
+
this.reason = reason;
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
function startBoundedOperation(start, options) {
|
|
3033
|
+
const deadline = new AbortController();
|
|
3034
|
+
const combined = options.external === void 0 ? {
|
|
3035
|
+
signal: deadline.signal,
|
|
3036
|
+
cleanup: () => void 0
|
|
3037
|
+
} : combineSignalLifetime(options.external, deadline.signal);
|
|
3038
|
+
let detached = false;
|
|
3039
|
+
let workState;
|
|
3040
|
+
let lateFulfilled;
|
|
3041
|
+
let notifySettled;
|
|
3042
|
+
let callerTimer;
|
|
3043
|
+
let onExternal;
|
|
3044
|
+
const settled = new Promise((resolve) => {
|
|
3045
|
+
notifySettled = resolve;
|
|
3046
|
+
});
|
|
3047
|
+
const detach = (reject, reason) => {
|
|
3048
|
+
if (detached || workState !== void 0) return;
|
|
3049
|
+
detached = true;
|
|
3050
|
+
if (callerTimer !== void 0) clearTimeout(callerTimer);
|
|
3051
|
+
if (onExternal !== void 0) options.external?.removeEventListener("abort", onExternal);
|
|
3052
|
+
options.onDetach?.();
|
|
3053
|
+
deadline.abort(reason);
|
|
3054
|
+
reject(new OperationCancelled(reason));
|
|
3055
|
+
};
|
|
3056
|
+
const caller = new Promise((resolve, reject) => {
|
|
3057
|
+
callerTimer = setTimeout(() => detach(reject, options.timeoutError()), options.timeoutMs);
|
|
3058
|
+
onExternal = () => {
|
|
3059
|
+
detach(reject, combined.signal.reason);
|
|
3060
|
+
};
|
|
3061
|
+
options.external?.addEventListener("abort", onExternal, { once: true });
|
|
3062
|
+
(async () => {
|
|
3063
|
+
try {
|
|
3064
|
+
const value = await start(combined.signal);
|
|
3065
|
+
workState = "fulfilled";
|
|
3066
|
+
if (callerTimer !== void 0) clearTimeout(callerTimer);
|
|
3067
|
+
if (onExternal !== void 0) options.external?.removeEventListener("abort", onExternal);
|
|
3068
|
+
if (detached) combined.cleanup();
|
|
3069
|
+
if (detached) lateFulfilled = value;
|
|
3070
|
+
else resolve(value);
|
|
3071
|
+
notifySettled?.();
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
workState = "rejected";
|
|
3074
|
+
if (callerTimer !== void 0) clearTimeout(callerTimer);
|
|
3075
|
+
if (onExternal !== void 0) options.external?.removeEventListener("abort", onExternal);
|
|
3076
|
+
if (detached) combined.cleanup();
|
|
3077
|
+
if (!detached) reject(error);
|
|
3078
|
+
notifySettled?.();
|
|
3079
|
+
}
|
|
3080
|
+
})();
|
|
3081
|
+
});
|
|
3082
|
+
let released = false;
|
|
3083
|
+
return {
|
|
3084
|
+
caller,
|
|
3085
|
+
settled,
|
|
3086
|
+
detached: () => detached,
|
|
3087
|
+
lateValue: () => detached && workState === "fulfilled" ? lateFulfilled : void 0,
|
|
3088
|
+
release: () => {
|
|
3089
|
+
if (!released) {
|
|
3090
|
+
released = true;
|
|
3091
|
+
combined.cleanup();
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
2683
3096
|
async function abortAndDisposeLocal(entries) {
|
|
2684
3097
|
const pending = [];
|
|
2685
3098
|
for (const entry of entries) {
|
|
@@ -2867,6 +3280,7 @@ function renderPairPage(locale) {
|
|
|
2867
3280
|
<html lang="${copy.lang}">
|
|
2868
3281
|
<meta charset="utf-8">
|
|
2869
3282
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
3283
|
+
<style>input,textarea{font-size:16px}</style>
|
|
2870
3284
|
<title>${copy.pairTitle}</title>
|
|
2871
3285
|
<main>
|
|
2872
3286
|
<h1>${copy.pairHeading}</h1>
|
|
@@ -3160,32 +3574,60 @@ var ExcludedClientModulesError = class extends Error {
|
|
|
3160
3574
|
super(`excludedClientModules: ${message}`);
|
|
3161
3575
|
}
|
|
3162
3576
|
};
|
|
3163
|
-
/**
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3577
|
+
/** A graph-selection conflict with a bounded diagnostic safe for authenticated callers. */
|
|
3578
|
+
var ClientModuleConflictError = class extends HttpError {
|
|
3579
|
+
detail;
|
|
3580
|
+
constructor(detail) {
|
|
3581
|
+
super(409, "excluded_client_modules_invalid");
|
|
3582
|
+
this.detail = detail.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/(?:https?:\/\/|[a-z]:[\\/])\S+/giu, "[redacted]").slice(0, 512);
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
function requiredClientModule(entry, phase) {
|
|
3586
|
+
return MOBILE_BOOT_CORE_MODULES.has(entry.id) || entry.immediately === true || phase !== "application";
|
|
3587
|
+
}
|
|
3588
|
+
function clientModuleDependencies(entry) {
|
|
3589
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
3590
|
+
for (const [field, edges] of [["inject", entry.inject], ["external", entry.external]]) {
|
|
3591
|
+
if (edges !== void 0 && (!Array.isArray(edges) || edges.some((id) => typeof id !== "string"))) throw new ExcludedClientModulesError(`upstream module ${entry.id} has malformed ${field} dependencies`);
|
|
3592
|
+
for (const dependency of edges ?? []) dependencies.add(dependency.endsWith("/client") ? dependency.slice(0, -7) : dependency);
|
|
3593
|
+
}
|
|
3594
|
+
return Object.freeze([...dependencies]);
|
|
3595
|
+
}
|
|
3596
|
+
function clientModulePhases(entries, batches) {
|
|
3169
3597
|
const phases = /* @__PURE__ */ new Map();
|
|
3170
3598
|
for (const batch of batches) for (const id of batch.entries) {
|
|
3171
3599
|
if (phases.has(id)) throw new ExcludedClientModulesError(`upstream module ${id} belongs to multiple batches`);
|
|
3172
3600
|
phases.set(id, batch.phase);
|
|
3173
3601
|
}
|
|
3174
3602
|
for (const entry of entries) if (!phases.has(entry.id)) throw new ExcludedClientModulesError(`upstream module ${entry.id} belongs to no batch`);
|
|
3603
|
+
return phases;
|
|
3604
|
+
}
|
|
3605
|
+
/** Read only installed module ids and declared edges from the same boot parser used for serving. */
|
|
3606
|
+
function describeMobileClientModules(html) {
|
|
3607
|
+
const plan = parseMobileBootPlan(html);
|
|
3608
|
+
const ids = new Set(plan.entries.map((entry) => entry.id));
|
|
3609
|
+
const phases = plan.batches === void 0 ? /* @__PURE__ */ new Map() : clientModulePhases(plan.entries, plan.batches);
|
|
3610
|
+
return Object.freeze(plan.entries.map((entry) => Object.freeze({
|
|
3611
|
+
id: entry.id,
|
|
3612
|
+
required: requiredClientModule(entry, phases.get(entry.id)),
|
|
3613
|
+
dependencies: Object.freeze(clientModuleDependencies(entry).filter((id) => ids.has(id)))
|
|
3614
|
+
})));
|
|
3615
|
+
}
|
|
3616
|
+
/** Validate graph edges before removing application rows from both manifest views. */
|
|
3617
|
+
function excludeMobileBootEntries(entries, batches, excludedIds) {
|
|
3618
|
+
if (excludedIds.length === 0) return;
|
|
3619
|
+
const excluded = new Set(excludedIds);
|
|
3620
|
+
if (excluded.size !== excludedIds.length) throw new ExcludedClientModulesError("duplicate module ids");
|
|
3621
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
3622
|
+
const phases = clientModulePhases(entries, batches);
|
|
3175
3623
|
for (const id of excluded) {
|
|
3176
3624
|
const entry = byId.get(id);
|
|
3177
3625
|
if (entry === void 0) throw new ExcludedClientModulesError(`module ${id} is not installed in this DSH client graph`);
|
|
3178
|
-
if (
|
|
3626
|
+
if (requiredClientModule(entry, phases.get(id))) throw new ExcludedClientModulesError(`module ${id} is required for mobile boot or belongs to a bootstrap batch`);
|
|
3179
3627
|
}
|
|
3180
3628
|
for (const entry of entries) {
|
|
3181
3629
|
if (excluded.has(entry.id)) continue;
|
|
3182
|
-
for (const
|
|
3183
|
-
if (edges !== void 0 && (!Array.isArray(edges) || edges.some((id) => typeof id !== "string"))) throw new ExcludedClientModulesError(`upstream module ${entry.id} has malformed ${field} dependencies`);
|
|
3184
|
-
for (const dependency of edges ?? []) {
|
|
3185
|
-
const packageId = dependency.endsWith("/client") ? dependency.slice(0, -7) : dependency;
|
|
3186
|
-
if (excluded.has(packageId)) throw new ExcludedClientModulesError(`module ${entry.id} still depends on excluded module ${packageId}; exclude the dependent too or keep ${packageId}`);
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3630
|
+
for (const packageId of clientModuleDependencies(entry)) if (excluded.has(packageId)) throw new ExcludedClientModulesError(`module ${entry.id} still depends on excluded module ${packageId}; exclude the dependent too or keep ${packageId}`);
|
|
3189
3631
|
}
|
|
3190
3632
|
entries.splice(0, entries.length, ...entries.filter((entry) => !excluded.has(entry.id)));
|
|
3191
3633
|
for (const batch of batches) batch.entries = batch.entries.filter((id) => !excluded.has(id));
|
|
@@ -3830,13 +4272,18 @@ var MobileAccessGateway = class {
|
|
|
3830
4272
|
removeExtensionContentListener;
|
|
3831
4273
|
renewLimiter;
|
|
3832
4274
|
probeLimiter;
|
|
3833
|
-
|
|
4275
|
+
clientModulePreferences;
|
|
4276
|
+
clientModuleOperations = /* @__PURE__ */ new Set();
|
|
4277
|
+
clientModuleTasks = /* @__PURE__ */ new Set();
|
|
4278
|
+
clientModuleRevocations = /* @__PURE__ */ new Set();
|
|
4279
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded, clientModulePreferences) {
|
|
3834
4280
|
this.config = config;
|
|
3835
4281
|
this.extensions = extensions;
|
|
3836
4282
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
3837
4283
|
this.extraWebSocketPaths = extraWebSocketPaths;
|
|
3838
4284
|
this.blockedUpgradeLog = blockedUpgradeLog;
|
|
3839
4285
|
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
4286
|
+
this.clientModulePreferences = clientModulePreferences ?? new ClientModulePreferenceStore(join(dirname(config.stateFile), `${basename(config.stateFile)}.client-modules.json`), config.excludedClientModules);
|
|
3840
4287
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
3841
4288
|
this.tlsEnabled = config.publicTls;
|
|
3842
4289
|
this.access = new AccessController(store, {
|
|
@@ -3853,6 +4300,11 @@ var MobileAccessGateway = class {
|
|
|
3853
4300
|
this.probeLimiter = new BoundedRateLimiter(Math.min(1e3, config.maxDevices * 4), config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
3854
4301
|
this.removeSessionListener = this.access.onSessionEnded((authorization, reason) => {
|
|
3855
4302
|
if (reason === "revoked") {
|
|
4303
|
+
const cleanup = this.clientModulePreferences.removeDevice(authorization.deviceId).catch(() => {
|
|
4304
|
+
process.emitWarning("Unable to remove revoked device client preferences", { code: "DSH_MOBILE_PREFERENCE_CLEANUP_FAILED" });
|
|
4305
|
+
});
|
|
4306
|
+
this.clientModuleRevocations.add(cleanup);
|
|
4307
|
+
cleanup.finally(() => this.clientModuleRevocations.delete(cleanup));
|
|
3856
4308
|
if (!this.pendingDeviceRevocations.has(authorization.deviceId)) {
|
|
3857
4309
|
this.pendingDeviceRevocations.add(authorization.deviceId);
|
|
3858
4310
|
this.broadcastDeviceRevoked(authorization.deviceId);
|
|
@@ -3873,6 +4325,7 @@ var MobileAccessGateway = class {
|
|
|
3873
4325
|
this.started = true;
|
|
3874
4326
|
await this.access.initialize();
|
|
3875
4327
|
try {
|
|
4328
|
+
await this.clientModulePreferences.initialize();
|
|
3876
4329
|
if (this.config.pairingCaFile !== void 0) {
|
|
3877
4330
|
const certificate = new X509Certificate(await readFile(this.config.pairingCaFile));
|
|
3878
4331
|
const fingerprint = certificate.fingerprint256.replaceAll(":", "").toLowerCase();
|
|
@@ -4376,6 +4829,7 @@ var MobileAccessGateway = class {
|
|
|
4376
4829
|
}
|
|
4377
4830
|
const computerImages = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-images`;
|
|
4378
4831
|
const computerImage = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-image`;
|
|
4832
|
+
const clientModules = target.decodedPathname === `${AUTH_PREFIX}/client-modules`;
|
|
4379
4833
|
const requestedExtension = extensionTarget(target.decodedPathname);
|
|
4380
4834
|
const requestedMobileBootBatch = mobileBootBatchKey(target.decodedPathname);
|
|
4381
4835
|
const customAsset = request.method === "GET" ? target.decodedPathname === `/mobile-access/custom.css` ? {
|
|
@@ -4395,7 +4849,7 @@ var MobileAccessGateway = class {
|
|
|
4395
4849
|
contentType: "text/javascript; charset=utf-8",
|
|
4396
4850
|
fallback: void 0
|
|
4397
4851
|
} : void 0 : void 0;
|
|
4398
|
-
if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
4852
|
+
if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && !clientModules && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
4399
4853
|
if (![
|
|
4400
4854
|
"GET",
|
|
4401
4855
|
"HEAD",
|
|
@@ -4424,6 +4878,44 @@ var MobileAccessGateway = class {
|
|
|
4424
4878
|
throw error;
|
|
4425
4879
|
}
|
|
4426
4880
|
if (isMutation) this.requireCsrf(request, authorization);
|
|
4881
|
+
if (clientModules) {
|
|
4882
|
+
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
4883
|
+
if (request.method !== "GET" && request.method !== "POST") throw new HttpError(405, "method_not_allowed");
|
|
4884
|
+
const operation = this.allocateRequest(authorization, response, {});
|
|
4885
|
+
const onClose = () => {
|
|
4886
|
+
operation.abort();
|
|
4887
|
+
operation.release();
|
|
4888
|
+
};
|
|
4889
|
+
response.once("close", onClose);
|
|
4890
|
+
try {
|
|
4891
|
+
let kind = "describe";
|
|
4892
|
+
let excluded;
|
|
4893
|
+
if (request.method === "POST") {
|
|
4894
|
+
const body = await readJsonObject(request, this.config.maxBodyBytes);
|
|
4895
|
+
const keys = Object.keys(body);
|
|
4896
|
+
if (keys.length !== 1) throw new HttpError(400, "bad_request");
|
|
4897
|
+
if (keys[0] === "reset" && body.reset === true) kind = "reset";
|
|
4898
|
+
else if (keys[0] === "excludedClientModules") {
|
|
4899
|
+
kind = "configure";
|
|
4900
|
+
excluded = body.excludedClientModules;
|
|
4901
|
+
} else throw new HttpError(400, "bad_request");
|
|
4902
|
+
}
|
|
4903
|
+
const view = await this.clientModulePreferenceOperation(kind, excluded, authorization.deviceId, operation.signal);
|
|
4904
|
+
operation.signal.throwIfAborted();
|
|
4905
|
+
sendJson(response, 200, view, this.tlsEnabled);
|
|
4906
|
+
} catch (error) {
|
|
4907
|
+
if (operation.signal.aborted) return;
|
|
4908
|
+
if (error instanceof ClientModuleConflictError) sendJson(response, 409, {
|
|
4909
|
+
error: error.code,
|
|
4910
|
+
detail: error.detail
|
|
4911
|
+
}, this.tlsEnabled);
|
|
4912
|
+
else throw error;
|
|
4913
|
+
} finally {
|
|
4914
|
+
response.removeListener("close", onClose);
|
|
4915
|
+
operation.release();
|
|
4916
|
+
}
|
|
4917
|
+
return;
|
|
4918
|
+
}
|
|
4427
4919
|
const extension = requestedExtension;
|
|
4428
4920
|
if (extension !== void 0) {
|
|
4429
4921
|
await this.handleExtensionRequest(extension, target, request, response, authorization);
|
|
@@ -4594,6 +5086,7 @@ var MobileAccessGateway = class {
|
|
|
4594
5086
|
const operation = this.allocateRequest(authorization, response, {});
|
|
4595
5087
|
const abort = new AbortController();
|
|
4596
5088
|
response.once("close", () => {
|
|
5089
|
+
operation.release();
|
|
4597
5090
|
abort.abort();
|
|
4598
5091
|
});
|
|
4599
5092
|
const generationSignal = extensions.signal(targetInfo.id, generation);
|
|
@@ -4639,6 +5132,7 @@ var MobileAccessGateway = class {
|
|
|
4639
5132
|
const operation = this.allocateRequest(authorization, response, {});
|
|
4640
5133
|
const abort = new AbortController();
|
|
4641
5134
|
response.once("close", () => {
|
|
5135
|
+
operation.release();
|
|
4642
5136
|
abort.abort();
|
|
4643
5137
|
});
|
|
4644
5138
|
const generationSignal = extensions.signal(targetInfo.id, generation);
|
|
@@ -4742,7 +5236,7 @@ var MobileAccessGateway = class {
|
|
|
4742
5236
|
});
|
|
4743
5237
|
this.upstreamAuthRequest = upstreamRequest;
|
|
4744
5238
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
4745
|
-
upstreamRequest.destroy(
|
|
5239
|
+
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
4746
5240
|
});
|
|
4747
5241
|
upstreamRequest.once("response", resolve);
|
|
4748
5242
|
upstreamRequest.once("error", reject);
|
|
@@ -4770,33 +5264,125 @@ var MobileAccessGateway = class {
|
|
|
4770
5264
|
this.upstreamAuthRequest = void 0;
|
|
4771
5265
|
}
|
|
4772
5266
|
}
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
5267
|
+
/** List installed modules and resolve the next-open selection.
|
|
5268
|
+
* @param deviceId - Authenticated device identity; omitted for computer defaults.
|
|
5269
|
+
* @param callerSignal - Cancels catalog fetching when the admin request closes.
|
|
5270
|
+
* @returns The installed catalog and effective module selection.
|
|
5271
|
+
*/
|
|
5272
|
+
describeClientModules(deviceId, callerSignal) {
|
|
5273
|
+
return this.clientModulePreferenceOperation("describe", void 0, deviceId, callerSignal);
|
|
5274
|
+
}
|
|
5275
|
+
/** Persist a validated device override or computer default.
|
|
5276
|
+
* @param excluded - Untrusted module IDs to validate against the current boot graph.
|
|
5277
|
+
* @param deviceId - Authenticated device identity; omitted for computer defaults.
|
|
5278
|
+
* @param callerSignal - Cancels fetching and validation, not an atomic write already started.
|
|
5279
|
+
* @returns The catalog and saved selection without reloading clients.
|
|
5280
|
+
*/
|
|
5281
|
+
configureClientModules(excluded, deviceId, callerSignal) {
|
|
5282
|
+
return this.clientModulePreferenceOperation("configure", excluded, deviceId, callerSignal);
|
|
5283
|
+
}
|
|
5284
|
+
/** Reset one override without reloading connected clients.
|
|
5285
|
+
* @param deviceId - Authenticated device identity; omitted to reset computer defaults.
|
|
5286
|
+
* @param callerSignal - Cancels fetching and validation, not an atomic write already started.
|
|
5287
|
+
* @returns The catalog and inherited selection.
|
|
5288
|
+
*/
|
|
5289
|
+
resetClientModules(deviceId, callerSignal) {
|
|
5290
|
+
return this.clientModulePreferenceOperation("reset", void 0, deviceId, callerSignal);
|
|
5291
|
+
}
|
|
5292
|
+
async clientModulePreferenceOperation(kind, excluded, deviceId, callerSignal) {
|
|
5293
|
+
if (this.closing || !this.started) throw new HttpError(503, "unavailable");
|
|
5294
|
+
const controller = new AbortController();
|
|
5295
|
+
const abort = () => controller.abort();
|
|
5296
|
+
if (callerSignal?.aborted === true) controller.abort();
|
|
5297
|
+
callerSignal?.addEventListener("abort", abort, { once: true });
|
|
5298
|
+
this.clientModuleOperations.add(controller);
|
|
5299
|
+
const task = (async () => {
|
|
5300
|
+
let html = "";
|
|
5301
|
+
const validate = async (selection) => {
|
|
5302
|
+
html = (await this.fetchAuthenticatedUpstreamIndex(controller.signal)).html;
|
|
5303
|
+
try {
|
|
5304
|
+
parseMobileBootPlan(html, selection);
|
|
5305
|
+
} catch (error) {
|
|
5306
|
+
if (error instanceof ExcludedClientModulesError) throw new ClientModuleConflictError(error.message);
|
|
5307
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
5308
|
+
}
|
|
5309
|
+
controller.signal.throwIfAborted();
|
|
5310
|
+
};
|
|
5311
|
+
let selection;
|
|
5312
|
+
if (kind === "configure") {
|
|
5313
|
+
let ids;
|
|
5314
|
+
try {
|
|
5315
|
+
ids = parseExcludedClientModules(excluded);
|
|
5316
|
+
} catch {
|
|
5317
|
+
throw new HttpError(400, "excluded_client_modules_invalid");
|
|
5318
|
+
}
|
|
5319
|
+
selection = await this.clientModulePreferences.configure(ids, deviceId, validate);
|
|
5320
|
+
} else if (kind === "reset") selection = await this.clientModulePreferences.reset(deviceId, validate);
|
|
5321
|
+
else {
|
|
5322
|
+
html = (await this.fetchAuthenticatedUpstreamIndex(controller.signal)).html;
|
|
5323
|
+
selection = await this.clientModulePreferences.read(deviceId);
|
|
5324
|
+
}
|
|
5325
|
+
controller.signal.throwIfAborted();
|
|
5326
|
+
try {
|
|
5327
|
+
const plan = parseMobileBootPlan(html);
|
|
5328
|
+
return Object.freeze({
|
|
5329
|
+
entries: describeMobileClientModules(html),
|
|
5330
|
+
...selection,
|
|
5331
|
+
...plan.batches === void 0 ? {
|
|
5332
|
+
excludedClientModules: Object.freeze([]),
|
|
5333
|
+
defaultExcludedClientModules: Object.freeze([])
|
|
5334
|
+
} : {},
|
|
5335
|
+
reloadRequired: true
|
|
5336
|
+
});
|
|
5337
|
+
} catch (error) {
|
|
5338
|
+
if (error instanceof HttpError) throw error;
|
|
5339
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
5340
|
+
}
|
|
5341
|
+
})();
|
|
5342
|
+
this.clientModuleTasks.add(task);
|
|
4776
5343
|
try {
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
5344
|
+
return await task;
|
|
5345
|
+
} finally {
|
|
5346
|
+
callerSignal?.removeEventListener("abort", abort);
|
|
5347
|
+
this.clientModuleOperations.delete(controller);
|
|
5348
|
+
this.clientModuleTasks.delete(task);
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
async fetchAuthenticatedUpstreamIndex(signal, incoming) {
|
|
5352
|
+
signal.throwIfAborted();
|
|
5353
|
+
let upstreamRequest;
|
|
5354
|
+
const abort = () => {
|
|
5355
|
+
upstreamRequest?.destroy(requestAbortedError());
|
|
5356
|
+
};
|
|
5357
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5358
|
+
try {
|
|
5359
|
+
const headers = incoming === void 0 ? {
|
|
5360
|
+
host: this.config.upstreamOrigin.host,
|
|
5361
|
+
accept: "text/html",
|
|
5362
|
+
"accept-encoding": "identity"
|
|
5363
|
+
} : sanitizeRequestHeaders(incoming, this.config.upstreamOrigin);
|
|
5364
|
+
const upstreamCookie = await waitForRequestTask(this.upstreamCookieHeader(), signal);
|
|
5365
|
+
signal.throwIfAborted();
|
|
5366
|
+
if (upstreamCookie !== void 0) headers.cookie = upstreamCookie;
|
|
5367
|
+
headers["accept-encoding"] = "identity";
|
|
4781
5368
|
const proxied = await new Promise((resolve, reject) => {
|
|
4782
|
-
|
|
5369
|
+
upstreamRequest = request({
|
|
4783
5370
|
protocol: "http:",
|
|
4784
5371
|
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
4785
5372
|
port: Number(this.config.upstreamOrigin.port),
|
|
4786
5373
|
method: "GET",
|
|
4787
5374
|
path: "/",
|
|
4788
|
-
headers
|
|
5375
|
+
headers,
|
|
4789
5376
|
agent: false
|
|
4790
5377
|
});
|
|
4791
|
-
holder.request = upstreamRequest;
|
|
4792
5378
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
4793
|
-
upstreamRequest
|
|
5379
|
+
upstreamRequest?.destroy(new HttpError(504, "upstream_timeout"));
|
|
4794
5380
|
});
|
|
4795
5381
|
upstreamRequest.once("response", resolve);
|
|
4796
5382
|
upstreamRequest.once("error", reject);
|
|
4797
5383
|
upstreamRequest.end();
|
|
4798
5384
|
});
|
|
4799
|
-
if (
|
|
5385
|
+
if (proxied.statusCode !== 200) throw new HttpError(502, "upstream_unavailable");
|
|
4800
5386
|
const chunks = [];
|
|
4801
5387
|
let bytes = 0;
|
|
4802
5388
|
for await (const chunk of proxied) {
|
|
@@ -4805,15 +5391,40 @@ var MobileAccessGateway = class {
|
|
|
4805
5391
|
if (bytes > 4194304) throw new HttpError(502, "upstream_unavailable");
|
|
4806
5392
|
chunks.push(buffer);
|
|
4807
5393
|
}
|
|
5394
|
+
signal.throwIfAborted();
|
|
5395
|
+
return {
|
|
5396
|
+
html: Buffer.concat(chunks).toString("utf8"),
|
|
5397
|
+
headers: proxied.headers
|
|
5398
|
+
};
|
|
5399
|
+
} catch (error) {
|
|
5400
|
+
if (signal.aborted || error instanceof HttpError) throw error;
|
|
5401
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
5402
|
+
} finally {
|
|
5403
|
+
signal.removeEventListener("abort", abort);
|
|
5404
|
+
upstreamRequest?.destroy();
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
async proxyMobileIndex(request, response, authorization) {
|
|
5408
|
+
const holder = {};
|
|
5409
|
+
const operation = this.allocateRequest(authorization, response, holder);
|
|
5410
|
+
const onClose = () => {
|
|
5411
|
+
operation.abort();
|
|
5412
|
+
operation.release();
|
|
5413
|
+
};
|
|
5414
|
+
response.once("close", onClose);
|
|
5415
|
+
try {
|
|
5416
|
+
const index = await this.fetchAuthenticatedUpstreamIndex(operation.signal, request);
|
|
5417
|
+
const preference = await this.clientModulePreferences.read(authorization.deviceId);
|
|
5418
|
+
const excludedClientModules = preference.excludedClientModules;
|
|
4808
5419
|
let body;
|
|
4809
5420
|
try {
|
|
4810
|
-
const html =
|
|
4811
|
-
const plan = parseMobileBootPlan(html,
|
|
4812
|
-
const options = plan.rewrittenBatchEntries === void 0 ? { excludedClientModules
|
|
4813
|
-
...await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries)),
|
|
4814
|
-
excludedClientModules
|
|
5421
|
+
const html = index.html;
|
|
5422
|
+
const plan = parseMobileBootPlan(html, excludedClientModules);
|
|
5423
|
+
const options = plan.rewrittenBatchEntries === void 0 ? { excludedClientModules } : {
|
|
5424
|
+
...await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries), operation.signal),
|
|
5425
|
+
excludedClientModules
|
|
4815
5426
|
};
|
|
4816
|
-
const cookies = browserAuthCookies(request
|
|
5427
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
4817
5428
|
const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
|
|
4818
5429
|
const rewritten = rewriteMobileIndexWithBatch(html, {
|
|
4819
5430
|
...options,
|
|
@@ -4828,6 +5439,18 @@ var MobileAccessGateway = class {
|
|
|
4828
5439
|
this.lastModuleExclusionError = detail;
|
|
4829
5440
|
process.emitWarning(detail, { code: "DSH_MOBILE_MODULE_EXCLUSION_INVALID" });
|
|
4830
5441
|
}
|
|
5442
|
+
if (preference.source === "device" || preference.source === "computer") {
|
|
5443
|
+
operation.signal.throwIfAborted();
|
|
5444
|
+
const locale = resolveAuthPageLocale(headerValue(request.headers, "accept-language"));
|
|
5445
|
+
const recovery = Buffer.from(renderClientModuleRecoveryPage(locale));
|
|
5446
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
5447
|
+
response.writeHead(200, {
|
|
5448
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
5449
|
+
"Content-Length": recovery.byteLength
|
|
5450
|
+
});
|
|
5451
|
+
response.end(recovery);
|
|
5452
|
+
return;
|
|
5453
|
+
}
|
|
4831
5454
|
sendJson(response, 409, {
|
|
4832
5455
|
error: "excluded_client_modules_invalid",
|
|
4833
5456
|
detail
|
|
@@ -4836,7 +5459,8 @@ var MobileAccessGateway = class {
|
|
|
4836
5459
|
}
|
|
4837
5460
|
throw new HttpError(502, "upstream_unavailable");
|
|
4838
5461
|
}
|
|
4839
|
-
|
|
5462
|
+
operation.signal.throwIfAborted();
|
|
5463
|
+
const headers = sanitizeResponseHeaders(index.headers, this.config.upstreamOrigin);
|
|
4840
5464
|
delete headers["content-length"];
|
|
4841
5465
|
delete headers["content-encoding"];
|
|
4842
5466
|
delete headers.etag;
|
|
@@ -4853,6 +5477,7 @@ var MobileAccessGateway = class {
|
|
|
4853
5477
|
if (response.headersSent) response.destroy();
|
|
4854
5478
|
else throw new HttpError(502, "upstream_unavailable");
|
|
4855
5479
|
} finally {
|
|
5480
|
+
response.removeListener("close", onClose);
|
|
4856
5481
|
operation.release();
|
|
4857
5482
|
}
|
|
4858
5483
|
}
|
|
@@ -4873,16 +5498,17 @@ var MobileAccessGateway = class {
|
|
|
4873
5498
|
* could not be probed, keeps its own upstream `/plugins` row: a merged batch
|
|
4874
5499
|
* cannot carry it, and an unknown bundle must not risk the whole batch.
|
|
4875
5500
|
*/
|
|
4876
|
-
async resolveMobileBootSizes(planEntries) {
|
|
5501
|
+
async resolveMobileBootSizes(planEntries, signal) {
|
|
4877
5502
|
const sizes = /* @__PURE__ */ new Map();
|
|
4878
5503
|
const passThrough = /* @__PURE__ */ new Set();
|
|
4879
5504
|
const candidates = planEntries.filter((entry) => entry.id !== MOBILE_LAYOUT_MODULE && upstreamPluginBundleUrl(entry.url, this.config.upstreamOrigin) !== void 0);
|
|
4880
5505
|
let cursor = 0;
|
|
4881
5506
|
const worker = async () => {
|
|
4882
5507
|
while (cursor < candidates.length) {
|
|
5508
|
+
signal?.throwIfAborted();
|
|
4883
5509
|
const index = cursor++;
|
|
4884
5510
|
const source = candidates[index].url;
|
|
4885
|
-
const size = await this.upstreamBundleSize(source);
|
|
5511
|
+
const size = await this.upstreamBundleSize(source, signal);
|
|
4886
5512
|
if (size === void 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
4887
5513
|
else sizes.set(source, size);
|
|
4888
5514
|
}
|
|
@@ -4899,14 +5525,20 @@ var MobileAccessGateway = class {
|
|
|
4899
5525
|
* counts bytes; it aborts instantly past the per-entry cap. Measurements are
|
|
4900
5526
|
* cached for a short window.
|
|
4901
5527
|
*/
|
|
4902
|
-
async upstreamBundleSize(source) {
|
|
5528
|
+
async upstreamBundleSize(source, signal) {
|
|
5529
|
+
signal?.throwIfAborted();
|
|
4903
5530
|
const target = upstreamPluginBundleUrl(source, this.config.upstreamOrigin);
|
|
4904
5531
|
if (target === void 0) return void 0;
|
|
4905
5532
|
const cached = this.bootEntrySizeCache.get(source);
|
|
4906
5533
|
if (cached !== void 0 && Date.now() - cached.at < MOBILE_BOOT_SIZE_CACHE_TTL_MS) return cached.size;
|
|
4907
|
-
const upstreamCookie = await this.upstreamCookieHeader();
|
|
4908
5534
|
let upstreamRequest;
|
|
5535
|
+
const abort = () => {
|
|
5536
|
+
upstreamRequest?.destroy(requestAbortedError());
|
|
5537
|
+
};
|
|
5538
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
4909
5539
|
try {
|
|
5540
|
+
const upstreamCookie = signal === void 0 ? await this.upstreamCookieHeader() : await waitForRequestTask(this.upstreamCookieHeader(), signal);
|
|
5541
|
+
signal?.throwIfAborted();
|
|
4910
5542
|
const proxied = await new Promise((resolve, reject) => {
|
|
4911
5543
|
upstreamRequest = request({
|
|
4912
5544
|
protocol: "http:",
|
|
@@ -4952,9 +5584,11 @@ var MobileAccessGateway = class {
|
|
|
4952
5584
|
at: Date.now()
|
|
4953
5585
|
});
|
|
4954
5586
|
return size;
|
|
4955
|
-
} catch {
|
|
5587
|
+
} catch (error) {
|
|
5588
|
+
if (signal?.aborted === true) throw error;
|
|
4956
5589
|
return;
|
|
4957
5590
|
} finally {
|
|
5591
|
+
signal?.removeEventListener("abort", abort);
|
|
4958
5592
|
upstreamRequest?.destroy();
|
|
4959
5593
|
}
|
|
4960
5594
|
}
|
|
@@ -5121,6 +5755,7 @@ var MobileAccessGateway = class {
|
|
|
5121
5755
|
}
|
|
5122
5756
|
}
|
|
5123
5757
|
allocateRequest(authorization, response, upstream) {
|
|
5758
|
+
if (this.closing || response.destroyed || response.writableEnded) throw requestAbortedError();
|
|
5124
5759
|
if (this.activeRequests.size >= this.config.maxActiveRequests) throw new HttpError(429, "busy");
|
|
5125
5760
|
const id = this.nextOperationId++;
|
|
5126
5761
|
const controller = new AbortController();
|
|
@@ -5148,72 +5783,88 @@ var MobileAccessGateway = class {
|
|
|
5148
5783
|
};
|
|
5149
5784
|
}
|
|
5150
5785
|
/**
|
|
5151
|
-
* Proxy one request
|
|
5152
|
-
*
|
|
5153
|
-
*
|
|
5154
|
-
*
|
|
5786
|
+
* Proxy one request, retrying transient failures for plugin and asset GETs.
|
|
5787
|
+
* Only bodyless GETs can be replayed, before response headers reach the client.
|
|
5788
|
+
* One request slot and cancellation signal cover attempts and backoff together.
|
|
5789
|
+
* API requests and requests with bodies are forwarded once.
|
|
5155
5790
|
*/
|
|
5156
5791
|
async proxyHttp(request, response, authorization) {
|
|
5157
|
-
|
|
5158
|
-
try {
|
|
5159
|
-
await this.proxyHttpOnce(request, response, authorization);
|
|
5160
|
-
} catch (error) {
|
|
5161
|
-
if (error instanceof HttpError) throw error;
|
|
5162
|
-
if (response.headersSent) response.destroy();
|
|
5163
|
-
else throw new HttpError(502, "upstream_unavailable");
|
|
5164
|
-
}
|
|
5165
|
-
return;
|
|
5166
|
-
}
|
|
5167
|
-
const delay = (attempt) => new Promise((resolve) => setTimeout(resolve, MOBILE_BOOT_RETRY_DELAY_MS * attempt));
|
|
5168
|
-
for (let attempt = 1; attempt <= MOBILE_BOOT_UPSTREAM_ATTEMPTS; attempt++) try {
|
|
5169
|
-
await this.proxyHttpOnce(request, response, authorization);
|
|
5170
|
-
return;
|
|
5171
|
-
} catch (error) {
|
|
5172
|
-
if (response.headersSent) {
|
|
5173
|
-
response.destroy();
|
|
5174
|
-
return;
|
|
5175
|
-
}
|
|
5176
|
-
if (error instanceof HttpError || !isTransientUpstreamError(error)) throw error instanceof HttpError ? error : new HttpError(502, "upstream_unavailable");
|
|
5177
|
-
if (attempt === MOBILE_BOOT_UPSTREAM_ATTEMPTS) throw new HttpError(502, "upstream_unavailable");
|
|
5178
|
-
await delay(attempt);
|
|
5179
|
-
}
|
|
5180
|
-
throw new HttpError(502, "upstream_unavailable");
|
|
5181
|
-
}
|
|
5182
|
-
async proxyHttpOnce(request$3, response, authorization) {
|
|
5183
|
-
const declared = request$3.headers["content-length"];
|
|
5792
|
+
const declared = request.headers["content-length"];
|
|
5184
5793
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
5794
|
+
const replayable = request.method === "GET" && request.headers["transfer-encoding"] === void 0 && (declared === void 0 || Number(declared) === 0) && (request.url?.split("?", 1)[0]?.startsWith("/plugins/") === true || request.url?.split("?", 1)[0]?.startsWith("/assets/") === true);
|
|
5185
5795
|
const holder = {};
|
|
5186
5796
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
5187
|
-
|
|
5797
|
+
const onClose = () => {
|
|
5798
|
+
operation.release();
|
|
5799
|
+
operation.abort();
|
|
5800
|
+
};
|
|
5801
|
+
response.once("close", onClose);
|
|
5802
|
+
if (replayable) request.resume();
|
|
5188
5803
|
try {
|
|
5189
|
-
const
|
|
5190
|
-
|
|
5191
|
-
|
|
5804
|
+
const attempts = replayable ? MOBILE_BOOT_UPSTREAM_ATTEMPTS : 1;
|
|
5805
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
5806
|
+
operation.signal.throwIfAborted();
|
|
5807
|
+
try {
|
|
5808
|
+
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable);
|
|
5809
|
+
return;
|
|
5810
|
+
} catch (error) {
|
|
5811
|
+
if (operation.signal.aborted || response.destroyed) return;
|
|
5812
|
+
if (response.headersSent) {
|
|
5813
|
+
response.destroy();
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
if (error instanceof HttpError || !isTransientUpstreamError(error)) throw error instanceof HttpError ? error : new HttpError(502, "upstream_unavailable");
|
|
5817
|
+
if (attempt === attempts) throw new HttpError(502, "upstream_unavailable");
|
|
5818
|
+
await waitForAbortableDelay(MOBILE_BOOT_RETRY_DELAY_MS * attempt, operation.signal);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
} catch (error) {
|
|
5822
|
+
if (!operation.signal.aborted) throw error;
|
|
5823
|
+
} finally {
|
|
5824
|
+
response.removeListener("close", onClose);
|
|
5825
|
+
operation.release();
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
async proxyHttpOnce(request$2, response, holder, signal, replayable) {
|
|
5829
|
+
let bodyDone;
|
|
5830
|
+
try {
|
|
5831
|
+
const upstreamHeaders = sanitizeRequestHeaders(request$2, this.config.upstreamOrigin);
|
|
5832
|
+
if (request$2.headers["transfer-encoding"] !== void 0) upstreamHeaders["transfer-encoding"] = request$2.headers["transfer-encoding"];
|
|
5833
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
5834
|
+
signal.throwIfAborted();
|
|
5835
|
+
if (upstreamCookie !== void 0) upstreamHeaders.cookie = upstreamCookie;
|
|
5192
5836
|
const proxied = await new Promise((resolve, reject) => {
|
|
5193
5837
|
const upstreamRequest = request({
|
|
5194
5838
|
protocol: "http:",
|
|
5195
5839
|
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
5196
5840
|
port: Number(this.config.upstreamOrigin.port),
|
|
5197
|
-
method: request$
|
|
5198
|
-
path: request$
|
|
5841
|
+
method: request$2.method,
|
|
5842
|
+
path: request$2.url,
|
|
5199
5843
|
headers: upstreamHeaders,
|
|
5200
5844
|
agent: false
|
|
5201
5845
|
});
|
|
5202
5846
|
holder.request = upstreamRequest;
|
|
5203
5847
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
5204
|
-
upstreamRequest.destroy(
|
|
5848
|
+
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
5849
|
+
});
|
|
5850
|
+
const pathname = parseRequestTarget(request$2.url).decodedPathname;
|
|
5851
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) upstreamRequest.once("finish", () => {
|
|
5852
|
+
upstreamRequest.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
5205
5853
|
});
|
|
5206
5854
|
upstreamRequest.once("response", resolve);
|
|
5207
5855
|
upstreamRequest.once("error", reject);
|
|
5208
|
-
|
|
5209
|
-
|
|
5856
|
+
if (replayable) upstreamRequest.end();
|
|
5857
|
+
else {
|
|
5858
|
+
bodyDone = pipeline(request$2, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
|
|
5859
|
+
bodyDone.catch(reject);
|
|
5860
|
+
}
|
|
5210
5861
|
});
|
|
5211
5862
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
5212
5863
|
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
5213
5864
|
const statusCode = proxied.statusCode ?? 502;
|
|
5214
|
-
const cacheControl = revisionedStaticCacheControl(request$
|
|
5865
|
+
const cacheControl = revisionedStaticCacheControl(request$2, statusCode);
|
|
5215
5866
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
5216
|
-
const compressed = shouldCompressResponse(request$
|
|
5867
|
+
const compressed = shouldCompressResponse(request$2, proxied);
|
|
5217
5868
|
if (compressed) {
|
|
5218
5869
|
delete headers["accept-ranges"];
|
|
5219
5870
|
delete headers["content-length"];
|
|
@@ -5230,7 +5881,7 @@ var MobileAccessGateway = class {
|
|
|
5230
5881
|
if (response.headersSent) response.destroy();
|
|
5231
5882
|
throw error;
|
|
5232
5883
|
} finally {
|
|
5233
|
-
|
|
5884
|
+
delete holder.request;
|
|
5234
5885
|
}
|
|
5235
5886
|
}
|
|
5236
5887
|
abortSessionResources(sessionKey) {
|
|
@@ -5579,12 +6230,19 @@ var MobileAccessGateway = class {
|
|
|
5579
6230
|
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5580
6231
|
if (typeof body.deviceId !== "string" || !/^[a-f\d]{32}$/u.test(body.deviceId)) throw new HttpError(400, "bad_request");
|
|
5581
6232
|
if (!await this.access.revokeDevice(body.deviceId)) throw new HttpError(404, "not_found");
|
|
6233
|
+
await this.clientModulePreferences.removeDevice(body.deviceId).catch(() => {
|
|
6234
|
+
process.emitWarning("Unable to remove revoked device client preferences", { code: "DSH_MOBILE_PREFERENCE_CLEANUP_FAILED" });
|
|
6235
|
+
});
|
|
5582
6236
|
sendJson(response, 200, { revoked: true }, false);
|
|
5583
6237
|
return;
|
|
5584
6238
|
}
|
|
5585
6239
|
if (request.method === "POST" && target.decodedPathname === `${prefix}/devices/reset`) {
|
|
5586
6240
|
if ((await readJsonObject(request, MAX_CONTROL_BODY_BYTES)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
6241
|
+
const removed = this.access.listDevices().map((device) => device.id);
|
|
5587
6242
|
await this.access.resetDevices();
|
|
6243
|
+
await this.clientModulePreferences.removeDevices(removed).catch(() => {
|
|
6244
|
+
process.emitWarning("Unable to remove revoked device client preferences", { code: "DSH_MOBILE_PREFERENCE_CLEANUP_FAILED" });
|
|
6245
|
+
});
|
|
5588
6246
|
sendJson(response, 200, { reset: true }, false);
|
|
5589
6247
|
return;
|
|
5590
6248
|
}
|
|
@@ -5605,6 +6263,7 @@ var MobileAccessGateway = class {
|
|
|
5605
6263
|
}
|
|
5606
6264
|
async performClose() {
|
|
5607
6265
|
this.closing = true;
|
|
6266
|
+
for (const controller of this.clientModuleOperations) controller.abort();
|
|
5608
6267
|
if (this.extensionChangeTimer !== void 0) clearInterval(this.extensionChangeTimer);
|
|
5609
6268
|
this.extensionChangeTimer = void 0;
|
|
5610
6269
|
this.removeExtensionContentListener();
|
|
@@ -5637,6 +6296,8 @@ var MobileAccessGateway = class {
|
|
|
5637
6296
|
});
|
|
5638
6297
|
}
|
|
5639
6298
|
await accessClose;
|
|
6299
|
+
await Promise.allSettled(this.clientModuleTasks);
|
|
6300
|
+
await Promise.allSettled(this.clientModuleRevocations);
|
|
5640
6301
|
this.activeRequests.clear();
|
|
5641
6302
|
this.activeWebSockets.clear();
|
|
5642
6303
|
this.connectedSockets.clear();
|
|
@@ -5777,8 +6438,8 @@ var MemoryDeviceStore = class {
|
|
|
5777
6438
|
//#endregion
|
|
5778
6439
|
//#region src/frp-component.ts
|
|
5779
6440
|
const FRP_VERSION$1 = "0.70.1";
|
|
5780
|
-
const MAX_ARCHIVE_ENTRIES = 128;
|
|
5781
|
-
const MAX_ARCHIVE_LIST_BYTES = 262144;
|
|
6441
|
+
const MAX_ARCHIVE_ENTRIES$1 = 128;
|
|
6442
|
+
const MAX_ARCHIVE_LIST_BYTES$1 = 262144;
|
|
5782
6443
|
/** Pinned official FRP release metadata for supported desktop targets. */
|
|
5783
6444
|
const FRP_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
5784
6445
|
{
|
|
@@ -5883,12 +6544,12 @@ async function replaceDirectory(target, candidate) {
|
|
|
5883
6544
|
function sha256$2(bytes) {
|
|
5884
6545
|
return createHash("sha256").update(bytes).digest("hex");
|
|
5885
6546
|
}
|
|
5886
|
-
async function runCapture(file, args) {
|
|
6547
|
+
async function runCapture$1(file, args) {
|
|
5887
6548
|
return new Promise((resolveRun, reject) => {
|
|
5888
6549
|
execFile(file, [...args], {
|
|
5889
6550
|
windowsHide: true,
|
|
5890
6551
|
timeout: 12e4,
|
|
5891
|
-
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
6552
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES$1,
|
|
5892
6553
|
encoding: "utf8"
|
|
5893
6554
|
}, (error, stdout) => {
|
|
5894
6555
|
if (error === null) resolveRun(stdout);
|
|
@@ -5896,7 +6557,7 @@ async function runCapture(file, args) {
|
|
|
5896
6557
|
});
|
|
5897
6558
|
});
|
|
5898
6559
|
}
|
|
5899
|
-
function validatedArchiveEntry(rawEntry) {
|
|
6560
|
+
function validatedArchiveEntry$1(rawEntry) {
|
|
5900
6561
|
if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("frp_archive_path_invalid");
|
|
5901
6562
|
const segments = rawEntry.replace(/\/$/u, "").split("/");
|
|
5902
6563
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("frp_archive_path_invalid");
|
|
@@ -5904,10 +6565,10 @@ function validatedArchiveEntry(rawEntry) {
|
|
|
5904
6565
|
}
|
|
5905
6566
|
/** Select exactly one nested frpc executable from a safe archive listing. */
|
|
5906
6567
|
function selectFrpExecutableEntry(entries, executableName) {
|
|
5907
|
-
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("frp_archive_entries_invalid");
|
|
6568
|
+
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES$1) throw new Error("frp_archive_entries_invalid");
|
|
5908
6569
|
let executableEntry;
|
|
5909
6570
|
for (const entry of entries) {
|
|
5910
|
-
const segments = validatedArchiveEntry(entry);
|
|
6571
|
+
const segments = validatedArchiveEntry$1(entry);
|
|
5911
6572
|
if (segments.length >= 2 && segments.at(-1) === executableName) {
|
|
5912
6573
|
if (executableEntry !== void 0) throw new Error("frp_archive_executable_ambiguous");
|
|
5913
6574
|
executableEntry = entry.replace(/\/$/u, "");
|
|
@@ -5916,22 +6577,22 @@ function selectFrpExecutableEntry(entries, executableName) {
|
|
|
5916
6577
|
if (executableEntry === void 0) throw new Error("frp_archive_executable_missing");
|
|
5917
6578
|
return executableEntry;
|
|
5918
6579
|
}
|
|
5919
|
-
async function defaultExtractArtifact$
|
|
6580
|
+
async function defaultExtractArtifact$2(archive, destination, executableName) {
|
|
5920
6581
|
const tar = process.platform === "win32" ? "tar.exe" : "tar";
|
|
5921
|
-
const executableEntry = selectFrpExecutableEntry((await runCapture(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
|
|
6582
|
+
const executableEntry = selectFrpExecutableEntry((await runCapture$1(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
|
|
5922
6583
|
const unpacked = join(destination, "archive");
|
|
5923
6584
|
await mkdir(unpacked, {
|
|
5924
6585
|
recursive: true,
|
|
5925
6586
|
mode: 448
|
|
5926
6587
|
});
|
|
5927
|
-
await runCapture(tar, [
|
|
6588
|
+
await runCapture$1(tar, [
|
|
5928
6589
|
"-xf",
|
|
5929
6590
|
archive,
|
|
5930
6591
|
"-C",
|
|
5931
6592
|
unpacked,
|
|
5932
6593
|
executableEntry
|
|
5933
6594
|
]);
|
|
5934
|
-
const extracted = join(unpacked, ...validatedArchiveEntry(executableEntry));
|
|
6595
|
+
const extracted = join(unpacked, ...validatedArchiveEntry$1(executableEntry));
|
|
5935
6596
|
if (!await regularFile$2(extracted)) throw new Error("frp_archive_executable_invalid");
|
|
5936
6597
|
await copyFile(extracted, join(destination, executableName));
|
|
5937
6598
|
}
|
|
@@ -5971,7 +6632,7 @@ async function defaultFetchArtifact$2(artifact, signal) {
|
|
|
5971
6632
|
return bytes;
|
|
5972
6633
|
}
|
|
5973
6634
|
async function defaultInspectExecutable(executable) {
|
|
5974
|
-
return (await runCapture(executable, ["--version"])).trim();
|
|
6635
|
+
return (await runCapture$1(executable, ["--version"])).trim();
|
|
5975
6636
|
}
|
|
5976
6637
|
/** Owns the optional official frpc binary inside the DSH Mobile state directory. */
|
|
5977
6638
|
var FrpComponentManager = class {
|
|
@@ -6006,7 +6667,7 @@ var FrpComponentManager = class {
|
|
|
6006
6667
|
this.stagingRoot
|
|
6007
6668
|
]) if (!inside$2(stateDirectory, child)) throw new Error("frp component path escaped its state directory");
|
|
6008
6669
|
this.fetchArtifact = options.fetchArtifact ?? defaultFetchArtifact$2;
|
|
6009
|
-
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$
|
|
6670
|
+
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$2;
|
|
6010
6671
|
this.inspectExecutable = options.inspectExecutable ?? defaultInspectExecutable;
|
|
6011
6672
|
}
|
|
6012
6673
|
/** Inspect the managed executable without relying on global FRP installations. */
|
|
@@ -6345,6 +7006,14 @@ function resolveFrpVhostHttpPort(settings) {
|
|
|
6345
7006
|
function resolveFrpPublicPort(settings) {
|
|
6346
7007
|
return settings.publicPort ?? 33080;
|
|
6347
7008
|
}
|
|
7009
|
+
/** Public HTTPS endpoint the user opens, independent of the frps control address. */
|
|
7010
|
+
function resolveFrpEntryProbe(settings) {
|
|
7011
|
+
const origin = new URL(frpEntryOrigin(settings));
|
|
7012
|
+
return {
|
|
7013
|
+
host: origin.hostname,
|
|
7014
|
+
port: origin.port === "" ? 443 : Number(origin.port)
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
6348
7017
|
/** HTTPS origin users actually open; the self-signed TCP entry is not on 443. */
|
|
6349
7018
|
function frpEntryOrigin(settings) {
|
|
6350
7019
|
if (!isFrpSelfSignedIngress(settings)) return settings.publicOrigin;
|
|
@@ -7127,7 +7796,8 @@ function parseManagedSetup(value) {
|
|
|
7127
7796
|
"networkInterface",
|
|
7128
7797
|
"listenPort",
|
|
7129
7798
|
"upstreamOrigin",
|
|
7130
|
-
"tls"
|
|
7799
|
+
"tls",
|
|
7800
|
+
"extraAllowedCidrs"
|
|
7131
7801
|
].includes(key))) throw new Error("mobile setup file has an unsupported format");
|
|
7132
7802
|
if (!Number.isSafeInteger(record.listenPort) || record.listenPort < 1024 || record.listenPort > 65535) throw new Error("mobile setup listenPort must be from 1024 through 65535");
|
|
7133
7803
|
if (typeof record.tls !== "object" || record.tls === null || Array.isArray(record.tls)) throw new Error("mobile setup tls must be an object");
|
|
@@ -7144,6 +7814,7 @@ function parseManagedSetup(value) {
|
|
|
7144
7814
|
networkInterface: requiredString(record.networkInterface, "mobile setup networkInterface"),
|
|
7145
7815
|
listenPort: record.listenPort,
|
|
7146
7816
|
upstreamOrigin: requiredString(record.upstreamOrigin, "mobile setup upstreamOrigin"),
|
|
7817
|
+
...record.extraAllowedCidrs === void 0 ? {} : { extraAllowedCidrs: parseAdditionalTrustedNetworks(record.extraAllowedCidrs) },
|
|
7147
7818
|
tls: Object.freeze({
|
|
7148
7819
|
mode: "managed",
|
|
7149
7820
|
caCertFile: requiredString(tls.caCertFile, "mobile setup tls.caCertFile"),
|
|
@@ -7153,6 +7824,43 @@ function parseManagedSetup(value) {
|
|
|
7153
7824
|
})
|
|
7154
7825
|
});
|
|
7155
7826
|
}
|
|
7827
|
+
/** Validate explicitly trusted networks without adding implicit VPN or private ranges.
|
|
7828
|
+
* @param value - CIDR strings submitted by the computer administrator.
|
|
7829
|
+
* @returns Deduplicated validated CIDRs in input order.
|
|
7830
|
+
*/
|
|
7831
|
+
function parseAdditionalTrustedNetworks(value) {
|
|
7832
|
+
if (!Array.isArray(value)) throw new Error("extraAllowedCidrs must be an array of CIDR strings");
|
|
7833
|
+
const sources = value.map((entry) => {
|
|
7834
|
+
if (typeof entry !== "string" || entry.trim() === "") throw new Error("extraAllowedCidrs must be an array of CIDR strings");
|
|
7835
|
+
return entry.trim();
|
|
7836
|
+
});
|
|
7837
|
+
const entries = [];
|
|
7838
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7839
|
+
for (const cidr of sources) {
|
|
7840
|
+
const parsed = parseCidr(cidr);
|
|
7841
|
+
const key = `${parsed.bits}:${parsed.network}:${parsed.prefix}`;
|
|
7842
|
+
if (seen.has(key)) continue;
|
|
7843
|
+
seen.add(key);
|
|
7844
|
+
entries.push(cidr);
|
|
7845
|
+
}
|
|
7846
|
+
return Object.freeze(entries);
|
|
7847
|
+
}
|
|
7848
|
+
/** Save extra trusted networks without replacing the interface or pairing identity.
|
|
7849
|
+
* @param setupFile - Existing managed setup file.
|
|
7850
|
+
* @param values - Explicitly selected additional networks.
|
|
7851
|
+
* @returns The saved managed setup.
|
|
7852
|
+
*/
|
|
7853
|
+
async function saveManagedTrustedNetworks(setupFile, values) {
|
|
7854
|
+
const extraAllowedCidrs = parseAdditionalTrustedNetworks(values);
|
|
7855
|
+
const setup = parseManagedSetup(JSON.parse(await readFile(setupFile, "utf8")));
|
|
7856
|
+
const next = Object.freeze({
|
|
7857
|
+
...setup,
|
|
7858
|
+
extraAllowedCidrs
|
|
7859
|
+
});
|
|
7860
|
+
await atomicWrite(setupFile, `${JSON.stringify(next, null, 2)}\n`);
|
|
7861
|
+
await restrictPrivateFile(setupFile);
|
|
7862
|
+
return next;
|
|
7863
|
+
}
|
|
7156
7864
|
function privateIpv4(value) {
|
|
7157
7865
|
const parts = value.split(".").map(Number);
|
|
7158
7866
|
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
|
|
@@ -7293,10 +8001,18 @@ async function atomicWrite(file, contents) {
|
|
|
7293
8001
|
recursive: true,
|
|
7294
8002
|
mode: 448
|
|
7295
8003
|
});
|
|
7296
|
-
const temporary = join(directory, `.${basename(file)}.${
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
8004
|
+
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
8005
|
+
try {
|
|
8006
|
+
await writeFile(temporary, contents, {
|
|
8007
|
+
mode: 384,
|
|
8008
|
+
flag: "wx"
|
|
8009
|
+
});
|
|
8010
|
+
await restrictPrivateFile(temporary);
|
|
8011
|
+
await rename(temporary, file);
|
|
8012
|
+
await restrictPrivateFile(file);
|
|
8013
|
+
} finally {
|
|
8014
|
+
await rm(temporary, { force: true });
|
|
8015
|
+
}
|
|
7300
8016
|
}
|
|
7301
8017
|
/** Create a long-lived CA or migrate the legacy self-signed server certificate as that CA. */
|
|
7302
8018
|
async function ensureManagedCa(setup, legacy) {
|
|
@@ -7435,7 +8151,7 @@ async function materializeManagedSetup(setup, table) {
|
|
|
7435
8151
|
return {
|
|
7436
8152
|
publicOrigin: `https://${network.address}:${String(setup.listenPort)}`,
|
|
7437
8153
|
listenHost: network.address,
|
|
7438
|
-
allowedCidrs: [network.cidr],
|
|
8154
|
+
allowedCidrs: parseAdditionalTrustedNetworks([network.cidr, ...setup.extraAllowedCidrs ?? []]),
|
|
7439
8155
|
instanceId: ca.fingerprint256.replaceAll(":", "").toLowerCase(),
|
|
7440
8156
|
pairingCaFile: setup.tls.caCertFile,
|
|
7441
8157
|
tls: {
|
|
@@ -7718,6 +8434,17 @@ var RemoteProviderCoordinator = class {
|
|
|
7718
8434
|
mutate(operation) {
|
|
7719
8435
|
return this.enqueue(() => operation(this.controller()));
|
|
7720
8436
|
}
|
|
8437
|
+
/** Apply one operation only while its intended provider remains selected.
|
|
8438
|
+
* @param provider - Provider the caller explicitly chose.
|
|
8439
|
+
* @param operation - Configuration and activation performed in the same queue item.
|
|
8440
|
+
* @returns The operation result, or a rejection when another selection won the queue.
|
|
8441
|
+
*/
|
|
8442
|
+
mutateSelected(provider, operation) {
|
|
8443
|
+
return this.enqueue(async () => {
|
|
8444
|
+
if (provider !== this.selectedValue) throw new Error("remote_provider_changed");
|
|
8445
|
+
return operation(this.controller());
|
|
8446
|
+
});
|
|
8447
|
+
}
|
|
7721
8448
|
/** Disable the previous provider, persist the new selection, and retain rollback on write failure. */
|
|
7722
8449
|
select(provider) {
|
|
7723
8450
|
return this.enqueue(async () => {
|
|
@@ -8909,6 +9636,10 @@ async function downloadPinnedArtifact(request) {
|
|
|
8909
9636
|
}
|
|
8910
9637
|
//#endregion
|
|
8911
9638
|
//#region src/cloudflared-component.ts
|
|
9639
|
+
/** Upper bound for `tar -t` output; a pinned release listing is a few lines. */
|
|
9640
|
+
const MAX_ARCHIVE_LIST_BYTES = 1048576;
|
|
9641
|
+
const MAX_ARCHIVE_ENTRIES = 256;
|
|
9642
|
+
const DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
8912
9643
|
const CLOUDFLARED_VERSION = "2026.9.1";
|
|
8913
9644
|
/** Pinned official cloudflared release metadata for supported desktop targets. */
|
|
8914
9645
|
const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
@@ -8919,7 +9650,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8919
9650
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
8920
9651
|
downloadBytes: 54976432,
|
|
8921
9652
|
downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
|
|
8922
|
-
executableName: "cloudflared.exe"
|
|
9653
|
+
executableName: "cloudflared.exe",
|
|
9654
|
+
executableBytes: 54976432,
|
|
9655
|
+
executableSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712"
|
|
8923
9656
|
},
|
|
8924
9657
|
{
|
|
8925
9658
|
version: CLOUDFLARED_VERSION,
|
|
@@ -8928,7 +9661,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8928
9661
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
8929
9662
|
downloadBytes: 39838488,
|
|
8930
9663
|
downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
|
|
8931
|
-
executableName: "cloudflared"
|
|
9664
|
+
executableName: "cloudflared",
|
|
9665
|
+
executableBytes: 39838488,
|
|
9666
|
+
executableSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc"
|
|
8932
9667
|
},
|
|
8933
9668
|
{
|
|
8934
9669
|
version: CLOUDFLARED_VERSION,
|
|
@@ -8937,7 +9672,39 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8937
9672
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
|
|
8938
9673
|
downloadBytes: 37466252,
|
|
8939
9674
|
downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
|
|
8940
|
-
executableName: "cloudflared"
|
|
9675
|
+
executableName: "cloudflared",
|
|
9676
|
+
executableBytes: 37466252,
|
|
9677
|
+
executableSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3"
|
|
9678
|
+
},
|
|
9679
|
+
{
|
|
9680
|
+
version: CLOUDFLARED_VERSION,
|
|
9681
|
+
platform: "darwin",
|
|
9682
|
+
arch: "x64",
|
|
9683
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64.tgz`,
|
|
9684
|
+
downloadBytes: 21118723,
|
|
9685
|
+
downloadSha256: "ff0d3b51d5ff70eceef89d6b32145fee985018a2174596a5dbe405e2766e2ac4",
|
|
9686
|
+
executableName: "cloudflared",
|
|
9687
|
+
executableBytes: 41731488,
|
|
9688
|
+
executableSha256: "1ea07ae775b03236bd6be18ca1848d6bdc4af2f4f3bce398823b5a36e5761b75",
|
|
9689
|
+
archive: {
|
|
9690
|
+
format: "tgz",
|
|
9691
|
+
member: "cloudflared"
|
|
9692
|
+
}
|
|
9693
|
+
},
|
|
9694
|
+
{
|
|
9695
|
+
version: CLOUDFLARED_VERSION,
|
|
9696
|
+
platform: "darwin",
|
|
9697
|
+
arch: "arm64",
|
|
9698
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-arm64.tgz`,
|
|
9699
|
+
downloadBytes: 19217478,
|
|
9700
|
+
downloadSha256: "c27ab8fd0aa489449e3d201eb02f957ef460a13b613662928b1b23394bf1bcfe",
|
|
9701
|
+
executableName: "cloudflared",
|
|
9702
|
+
executableBytes: 38893072,
|
|
9703
|
+
executableSha256: "9a0b19f67dc7a3011bc6b972c7ce06a5fcea8784ac6bd599ffa382ea4aeb5a6e",
|
|
9704
|
+
archive: {
|
|
9705
|
+
format: "tgz",
|
|
9706
|
+
member: "cloudflared"
|
|
9707
|
+
}
|
|
8941
9708
|
}
|
|
8942
9709
|
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
8943
9710
|
/**
|
|
@@ -8964,6 +9731,60 @@ async function regularFile$1(file, expectedBytes) {
|
|
|
8964
9731
|
throw error;
|
|
8965
9732
|
}
|
|
8966
9733
|
}
|
|
9734
|
+
async function runCapture(file, args) {
|
|
9735
|
+
return new Promise((resolveRun, reject) => {
|
|
9736
|
+
execFile(file, [...args], {
|
|
9737
|
+
windowsHide: true,
|
|
9738
|
+
timeout: 12e4,
|
|
9739
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
9740
|
+
encoding: "utf8"
|
|
9741
|
+
}, (error, stdout) => {
|
|
9742
|
+
if (error === null) resolveRun(stdout);
|
|
9743
|
+
else reject(error);
|
|
9744
|
+
});
|
|
9745
|
+
});
|
|
9746
|
+
}
|
|
9747
|
+
/** Reject absolute, traversing, or Windows-shaped archive members. */
|
|
9748
|
+
function validatedArchiveEntry(rawEntry) {
|
|
9749
|
+
if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("cloudflared_archive_path_invalid");
|
|
9750
|
+
const segments = rawEntry.replace(/\/$/u, "").split("/");
|
|
9751
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("cloudflared_archive_path_invalid");
|
|
9752
|
+
return segments;
|
|
9753
|
+
}
|
|
9754
|
+
/** Select exactly one archive member named `member`, wherever it sits in the tree. */
|
|
9755
|
+
function selectCloudflaredArchiveEntry(entries, member) {
|
|
9756
|
+
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("cloudflared_archive_entries_invalid");
|
|
9757
|
+
let selected;
|
|
9758
|
+
for (const entry of entries) if (validatedArchiveEntry(entry).at(-1) === member) {
|
|
9759
|
+
if (selected !== void 0) throw new Error("cloudflared_archive_executable_ambiguous");
|
|
9760
|
+
selected = entry.replace(/\/$/u, "");
|
|
9761
|
+
}
|
|
9762
|
+
if (selected === void 0) throw new Error("cloudflared_archive_executable_missing");
|
|
9763
|
+
return selected;
|
|
9764
|
+
}
|
|
9765
|
+
/**
|
|
9766
|
+
* Unpack the pinned member out of a `.tgz` using the system `tar`, which ships
|
|
9767
|
+
* with Windows 10+, macOS, and every supported Linux base.
|
|
9768
|
+
*/
|
|
9769
|
+
async function defaultExtractArtifact$1(archive, destination, member) {
|
|
9770
|
+
const tar = process.platform === "win32" ? "tar.exe" : "tar";
|
|
9771
|
+
const selected = selectCloudflaredArchiveEntry((await runCapture(tar, ["-tzf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), member);
|
|
9772
|
+
const unpacked = join(destination, "archive");
|
|
9773
|
+
await mkdir(unpacked, {
|
|
9774
|
+
recursive: true,
|
|
9775
|
+
mode: 448
|
|
9776
|
+
});
|
|
9777
|
+
await runCapture(tar, [
|
|
9778
|
+
"-xzf",
|
|
9779
|
+
archive,
|
|
9780
|
+
"-C",
|
|
9781
|
+
unpacked,
|
|
9782
|
+
selected
|
|
9783
|
+
]);
|
|
9784
|
+
const extracted = join(unpacked, ...validatedArchiveEntry(selected));
|
|
9785
|
+
if (!await regularFile$1(extracted)) throw new Error("cloudflared_archive_executable_invalid");
|
|
9786
|
+
await copyFile(extracted, join(destination, member));
|
|
9787
|
+
}
|
|
8967
9788
|
async function defaultFetchArtifact$1(url, signal, expectedBytes) {
|
|
8968
9789
|
return downloadPinnedArtifact({
|
|
8969
9790
|
url,
|
|
@@ -8988,6 +9809,7 @@ var CloudflaredComponentManager = class {
|
|
|
8988
9809
|
arch;
|
|
8989
9810
|
release;
|
|
8990
9811
|
fetchArtifact;
|
|
9812
|
+
extractArtifact;
|
|
8991
9813
|
installed = false;
|
|
8992
9814
|
errorCode;
|
|
8993
9815
|
queue = Promise.resolve();
|
|
@@ -9012,12 +9834,14 @@ var CloudflaredComponentManager = class {
|
|
|
9012
9834
|
]) if (!inside$1(stateDirectory, child)) throw new Error("cloudflared component path escaped its state directory");
|
|
9013
9835
|
const release = this.release;
|
|
9014
9836
|
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact$1(url, signal, release?.downloadBytes ?? 0));
|
|
9837
|
+
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$1;
|
|
9015
9838
|
}
|
|
9016
9839
|
/** Inspect the managed binary without using any global cloudflared state. */
|
|
9017
9840
|
async initialize() {
|
|
9018
9841
|
const release = this.release;
|
|
9019
|
-
this.
|
|
9020
|
-
|
|
9842
|
+
this.errorCode = void 0;
|
|
9843
|
+
this.installed = release !== void 0 && await regularFile$1(this.executable, release.executableBytes);
|
|
9844
|
+
if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.executableSha256) {
|
|
9021
9845
|
this.installed = false;
|
|
9022
9846
|
this.errorCode = "cloudflared_component_invalid";
|
|
9023
9847
|
}
|
|
@@ -9030,7 +9854,7 @@ var CloudflaredComponentManager = class {
|
|
|
9030
9854
|
installed: this.installed,
|
|
9031
9855
|
version: release.version,
|
|
9032
9856
|
downloadBytes: release.downloadBytes,
|
|
9033
|
-
installedBytes: release.
|
|
9857
|
+
installedBytes: release.executableBytes,
|
|
9034
9858
|
sourceUrl: release.downloadUrl,
|
|
9035
9859
|
downloadPage: DOWNLOAD_PAGE$1,
|
|
9036
9860
|
termsUrl: TERMS_URL$1,
|
|
@@ -9052,7 +9876,7 @@ var CloudflaredComponentManager = class {
|
|
|
9052
9876
|
const controller = new AbortController();
|
|
9053
9877
|
const timeout = setTimeout(() => {
|
|
9054
9878
|
controller.abort();
|
|
9055
|
-
},
|
|
9879
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
9056
9880
|
timeout.unref();
|
|
9057
9881
|
let bytes;
|
|
9058
9882
|
try {
|
|
@@ -9063,12 +9887,24 @@ var CloudflaredComponentManager = class {
|
|
|
9063
9887
|
if (bytes.byteLength !== release.downloadBytes) throw new Error("cloudflared_download_size_mismatch");
|
|
9064
9888
|
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cloudflared_download_hash_mismatch");
|
|
9065
9889
|
const staged = join(staging, release.executableName);
|
|
9066
|
-
await writeFile(staged, bytes, {
|
|
9890
|
+
if (release.archive === void 0) await writeFile(staged, bytes, {
|
|
9067
9891
|
flag: "wx",
|
|
9068
9892
|
mode: 384
|
|
9069
9893
|
});
|
|
9894
|
+
else {
|
|
9895
|
+
const downloaded = join(staging, `${release.executableName}.download`);
|
|
9896
|
+
await writeFile(downloaded, bytes, {
|
|
9897
|
+
flag: "wx",
|
|
9898
|
+
mode: 384
|
|
9899
|
+
});
|
|
9900
|
+
try {
|
|
9901
|
+
await this.extractArtifact(downloaded, staging, release.archive.member);
|
|
9902
|
+
} finally {
|
|
9903
|
+
await rm(downloaded, { force: true });
|
|
9904
|
+
}
|
|
9905
|
+
}
|
|
9070
9906
|
await chmod(staged, 448);
|
|
9071
|
-
if (!await regularFile$1(staged, release.
|
|
9907
|
+
if (!await regularFile$1(staged, release.executableBytes) || await sha256$1(staged) !== release.executableSha256) throw new Error("cloudflared_executable_hash_mismatch");
|
|
9072
9908
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
9073
9909
|
await mkdir(candidate, {
|
|
9074
9910
|
recursive: true,
|
|
@@ -9643,7 +10479,7 @@ var CloudflaredController = class {
|
|
|
9643
10479
|
};
|
|
9644
10480
|
//#endregion
|
|
9645
10481
|
//#region src/cloudflared-tunnel.ts
|
|
9646
|
-
const MAX_CONFIG_BYTES = 8192;
|
|
10482
|
+
const MAX_CONFIG_BYTES$1 = 8192;
|
|
9647
10483
|
const QUICK_TUNNEL_SUFFIX = ".trycloudflare.com";
|
|
9648
10484
|
/** Cloudflare's own tunnel-routing domain; it can never be a public hostname. */
|
|
9649
10485
|
const TUNNEL_ROUTING_SUFFIX = ".cfargotunnel.com";
|
|
@@ -9802,7 +10638,7 @@ var CloudflaredTunnelStore = class {
|
|
|
9802
10638
|
if (error.code === "ENOENT") return;
|
|
9803
10639
|
throw error;
|
|
9804
10640
|
}
|
|
9805
|
-
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_CONFIG_BYTES) {
|
|
10641
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_CONFIG_BYTES$1) {
|
|
9806
10642
|
this.errorCode = "cloudflared_tunnel_config_invalid";
|
|
9807
10643
|
return;
|
|
9808
10644
|
}
|
|
@@ -10440,6 +11276,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
10440
11276
|
- 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
|
|
10441
11277
|
- extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
|
|
10442
11278
|
- host.mjs:电脑端 Node.js 代码(可信本地代码,可读写文件、执行命令)。导出默认函数 (api) => { ... },用 api.action('名称', { input, run }) 注册动作、api.route({ method, path, handle }) 注册路由、api.effect(fn) 注册清理。input 可以直接使用 api.schema.object(...) 等 Schemastery schema,也可以传入带 parse(value) 的适配器;两种形式都会在动作执行前校验并规范化输入。
|
|
11279
|
+
- 动作与路由的执行有时间预算:默认 30 秒(输入校验也计入),可用 api.action('名称', { input, run, timeoutMs }) 或 api.route({ ..., timeoutMs }) 按操作覆盖(1–300000 的整数毫秒)。超时或调用方取消后,调用方收到 500 extension_action_timeout / extension_route_timeout(或取消原因);这只表示调用方不再等待,底层代码可能仍在运行并已产生副作用,请在 run / handle 中响应 signal 实现协作式取消。路由的预算到 handle() 返回响应为止,不影响已返回流的存活。若某扩展存在调用方已离开(超时或取消)但仍未结束的操作,后续对该扩展的新调用会收到 503 extension_busy,直到该操作真正结束。
|
|
10443
11280
|
- mobile.js:手机端脚本,用 window.dshMobile.define({ apiVersion:1, id:'<id>', activate(api) { ... } }),activate 返回清理函数
|
|
10444
11281
|
- mobile.css:手机端样式(可选)
|
|
10445
11282
|
- assets/:手机端静态资源(可选)
|
|
@@ -10448,6 +11285,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
10448
11285
|
|
|
10449
11286
|
安全约束:
|
|
10450
11287
|
- host.mjs 拥有电脑用户的完整权限,绝不能放入不可信代码,也不要让手机端无条件执行任意命令
|
|
11288
|
+
- host.mjs 的同步阻塞(如 while (true) {} 或长时间同步循环)会卡死整个电脑端网关进程,超时与取消机制都无法打断;绝不编写同步阻塞代码
|
|
10451
11289
|
- 所有改动只限 $DSH_HOME/mobile-access/,不要动 DeepSeek Harness 源码
|
|
10452
11290
|
|
|
10453
11291
|
完成前请自检:
|
|
@@ -12631,139 +13469,1098 @@ var CpolarComponentManager = class {
|
|
|
12631
13469
|
}
|
|
12632
13470
|
};
|
|
12633
13471
|
//#endregion
|
|
12634
|
-
//#region src/
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
const
|
|
12638
|
-
const
|
|
12639
|
-
const
|
|
12640
|
-
|
|
12641
|
-
const
|
|
12642
|
-
|
|
12643
|
-
const
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
const
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
const core = [
|
|
12655
|
-
Number(match[1]),
|
|
12656
|
-
Number(match[2]),
|
|
12657
|
-
Number(match[3])
|
|
12658
|
-
];
|
|
12659
|
-
if (core.some((part) => !Number.isSafeInteger(part))) return void 0;
|
|
12660
|
-
const prerelease = match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/u.test(part) ? Number(part) : part);
|
|
12661
|
-
if (prerelease.some((part) => typeof part === "number" && !Number.isSafeInteger(part))) return void 0;
|
|
12662
|
-
return Object.freeze({
|
|
12663
|
-
core,
|
|
12664
|
-
prerelease: Object.freeze(prerelease)
|
|
12665
|
-
});
|
|
12666
|
-
}
|
|
12667
|
-
/** Compare two strict SemVer strings, including prerelease precedence. */
|
|
12668
|
-
function comparePluginVersions(left, right) {
|
|
12669
|
-
const a = parseSemver(left);
|
|
12670
|
-
const b = parseSemver(right);
|
|
12671
|
-
if (a === void 0 || b === void 0) return void 0;
|
|
12672
|
-
for (let index = 0; index < a.core.length; index += 1) {
|
|
12673
|
-
const difference = a.core[index] - b.core[index];
|
|
12674
|
-
if (difference !== 0) return Math.sign(difference);
|
|
12675
|
-
}
|
|
12676
|
-
if (a.prerelease.length === 0 || b.prerelease.length === 0) return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
12677
|
-
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
12678
|
-
for (let index = 0; index < length; index += 1) {
|
|
12679
|
-
const leftPart = a.prerelease[index];
|
|
12680
|
-
const rightPart = b.prerelease[index];
|
|
12681
|
-
if (leftPart === void 0 || rightPart === void 0) return leftPart === void 0 ? -1 : 1;
|
|
12682
|
-
if (leftPart === rightPart) continue;
|
|
12683
|
-
if (typeof leftPart === "number" && typeof rightPart === "number") return Math.sign(leftPart - rightPart);
|
|
12684
|
-
if (typeof leftPart === "number") return -1;
|
|
12685
|
-
if (typeof rightPart === "number") return 1;
|
|
12686
|
-
return leftPart < rightPart ? -1 : 1;
|
|
12687
|
-
}
|
|
12688
|
-
return 0;
|
|
12689
|
-
}
|
|
12690
|
-
function isComparatorSet(value) {
|
|
12691
|
-
if (HYPHEN_RANGE.test(value)) return true;
|
|
12692
|
-
const comparators = value.replace(/(<=|>=|<|>|=|~|\^) +/gu, "$1").split(/ +/u);
|
|
12693
|
-
return comparators.length > 0 && comparators.every((comparator) => COMPARATOR.test(comparator));
|
|
12694
|
-
}
|
|
12695
|
-
function isNpmVersionRange(value) {
|
|
12696
|
-
if (!/^[0-9xX*<>=~^|.+\- ]+$/u.test(value)) return false;
|
|
12697
|
-
const alternatives = value.split(/ *\|\| */u);
|
|
12698
|
-
return alternatives.length > 0 && alternatives.every((alternative) => alternative !== "" && isComparatorSet(alternative));
|
|
12699
|
-
}
|
|
12700
|
-
/** Return whether pnpm may safely replace this profile dependency from an npm version, range, or tag. */
|
|
12701
|
-
function isRegistryPluginSpec(value) {
|
|
12702
|
-
if (typeof value !== "string" || value.trim() !== value || value === "" || /[\u0000-\u001f\u007f]/u.test(value)) return false;
|
|
12703
|
-
if (/\.(?:tgz|tar(?:\.gz)?)$/iu.test(value)) return false;
|
|
12704
|
-
return parseSemver(value) !== void 0 || isNpmVersionRange(value) || DIST_TAG.test(value);
|
|
12705
|
-
}
|
|
12706
|
-
/** Resolve the DSH profile named by the current launcher arguments. */
|
|
12707
|
-
function launchedProfileName(argv) {
|
|
12708
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
12709
|
-
if (argv[index] === "--profile") {
|
|
12710
|
-
const candidate = argv[index + 1];
|
|
12711
|
-
if (candidate !== void 0 && /^[\w.-]+$/u.test(candidate)) return candidate;
|
|
13472
|
+
//#region src/caddy-files.ts
|
|
13473
|
+
/** Refuse link-shaped ancestors within the owned state root before filesystem operations. */
|
|
13474
|
+
async function assertCaddyParents(root, target) {
|
|
13475
|
+
const parent = resolve(root);
|
|
13476
|
+
const candidate = resolve(target);
|
|
13477
|
+
const offset = relative(parent, candidate);
|
|
13478
|
+
if (!isAbsolute(root) || offset !== "" && (offset.startsWith("..") || isAbsolute(offset))) throw new Error("caddy_path_invalid");
|
|
13479
|
+
const paths = [parent];
|
|
13480
|
+
let cursor = parent;
|
|
13481
|
+
for (const segment of relative(parent, dirname(candidate)).split(/[\\/]/u).filter(Boolean)) {
|
|
13482
|
+
cursor = join(cursor, segment);
|
|
13483
|
+
paths.push(cursor);
|
|
13484
|
+
}
|
|
13485
|
+
for (const path of paths) {
|
|
13486
|
+
let entry;
|
|
13487
|
+
try {
|
|
13488
|
+
entry = await lstat(path);
|
|
13489
|
+
} catch (error) {
|
|
13490
|
+
if (error.code === "ENOENT") continue;
|
|
13491
|
+
throw error;
|
|
12712
13492
|
}
|
|
12713
|
-
|
|
12714
|
-
if (match?.[1] !== void 0) return match[1];
|
|
13493
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
12715
13494
|
}
|
|
12716
|
-
return "web";
|
|
12717
13495
|
}
|
|
12718
|
-
/**
|
|
12719
|
-
function
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
13496
|
+
/** Create a private real directory, never traversing a link owned by the component. */
|
|
13497
|
+
async function ensureCaddyDirectory(root, directory) {
|
|
13498
|
+
await assertCaddyParents(root, directory);
|
|
13499
|
+
await mkdir(directory, {
|
|
13500
|
+
recursive: true,
|
|
13501
|
+
mode: 448
|
|
13502
|
+
});
|
|
13503
|
+
const entry = await lstat(directory);
|
|
13504
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
13505
|
+
await restrictPrivateFile(directory, 448);
|
|
12725
13506
|
}
|
|
12726
|
-
|
|
13507
|
+
/** Atomically replace one private configuration file under a real owned directory. */
|
|
13508
|
+
async function writeCaddyPrivateFile(root, file, body) {
|
|
13509
|
+
await ensureCaddyDirectory(root, dirname(file));
|
|
12727
13510
|
try {
|
|
12728
|
-
const
|
|
12729
|
-
|
|
12730
|
-
} catch {
|
|
12731
|
-
|
|
13511
|
+
const entry = await lstat(file);
|
|
13512
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("caddy_config_target_invalid");
|
|
13513
|
+
} catch (error) {
|
|
13514
|
+
if (error.code !== "ENOENT") throw error;
|
|
13515
|
+
}
|
|
13516
|
+
const temporary = join(dirname(file), "." + basename(file) + "." + randomBytes(12).toString("hex") + ".tmp");
|
|
13517
|
+
try {
|
|
13518
|
+
await writeFile(temporary, body, {
|
|
13519
|
+
encoding: "utf8",
|
|
13520
|
+
flag: "wx",
|
|
13521
|
+
mode: 384
|
|
13522
|
+
});
|
|
13523
|
+
await restrictPrivateFile(temporary);
|
|
13524
|
+
await rename(temporary, file);
|
|
13525
|
+
} finally {
|
|
13526
|
+
await rm(temporary, { force: true });
|
|
12732
13527
|
}
|
|
12733
13528
|
}
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
"user-agent": "dsh-mobile-release-check"
|
|
12739
|
-
},
|
|
12740
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
12741
|
-
});
|
|
12742
|
-
if (!response.ok) return void 0;
|
|
12743
|
-
const payload = await response.json();
|
|
12744
|
-
return typeof payload.version === "string" && parseSemver(payload.version) !== void 0 ? payload.version : void 0;
|
|
12745
|
-
}
|
|
12746
|
-
function githubReleaseVersion(location, responseUrl) {
|
|
12747
|
-
let url;
|
|
13529
|
+
/** Delete only owned entries; junctions and symlinks are unlinked, never descended into. */
|
|
13530
|
+
async function removeCaddyTree(root, target) {
|
|
13531
|
+
await assertCaddyParents(root, target);
|
|
13532
|
+
let entry;
|
|
12748
13533
|
try {
|
|
12749
|
-
|
|
12750
|
-
} catch {
|
|
13534
|
+
entry = await lstat(target);
|
|
13535
|
+
} catch (error) {
|
|
13536
|
+
if (error.code === "ENOENT") return;
|
|
13537
|
+
throw error;
|
|
13538
|
+
}
|
|
13539
|
+
if (entry.isSymbolicLink()) {
|
|
13540
|
+
await unlink(target);
|
|
12751
13541
|
return;
|
|
12752
13542
|
}
|
|
12753
|
-
if (
|
|
12754
|
-
|
|
12755
|
-
let version;
|
|
12756
|
-
try {
|
|
12757
|
-
version = decodeURIComponent(url.pathname.slice(34));
|
|
12758
|
-
} catch {
|
|
13543
|
+
if (!entry.isDirectory()) {
|
|
13544
|
+
await unlink(target);
|
|
12759
13545
|
return;
|
|
12760
13546
|
}
|
|
12761
|
-
|
|
13547
|
+
for (const name of await readdir(target)) await removeCaddyTree(root, join(target, name));
|
|
13548
|
+
await rmdir(target);
|
|
12762
13549
|
}
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
13550
|
+
//#endregion
|
|
13551
|
+
//#region src/caddy-component.ts
|
|
13552
|
+
const CADDY_VERSION = "2.11.6";
|
|
13553
|
+
const CADDY_DNS_PLUGIN_VERSION = "v0.4.3";
|
|
13554
|
+
const CADDY_DOWNLOAD_PAGE = "https://caddyserver.com/download";
|
|
13555
|
+
const CADDY_COMPONENT_RELEASES = Object.freeze({});
|
|
13556
|
+
Object.freeze({
|
|
13557
|
+
version: CADDY_VERSION,
|
|
13558
|
+
dnsPluginVersion: CADDY_DNS_PLUGIN_VERSION
|
|
13559
|
+
});
|
|
13560
|
+
function isCaddyDnsProvider(value) {
|
|
13561
|
+
return value === "tencentcloud";
|
|
13562
|
+
}
|
|
13563
|
+
async function digest(file) {
|
|
13564
|
+
return createHash("sha256").update(await readFile(file)).digest("hex");
|
|
13565
|
+
}
|
|
13566
|
+
/** Minimal process environment for version/module checks, without ambient API credentials or proxy overrides. */
|
|
13567
|
+
function caddyInspectionEnvironment(environment = process.env) {
|
|
13568
|
+
const result = {};
|
|
13569
|
+
for (const name of [
|
|
13570
|
+
"SystemRoot",
|
|
13571
|
+
"WINDIR",
|
|
13572
|
+
"TEMP",
|
|
13573
|
+
"TMP",
|
|
13574
|
+
"LANG",
|
|
13575
|
+
"LC_ALL"
|
|
13576
|
+
]) if (environment[name] !== void 0) result[name] = environment[name];
|
|
13577
|
+
return result;
|
|
13578
|
+
}
|
|
13579
|
+
async function inspect(executable) {
|
|
13580
|
+
const options = {
|
|
13581
|
+
env: caddyInspectionEnvironment(),
|
|
13582
|
+
timeout: 15e3,
|
|
13583
|
+
maxBuffer: 262144
|
|
13584
|
+
};
|
|
13585
|
+
return {
|
|
13586
|
+
version: (await execFileText(executable, ["version"], options)).stdout.trim(),
|
|
13587
|
+
modules: (await execFileText(executable, ["list-modules", "--versions"], options)).stdout
|
|
13588
|
+
};
|
|
13589
|
+
}
|
|
13590
|
+
/** Owns only the optional pinned executable; runtime configuration belongs to CaddyConfigStore. */
|
|
13591
|
+
var CaddyComponentManager = class {
|
|
13592
|
+
executable;
|
|
13593
|
+
componentRoot;
|
|
13594
|
+
componentStorage;
|
|
13595
|
+
logRoot;
|
|
13596
|
+
stateDirectory;
|
|
13597
|
+
stagingRoot;
|
|
13598
|
+
artifact;
|
|
13599
|
+
fetchArtifact;
|
|
13600
|
+
inspectExecutable;
|
|
13601
|
+
promote;
|
|
13602
|
+
installed = false;
|
|
13603
|
+
errorCode;
|
|
13604
|
+
queue = Promise.resolve();
|
|
13605
|
+
constructor(options) {
|
|
13606
|
+
if (!isAbsolute(options.stateDirectory)) throw new Error("caddy state directory must be absolute");
|
|
13607
|
+
this.stateDirectory = resolve(options.stateDirectory);
|
|
13608
|
+
const platform = options.platform ?? process.platform;
|
|
13609
|
+
const arch = options.arch ?? process.arch;
|
|
13610
|
+
const artifact = options.artifact ?? CADDY_COMPONENT_RELEASES[`${platform}-${arch}`];
|
|
13611
|
+
if (artifact !== void 0 && (artifact.platform !== platform || artifact.arch !== arch || !/^https:\/\//u.test(artifact.downloadUrl) || !/^[a-f0-9]{64}$/u.test(artifact.downloadSha256) || !/^[a-f0-9]{64}$/u.test(artifact.executableSha256) || artifact.downloadBytes < 1 || artifact.executableBytes < 1 || !/^[a-z0-9.-]+$/u.test(artifact.executableName))) throw new Error("caddy_artifact_invalid");
|
|
13612
|
+
this.artifact = artifact;
|
|
13613
|
+
this.componentRoot = join(this.stateDirectory, "components", "caddy");
|
|
13614
|
+
this.componentStorage = join(this.componentRoot, artifact?.version ?? "2.11.6");
|
|
13615
|
+
this.executable = join(this.componentStorage, artifact?.executableName ?? (platform === "win32" ? "caddy.exe" : "caddy"));
|
|
13616
|
+
this.logRoot = join(this.stateDirectory, "caddy", "logs");
|
|
13617
|
+
this.stagingRoot = join(this.stateDirectory, "staging", "caddy");
|
|
13618
|
+
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => downloadPinnedArtifact({
|
|
13619
|
+
url,
|
|
13620
|
+
signal,
|
|
13621
|
+
expectedBytes: artifact?.downloadBytes ?? 0,
|
|
13622
|
+
errorPrefix: "caddy"
|
|
13623
|
+
}));
|
|
13624
|
+
this.inspectExecutable = options.inspectExecutable ?? inspect;
|
|
13625
|
+
this.promote = options.promoteDirectory ?? rename;
|
|
13626
|
+
}
|
|
13627
|
+
/** Recheck a pinned executable before launching; no unverified local binary can run. */
|
|
13628
|
+
async ensureExecutable() {
|
|
13629
|
+
const artifact = this.artifact;
|
|
13630
|
+
if (artifact === void 0) throw new Error("caddy_component_unavailable");
|
|
13631
|
+
await assertCaddyParents(this.stateDirectory, this.executable);
|
|
13632
|
+
const entry = await lstat(this.executable);
|
|
13633
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size !== artifact.executableBytes || await digest(this.executable) !== artifact.executableSha256) throw new Error("caddy_component_invalid");
|
|
13634
|
+
await this.verifyMetadata(this.executable, artifact);
|
|
13635
|
+
}
|
|
13636
|
+
async initialize() {
|
|
13637
|
+
this.installed = false;
|
|
13638
|
+
this.errorCode = void 0;
|
|
13639
|
+
if (this.artifact === void 0) {
|
|
13640
|
+
this.errorCode = "caddy_component_unavailable";
|
|
13641
|
+
return;
|
|
13642
|
+
}
|
|
13643
|
+
try {
|
|
13644
|
+
await this.ensureExecutable();
|
|
13645
|
+
this.installed = true;
|
|
13646
|
+
} catch (error) {
|
|
13647
|
+
if (error.code !== "ENOENT") this.errorCode = "caddy_component_invalid";
|
|
13648
|
+
}
|
|
13649
|
+
}
|
|
13650
|
+
status() {
|
|
13651
|
+
const artifact = this.artifact;
|
|
13652
|
+
return Object.freeze({
|
|
13653
|
+
supported: artifact !== void 0,
|
|
13654
|
+
installed: this.installed,
|
|
13655
|
+
version: artifact?.version ?? "2.11.6",
|
|
13656
|
+
downloadBytes: artifact?.downloadBytes ?? 0,
|
|
13657
|
+
installedBytes: this.installed ? artifact?.executableBytes ?? 0 : 0,
|
|
13658
|
+
sourceUrl: artifact?.downloadUrl ?? "https://caddyserver.com/download",
|
|
13659
|
+
downloadPage: CADDY_DOWNLOAD_PAGE,
|
|
13660
|
+
storagePath: this.componentRoot,
|
|
13661
|
+
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
13662
|
+
});
|
|
13663
|
+
}
|
|
13664
|
+
/** Explicit installation verifies bytes and reported modules before preserving-old promotion. */
|
|
13665
|
+
install() {
|
|
13666
|
+
return this.enqueue(async () => {
|
|
13667
|
+
const artifact = this.artifact;
|
|
13668
|
+
if (artifact === void 0) throw new Error("caddy_component_unavailable");
|
|
13669
|
+
await ensureCaddyDirectory(this.stateDirectory, this.stagingRoot);
|
|
13670
|
+
await ensureCaddyDirectory(this.stateDirectory, this.componentRoot);
|
|
13671
|
+
const staging = await mkdtemp(join(this.stagingRoot, "install-"));
|
|
13672
|
+
const candidate = join(this.componentRoot, ".install-" + randomBytes(12).toString("hex"));
|
|
13673
|
+
const backup = join(this.componentRoot, ".previous-" + randomBytes(12).toString("hex"));
|
|
13674
|
+
let previous = false;
|
|
13675
|
+
let promoted = false;
|
|
13676
|
+
try {
|
|
13677
|
+
const controller = new AbortController();
|
|
13678
|
+
const timeout = setTimeout(() => {
|
|
13679
|
+
controller.abort();
|
|
13680
|
+
}, 6e5);
|
|
13681
|
+
timeout.unref();
|
|
13682
|
+
let bytes;
|
|
13683
|
+
try {
|
|
13684
|
+
bytes = await this.fetchArtifact(artifact.downloadUrl, controller.signal);
|
|
13685
|
+
} finally {
|
|
13686
|
+
clearTimeout(timeout);
|
|
13687
|
+
}
|
|
13688
|
+
if (bytes.byteLength !== artifact.downloadBytes) throw new Error("caddy_download_size_mismatch");
|
|
13689
|
+
if (createHash("sha256").update(bytes).digest("hex") !== artifact.downloadSha256) throw new Error("caddy_download_hash_mismatch");
|
|
13690
|
+
await ensureCaddyDirectory(this.stateDirectory, candidate);
|
|
13691
|
+
const executable = join(candidate, artifact.executableName);
|
|
13692
|
+
await writeFile(executable, bytes, {
|
|
13693
|
+
flag: "wx",
|
|
13694
|
+
mode: 448
|
|
13695
|
+
});
|
|
13696
|
+
await chmod(executable, 448);
|
|
13697
|
+
if (bytes.byteLength !== artifact.executableBytes || await digest(executable) !== artifact.executableSha256) throw new Error("caddy_executable_hash_mismatch");
|
|
13698
|
+
await this.verifyMetadata(executable, artifact);
|
|
13699
|
+
try {
|
|
13700
|
+
const entry = await lstat(this.componentStorage);
|
|
13701
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
13702
|
+
await rename(this.componentStorage, backup);
|
|
13703
|
+
previous = true;
|
|
13704
|
+
} catch (error) {
|
|
13705
|
+
if (error.code !== "ENOENT") throw error;
|
|
13706
|
+
}
|
|
13707
|
+
try {
|
|
13708
|
+
await this.promote(candidate, this.componentStorage);
|
|
13709
|
+
promoted = true;
|
|
13710
|
+
} catch (error) {
|
|
13711
|
+
if (previous) {
|
|
13712
|
+
await rename(backup, this.componentStorage);
|
|
13713
|
+
previous = false;
|
|
13714
|
+
}
|
|
13715
|
+
throw error;
|
|
13716
|
+
}
|
|
13717
|
+
this.installed = true;
|
|
13718
|
+
this.errorCode = void 0;
|
|
13719
|
+
} finally {
|
|
13720
|
+
await removeCaddyTree(this.stateDirectory, staging);
|
|
13721
|
+
await removeCaddyTree(this.stateDirectory, candidate);
|
|
13722
|
+
if (previous && promoted) await removeCaddyTree(this.stateDirectory, backup);
|
|
13723
|
+
}
|
|
13724
|
+
});
|
|
13725
|
+
}
|
|
13726
|
+
/** Remove optional component files; callers stop the controller and purge runtime configuration separately. */
|
|
13727
|
+
purge() {
|
|
13728
|
+
return this.enqueue(async () => {
|
|
13729
|
+
await removeCaddyTree(this.stateDirectory, this.componentRoot);
|
|
13730
|
+
await removeCaddyTree(this.stateDirectory, this.stagingRoot);
|
|
13731
|
+
this.installed = false;
|
|
13732
|
+
this.errorCode = this.artifact === void 0 ? "caddy_component_unavailable" : void 0;
|
|
13733
|
+
});
|
|
13734
|
+
}
|
|
13735
|
+
async verifyMetadata(executable, artifact) {
|
|
13736
|
+
const info = await this.inspectExecutable(executable);
|
|
13737
|
+
if (!info.version.startsWith("v" + artifact.version + " ")) throw new Error("caddy_executable_version_mismatch");
|
|
13738
|
+
const nonstandard = info.modules.split(/\r?\n/u).filter((line) => line.startsWith("dns.providers."));
|
|
13739
|
+
if (nonstandard.length !== 1 || nonstandard[0] !== "dns.providers.tencentcloud " + artifact.dnsPluginVersion || !info.modules.includes("Non-standard modules: 1")) throw new Error("caddy_executable_modules_mismatch");
|
|
13740
|
+
}
|
|
13741
|
+
enqueue(operation) {
|
|
13742
|
+
const task = this.queue.then(operation, operation);
|
|
13743
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
13744
|
+
return task.then(() => this.status());
|
|
13745
|
+
}
|
|
13746
|
+
};
|
|
13747
|
+
const MAX_CONFIG_BYTES = 16384;
|
|
13748
|
+
/** Inject only the DNS credentials needed by the managed child. */
|
|
13749
|
+
function caddyCredentialEnvironment(provider, credentials) {
|
|
13750
|
+
if (!isCaddyDnsProvider(provider)) throw new Error("caddy_dns_provider_invalid");
|
|
13751
|
+
return {
|
|
13752
|
+
TENCENTCLOUD_SECRET_ID: credentials.secretId,
|
|
13753
|
+
TENCENTCLOUD_SECRET_KEY: credentials.secretKey
|
|
13754
|
+
};
|
|
13755
|
+
}
|
|
13756
|
+
/** Validate saved or administrative settings, including ports reserved by DSH. */
|
|
13757
|
+
function parseCaddySettings(value) {
|
|
13758
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("caddy_settings_invalid");
|
|
13759
|
+
const record = value;
|
|
13760
|
+
if (Reflect.ownKeys(record).some((key) => ![
|
|
13761
|
+
"version",
|
|
13762
|
+
"publicOrigin",
|
|
13763
|
+
"dnsProvider",
|
|
13764
|
+
"listenPort"
|
|
13765
|
+
].includes(String(key))) || record.version !== void 0 && record.version !== 1) throw new Error("caddy_settings_invalid");
|
|
13766
|
+
if (!isCaddyDnsProvider(record.dnsProvider)) throw new Error("caddy_dns_provider_invalid");
|
|
13767
|
+
const listenPort = record.listenPort ?? 8443;
|
|
13768
|
+
if (!Number.isSafeInteger(listenPort) || Number(listenPort) < 1024 || Number(listenPort) > 65535) throw new Error("caddy_listen_port_invalid");
|
|
13769
|
+
if ([
|
|
13770
|
+
3444,
|
|
13771
|
+
3443,
|
|
13772
|
+
3080
|
|
13773
|
+
].includes(Number(listenPort))) throw new Error("caddy_listen_port_reserved");
|
|
13774
|
+
const publicOrigin = validateOriginPublicOrigin(record.publicOrigin);
|
|
13775
|
+
if (isIP(new URL(publicOrigin).hostname) !== 0) throw new Error("caddy_domain_required");
|
|
13776
|
+
return Object.freeze({
|
|
13777
|
+
version: 1,
|
|
13778
|
+
publicOrigin,
|
|
13779
|
+
dnsProvider: record.dnsProvider,
|
|
13780
|
+
listenPort: Number(listenPort)
|
|
13781
|
+
});
|
|
13782
|
+
}
|
|
13783
|
+
function parseCredentials(value) {
|
|
13784
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("caddy_credentials_invalid");
|
|
13785
|
+
const record = value;
|
|
13786
|
+
if (Reflect.ownKeys(record).some((key) => key !== "secretId" && key !== "secretKey")) throw new Error("caddy_credentials_invalid");
|
|
13787
|
+
const { secretId, secretKey } = record;
|
|
13788
|
+
if (typeof secretId !== "string" || typeof secretKey !== "string" || secretId.length === 0 || secretKey.length === 0 || secretId.length > 512 || secretKey.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(secretId + secretKey)) throw new Error("caddy_credentials_invalid");
|
|
13789
|
+
return Object.freeze({
|
|
13790
|
+
secretId,
|
|
13791
|
+
secretKey
|
|
13792
|
+
});
|
|
13793
|
+
}
|
|
13794
|
+
/** Render a single TLS site, with no admin API, config autosave, or embedded DNS secrets. */
|
|
13795
|
+
function renderCaddyfile(settings, stateDirectory, backendPort = 3444) {
|
|
13796
|
+
const validated = parseCaddySettings(settings);
|
|
13797
|
+
if (!Number.isSafeInteger(backendPort) || backendPort < 1 || backendPort > 65535) throw new Error("caddy_backend_port_invalid");
|
|
13798
|
+
const root = resolve(stateDirectory, "caddy");
|
|
13799
|
+
const dataDirectory = join(root, "data").replaceAll("\\", "/");
|
|
13800
|
+
const hostname = new URL(validated.publicOrigin).hostname;
|
|
13801
|
+
return [
|
|
13802
|
+
"{",
|
|
13803
|
+
" admin off",
|
|
13804
|
+
" persist_config off",
|
|
13805
|
+
" auto_https disable_redirects",
|
|
13806
|
+
`\tstorage file_system ${JSON.stringify(dataDirectory)}`,
|
|
13807
|
+
" log {",
|
|
13808
|
+
" output discard",
|
|
13809
|
+
" }",
|
|
13810
|
+
"}",
|
|
13811
|
+
"",
|
|
13812
|
+
`https://${hostname}:${String(validated.listenPort)} {`,
|
|
13813
|
+
" tls {",
|
|
13814
|
+
" dns tencentcloud {",
|
|
13815
|
+
" secret_id {env.TENCENTCLOUD_SECRET_ID}",
|
|
13816
|
+
" secret_key {env.TENCENTCLOUD_SECRET_KEY}",
|
|
13817
|
+
" }",
|
|
13818
|
+
" }",
|
|
13819
|
+
`\treverse_proxy 127.0.0.1:${String(backendPort)} {`,
|
|
13820
|
+
" header_up Host {http.request.hostport}",
|
|
13821
|
+
" transport http {",
|
|
13822
|
+
" keepalive off",
|
|
13823
|
+
" dial_timeout 5s",
|
|
13824
|
+
" }",
|
|
13825
|
+
" }",
|
|
13826
|
+
"}",
|
|
13827
|
+
""
|
|
13828
|
+
].join("\n");
|
|
13829
|
+
}
|
|
13830
|
+
/** Owns one atomic configuration record and every Caddy-created private file. */
|
|
13831
|
+
var CaddyConfigStore = class {
|
|
13832
|
+
stateDirectory;
|
|
13833
|
+
rootDirectory;
|
|
13834
|
+
settingsFile;
|
|
13835
|
+
envFile;
|
|
13836
|
+
caddyfile;
|
|
13837
|
+
dataDirectory;
|
|
13838
|
+
configDirectory;
|
|
13839
|
+
logsDirectory;
|
|
13840
|
+
settingsValue;
|
|
13841
|
+
credentialsValue;
|
|
13842
|
+
errorCodeValue;
|
|
13843
|
+
queue = Promise.resolve();
|
|
13844
|
+
constructor(stateDirectory) {
|
|
13845
|
+
this.stateDirectory = stateDirectory;
|
|
13846
|
+
if (!isAbsolute(stateDirectory)) throw new Error("caddy config state directory must be absolute");
|
|
13847
|
+
this.rootDirectory = resolve(stateDirectory, "caddy");
|
|
13848
|
+
this.settingsFile = join(this.rootDirectory, "config.json");
|
|
13849
|
+
this.envFile = this.settingsFile;
|
|
13850
|
+
this.caddyfile = join(this.rootDirectory, "Caddyfile");
|
|
13851
|
+
this.dataDirectory = join(this.rootDirectory, "data");
|
|
13852
|
+
this.configDirectory = join(this.rootDirectory, "xdg-config");
|
|
13853
|
+
this.logsDirectory = join(this.rootDirectory, "logs");
|
|
13854
|
+
}
|
|
13855
|
+
settings() {
|
|
13856
|
+
return this.settingsValue;
|
|
13857
|
+
}
|
|
13858
|
+
credentials() {
|
|
13859
|
+
return this.credentialsValue;
|
|
13860
|
+
}
|
|
13861
|
+
errorCode() {
|
|
13862
|
+
return this.errorCodeValue;
|
|
13863
|
+
}
|
|
13864
|
+
status() {
|
|
13865
|
+
const settings = this.settingsValue;
|
|
13866
|
+
return Object.freeze({
|
|
13867
|
+
configured: settings !== void 0 && this.credentialsValue !== void 0,
|
|
13868
|
+
credentialsConfigured: this.credentialsValue !== void 0,
|
|
13869
|
+
storagePath: this.rootDirectory,
|
|
13870
|
+
...settings === void 0 ? {} : {
|
|
13871
|
+
publicOrigin: settings.publicOrigin,
|
|
13872
|
+
listenPort: settings.listenPort,
|
|
13873
|
+
dnsProvider: settings.dnsProvider
|
|
13874
|
+
},
|
|
13875
|
+
...this.errorCodeValue === void 0 ? {} : { errorCode: this.errorCodeValue }
|
|
13876
|
+
});
|
|
13877
|
+
}
|
|
13878
|
+
async initialize() {
|
|
13879
|
+
await this.enqueue(async () => {
|
|
13880
|
+
this.settingsValue = void 0;
|
|
13881
|
+
this.credentialsValue = void 0;
|
|
13882
|
+
this.errorCodeValue = void 0;
|
|
13883
|
+
await assertCaddyParents(this.stateDirectory, this.settingsFile);
|
|
13884
|
+
let entry;
|
|
13885
|
+
try {
|
|
13886
|
+
entry = await lstat(this.settingsFile);
|
|
13887
|
+
} catch (error) {
|
|
13888
|
+
if (error.code === "ENOENT") return;
|
|
13889
|
+
throw error;
|
|
13890
|
+
}
|
|
13891
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_CONFIG_BYTES) {
|
|
13892
|
+
this.errorCodeValue = "caddy_config_invalid";
|
|
13893
|
+
return;
|
|
13894
|
+
}
|
|
13895
|
+
await restrictPrivateFile(this.settingsFile);
|
|
13896
|
+
try {
|
|
13897
|
+
const record = JSON.parse(await readFile(this.settingsFile, "utf8"));
|
|
13898
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) throw new Error("invalid");
|
|
13899
|
+
const values = record;
|
|
13900
|
+
if (Reflect.ownKeys(values).some((key) => key !== "settings" && key !== "credentials")) throw new Error("invalid");
|
|
13901
|
+
const settings = parseCaddySettings(values.settings);
|
|
13902
|
+
const credentials = parseCredentials(values.credentials);
|
|
13903
|
+
this.settingsValue = settings;
|
|
13904
|
+
this.credentialsValue = credentials;
|
|
13905
|
+
} catch {
|
|
13906
|
+
this.errorCodeValue = "caddy_config_invalid";
|
|
13907
|
+
}
|
|
13908
|
+
});
|
|
13909
|
+
}
|
|
13910
|
+
/** Atomically save validated settings and credentials; omitted or both-blank secrets retain saved ones. */
|
|
13911
|
+
configure(value, credentialInput) {
|
|
13912
|
+
return this.enqueue(async () => {
|
|
13913
|
+
const settings = parseCaddySettings(value);
|
|
13914
|
+
const blank = typeof credentialInput === "object" && credentialInput !== null && !Array.isArray(credentialInput) && credentialInput.secretId === "" && credentialInput.secretKey === "" && Reflect.ownKeys(credentialInput).every((key) => key === "secretId" || key === "secretKey");
|
|
13915
|
+
const credentials = credentialInput === void 0 || blank ? this.credentialsValue : parseCredentials(credentialInput);
|
|
13916
|
+
if (credentials === void 0) throw new Error("caddy_credentials_missing");
|
|
13917
|
+
await writeCaddyPrivateFile(this.stateDirectory, this.settingsFile, JSON.stringify({
|
|
13918
|
+
settings,
|
|
13919
|
+
credentials
|
|
13920
|
+
}) + "\n");
|
|
13921
|
+
this.settingsValue = settings;
|
|
13922
|
+
this.credentialsValue = credentials;
|
|
13923
|
+
this.errorCodeValue = void 0;
|
|
13924
|
+
});
|
|
13925
|
+
}
|
|
13926
|
+
/** Write derived runtime files after authoritative settings are available. */
|
|
13927
|
+
prepareCaddyfile(backendPort) {
|
|
13928
|
+
return this.enqueue(async () => {
|
|
13929
|
+
const settings = this.settingsValue;
|
|
13930
|
+
if (settings === void 0 || this.credentialsValue === void 0) throw new Error("caddy_config_missing");
|
|
13931
|
+
for (const directory of [
|
|
13932
|
+
this.dataDirectory,
|
|
13933
|
+
this.configDirectory,
|
|
13934
|
+
this.logsDirectory
|
|
13935
|
+
]) await ensureCaddyDirectory(this.stateDirectory, directory);
|
|
13936
|
+
await writeCaddyPrivateFile(this.stateDirectory, this.caddyfile, renderCaddyfile(settings, this.stateDirectory, backendPort));
|
|
13937
|
+
});
|
|
13938
|
+
}
|
|
13939
|
+
/** Remove credentials, certificates, private keys, configuration, logs, and runtime cache. */
|
|
13940
|
+
purge() {
|
|
13941
|
+
return this.enqueue(async () => {
|
|
13942
|
+
await removeCaddyTree(this.stateDirectory, this.rootDirectory);
|
|
13943
|
+
this.settingsValue = void 0;
|
|
13944
|
+
this.credentialsValue = void 0;
|
|
13945
|
+
this.errorCodeValue = void 0;
|
|
13946
|
+
});
|
|
13947
|
+
}
|
|
13948
|
+
enqueue(operation) {
|
|
13949
|
+
const task = this.queue.then(operation, operation);
|
|
13950
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
13951
|
+
return task;
|
|
13952
|
+
}
|
|
13953
|
+
};
|
|
13954
|
+
//#endregion
|
|
13955
|
+
//#region src/caddy.ts
|
|
13956
|
+
/** Private directories override Caddy's platform defaults; ambient API keys and admin/proxy overrides are omitted. */
|
|
13957
|
+
function caddyProcessEnvironment(config, settings, environment = process.env) {
|
|
13958
|
+
const credentials = config.credentials();
|
|
13959
|
+
if (credentials === void 0) throw new Error("caddy_credentials_missing");
|
|
13960
|
+
return {
|
|
13961
|
+
...caddyInspectionEnvironment(environment),
|
|
13962
|
+
...caddyCredentialEnvironment(settings.dnsProvider, credentials),
|
|
13963
|
+
HOME: config.rootDirectory,
|
|
13964
|
+
USERPROFILE: config.rootDirectory,
|
|
13965
|
+
APPDATA: config.configDirectory,
|
|
13966
|
+
LOCALAPPDATA: config.dataDirectory,
|
|
13967
|
+
XDG_DATA_HOME: config.dataDirectory,
|
|
13968
|
+
XDG_CONFIG_HOME: config.configDirectory
|
|
13969
|
+
};
|
|
13970
|
+
}
|
|
13971
|
+
function spawnCaddy(executable, args, environment) {
|
|
13972
|
+
return spawn(executable, [...args], {
|
|
13973
|
+
env: environment,
|
|
13974
|
+
shell: false,
|
|
13975
|
+
stdio: [
|
|
13976
|
+
"pipe",
|
|
13977
|
+
"pipe",
|
|
13978
|
+
"pipe"
|
|
13979
|
+
],
|
|
13980
|
+
windowsHide: true
|
|
13981
|
+
});
|
|
13982
|
+
}
|
|
13983
|
+
async function validate(executable, caddyfile, environment, signal) {
|
|
13984
|
+
try {
|
|
13985
|
+
await execFileText(executable, [
|
|
13986
|
+
"adapt",
|
|
13987
|
+
"--config",
|
|
13988
|
+
caddyfile,
|
|
13989
|
+
"--adapter",
|
|
13990
|
+
"caddyfile"
|
|
13991
|
+
], {
|
|
13992
|
+
env: environment,
|
|
13993
|
+
signal,
|
|
13994
|
+
timeout: 15e3,
|
|
13995
|
+
maxBuffer: 262144
|
|
13996
|
+
});
|
|
13997
|
+
} catch {
|
|
13998
|
+
throw new Error("caddy_config_validation_failed");
|
|
13999
|
+
}
|
|
14000
|
+
}
|
|
14001
|
+
/** Owns Caddy and a loopback Gateway until both have stopped; ready requires verified public TLS and instance identity. */
|
|
14002
|
+
var CaddyController = class {
|
|
14003
|
+
options;
|
|
14004
|
+
enabled = false;
|
|
14005
|
+
initialized = false;
|
|
14006
|
+
disposed = false;
|
|
14007
|
+
child;
|
|
14008
|
+
gatewayValue;
|
|
14009
|
+
generation = 0;
|
|
14010
|
+
startupAbort;
|
|
14011
|
+
latest = Object.freeze({
|
|
14012
|
+
enabled: false,
|
|
14013
|
+
state: "off"
|
|
14014
|
+
});
|
|
14015
|
+
queue = Promise.resolve();
|
|
14016
|
+
closeTask;
|
|
14017
|
+
constructor(options) {
|
|
14018
|
+
this.options = options;
|
|
14019
|
+
}
|
|
14020
|
+
initialize() {
|
|
14021
|
+
return this.enqueue(async () => {
|
|
14022
|
+
if (this.initialized) return;
|
|
14023
|
+
if (this.disposed) throw new Error("caddy_controller_unavailable");
|
|
14024
|
+
this.enabled = (await this.options.store.load()).enabled;
|
|
14025
|
+
this.initialized = true;
|
|
14026
|
+
if (this.enabled) await this.start();
|
|
14027
|
+
});
|
|
14028
|
+
}
|
|
14029
|
+
gateway() {
|
|
14030
|
+
return this.latest.state === "ready" ? this.gatewayValue : void 0;
|
|
14031
|
+
}
|
|
14032
|
+
status() {
|
|
14033
|
+
return this.latest;
|
|
14034
|
+
}
|
|
14035
|
+
/** Cancel a readiness attempt before its owning coordinator waits for queued teardown. */
|
|
14036
|
+
cancelPendingStart() {
|
|
14037
|
+
this.startupAbort?.abort();
|
|
14038
|
+
}
|
|
14039
|
+
setEnabled(enabled) {
|
|
14040
|
+
if (!enabled) this.startupAbort?.abort();
|
|
14041
|
+
return this.enqueue(async () => {
|
|
14042
|
+
this.assertAvailable();
|
|
14043
|
+
if (this.enabled === enabled && (!enabled || this.latest.state === "ready")) return;
|
|
14044
|
+
if (!enabled) await this.stop();
|
|
14045
|
+
await this.options.store.save({
|
|
14046
|
+
version: 1,
|
|
14047
|
+
enabled
|
|
14048
|
+
});
|
|
14049
|
+
this.enabled = enabled;
|
|
14050
|
+
if (enabled) await this.start();
|
|
14051
|
+
else this.publish({
|
|
14052
|
+
enabled: false,
|
|
14053
|
+
state: "off"
|
|
14054
|
+
});
|
|
14055
|
+
}).then(() => this.status());
|
|
14056
|
+
}
|
|
14057
|
+
reconnect() {
|
|
14058
|
+
this.startupAbort?.abort();
|
|
14059
|
+
return this.enqueue(async () => {
|
|
14060
|
+
this.assertAvailable();
|
|
14061
|
+
await this.stop();
|
|
14062
|
+
await this.options.store.save({
|
|
14063
|
+
version: 1,
|
|
14064
|
+
enabled: true
|
|
14065
|
+
});
|
|
14066
|
+
this.enabled = true;
|
|
14067
|
+
await this.start();
|
|
14068
|
+
}).then(() => this.status());
|
|
14069
|
+
}
|
|
14070
|
+
reset() {
|
|
14071
|
+
return this.setEnabled(false);
|
|
14072
|
+
}
|
|
14073
|
+
close() {
|
|
14074
|
+
this.disposed = true;
|
|
14075
|
+
this.startupAbort?.abort();
|
|
14076
|
+
this.closeTask ??= this.enqueue(async () => {
|
|
14077
|
+
await this.stop();
|
|
14078
|
+
this.publish({
|
|
14079
|
+
enabled: this.enabled,
|
|
14080
|
+
state: "off"
|
|
14081
|
+
});
|
|
14082
|
+
}).catch((error) => {
|
|
14083
|
+
this.closeTask = void 0;
|
|
14084
|
+
throw error;
|
|
14085
|
+
});
|
|
14086
|
+
return this.closeTask;
|
|
14087
|
+
}
|
|
14088
|
+
assertAvailable() {
|
|
14089
|
+
if (!this.initialized || this.disposed) throw new Error("caddy_controller_unavailable");
|
|
14090
|
+
}
|
|
14091
|
+
enqueue(operation) {
|
|
14092
|
+
const task = this.queue.then(operation, operation);
|
|
14093
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
14094
|
+
return task;
|
|
14095
|
+
}
|
|
14096
|
+
publish(status) {
|
|
14097
|
+
this.latest = Object.freeze(status);
|
|
14098
|
+
try {
|
|
14099
|
+
this.options.onStatus?.(this.latest);
|
|
14100
|
+
} catch {}
|
|
14101
|
+
}
|
|
14102
|
+
async start() {
|
|
14103
|
+
const generation = ++this.generation;
|
|
14104
|
+
const startup = new AbortController();
|
|
14105
|
+
this.startupAbort = startup;
|
|
14106
|
+
const settings = this.options.config.settings();
|
|
14107
|
+
if (settings === void 0 || this.options.config.credentials() === void 0) {
|
|
14108
|
+
this.publish({
|
|
14109
|
+
enabled: true,
|
|
14110
|
+
state: "unavailable",
|
|
14111
|
+
errorCode: "caddy_config_missing"
|
|
14112
|
+
});
|
|
14113
|
+
this.startupAbort = void 0;
|
|
14114
|
+
return;
|
|
14115
|
+
}
|
|
14116
|
+
if (!this.options.component.status().supported) {
|
|
14117
|
+
this.publish({
|
|
14118
|
+
enabled: true,
|
|
14119
|
+
state: "unavailable",
|
|
14120
|
+
errorCode: "caddy_component_unavailable"
|
|
14121
|
+
});
|
|
14122
|
+
this.startupAbort = void 0;
|
|
14123
|
+
return;
|
|
14124
|
+
}
|
|
14125
|
+
this.publish({
|
|
14126
|
+
enabled: true,
|
|
14127
|
+
state: "starting",
|
|
14128
|
+
origin: settings.publicOrigin
|
|
14129
|
+
});
|
|
14130
|
+
let childFailure;
|
|
14131
|
+
const deadline = setTimeout(() => {
|
|
14132
|
+
startup.abort();
|
|
14133
|
+
}, this.options.startupTimeoutMs ?? 12e4);
|
|
14134
|
+
deadline.unref();
|
|
14135
|
+
try {
|
|
14136
|
+
await this.options.component.ensureExecutable();
|
|
14137
|
+
if (startup.signal.aborted || this.disposed) throw new Error("caddy_start_cancelled");
|
|
14138
|
+
const gateway = await this.options.createGateway(settings);
|
|
14139
|
+
this.gatewayValue = gateway;
|
|
14140
|
+
if (startup.signal.aborted || this.disposed) throw new Error("caddy_start_cancelled");
|
|
14141
|
+
await this.options.config.prepareCaddyfile(gateway.address().port);
|
|
14142
|
+
const environment = caddyProcessEnvironment(this.options.config, settings);
|
|
14143
|
+
await (this.options.validateConfig ?? validate)(this.options.component.executable, this.options.config.caddyfile, environment, startup.signal);
|
|
14144
|
+
if (startup.signal.aborted || this.disposed) throw new Error("caddy_start_cancelled");
|
|
14145
|
+
const child = (this.options.spawnProcess ?? spawnCaddy)(this.options.component.executable, [
|
|
14146
|
+
"run",
|
|
14147
|
+
"--config",
|
|
14148
|
+
this.options.config.caddyfile,
|
|
14149
|
+
"--adapter",
|
|
14150
|
+
"caddyfile"
|
|
14151
|
+
], environment);
|
|
14152
|
+
this.child = child;
|
|
14153
|
+
child.stdout.resume();
|
|
14154
|
+
child.stderr.resume();
|
|
14155
|
+
child.stdin.end();
|
|
14156
|
+
const failed = (code) => {
|
|
14157
|
+
if (generation !== this.generation || this.child !== child) return;
|
|
14158
|
+
childFailure = code;
|
|
14159
|
+
startup.abort();
|
|
14160
|
+
if (this.latest.state === "ready") this.enqueue(async () => {
|
|
14161
|
+
if (generation !== this.generation) return;
|
|
14162
|
+
await this.stop();
|
|
14163
|
+
if (this.enabled && !this.disposed) this.publish({
|
|
14164
|
+
enabled: true,
|
|
14165
|
+
state: "error",
|
|
14166
|
+
errorCode: code
|
|
14167
|
+
});
|
|
14168
|
+
}).catch(() => {
|
|
14169
|
+
this.publish({
|
|
14170
|
+
enabled: this.enabled,
|
|
14171
|
+
state: "error",
|
|
14172
|
+
errorCode: "caddy_cleanup_failed"
|
|
14173
|
+
});
|
|
14174
|
+
});
|
|
14175
|
+
};
|
|
14176
|
+
child.once("error", () => {
|
|
14177
|
+
failed("caddy_launch_failed");
|
|
14178
|
+
});
|
|
14179
|
+
child.once("close", () => {
|
|
14180
|
+
failed("caddy_exited");
|
|
14181
|
+
});
|
|
14182
|
+
const probe = this.options.probeDiscovery ?? ((origin, expected, signal) => defaultProbeDiscovery({ origin }, expected, signal));
|
|
14183
|
+
while (true) {
|
|
14184
|
+
if (startup.signal.aborted || this.disposed) throw new Error(childFailure ?? "caddy_start_timeout");
|
|
14185
|
+
let ready = false;
|
|
14186
|
+
try {
|
|
14187
|
+
ready = await probe(settings.publicOrigin, this.options.instanceId, startup.signal);
|
|
14188
|
+
} catch (error) {
|
|
14189
|
+
if (error instanceof Error && (error.message === "frp_discovery_mismatch" || error.message === "frp_discovery_invalid")) throw new Error("caddy_public_identity_invalid");
|
|
14190
|
+
}
|
|
14191
|
+
if (startup.signal.aborted || this.disposed || generation !== this.generation) throw new Error(childFailure ?? "caddy_start_timeout");
|
|
14192
|
+
if (ready) break;
|
|
14193
|
+
await this.waitForProbe(startup.signal);
|
|
14194
|
+
}
|
|
14195
|
+
this.publish({
|
|
14196
|
+
enabled: true,
|
|
14197
|
+
state: "ready",
|
|
14198
|
+
origin: settings.publicOrigin,
|
|
14199
|
+
backendOrigin: "http://127.0.0.1:" + String(gateway.address().port)
|
|
14200
|
+
});
|
|
14201
|
+
} catch (error) {
|
|
14202
|
+
++this.generation;
|
|
14203
|
+
await this.stopResources();
|
|
14204
|
+
if (!this.disposed) this.publish({
|
|
14205
|
+
enabled: this.enabled,
|
|
14206
|
+
state: "error",
|
|
14207
|
+
errorCode: childFailure ?? (error instanceof Error && error.message.startsWith("caddy_") ? error.message : error.code === "EADDRINUSE" ? "caddy_backend_port_in_use" : "caddy_start_failed")
|
|
14208
|
+
});
|
|
14209
|
+
} finally {
|
|
14210
|
+
clearTimeout(deadline);
|
|
14211
|
+
if (this.startupAbort === startup) this.startupAbort = void 0;
|
|
14212
|
+
}
|
|
14213
|
+
}
|
|
14214
|
+
waitForProbe(signal) {
|
|
14215
|
+
if (signal.aborted) return Promise.resolve();
|
|
14216
|
+
return new Promise((resolve) => {
|
|
14217
|
+
const done = () => {
|
|
14218
|
+
clearTimeout(timer);
|
|
14219
|
+
signal.removeEventListener("abort", done);
|
|
14220
|
+
resolve();
|
|
14221
|
+
};
|
|
14222
|
+
const timer = setTimeout(done, this.options.probeIntervalMs ?? 1e3);
|
|
14223
|
+
timer.unref();
|
|
14224
|
+
signal.addEventListener("abort", done, { once: true });
|
|
14225
|
+
});
|
|
14226
|
+
}
|
|
14227
|
+
async stop() {
|
|
14228
|
+
++this.generation;
|
|
14229
|
+
this.startupAbort?.abort();
|
|
14230
|
+
await this.stopResources();
|
|
14231
|
+
}
|
|
14232
|
+
async stopResources() {
|
|
14233
|
+
const child = this.child;
|
|
14234
|
+
const gateway = this.gatewayValue;
|
|
14235
|
+
await settleRemoteResources([async () => {
|
|
14236
|
+
if (child !== void 0) {
|
|
14237
|
+
await (this.options.terminateProcess ?? terminateRemoteProcess)(child);
|
|
14238
|
+
if (this.child === child) this.child = void 0;
|
|
14239
|
+
}
|
|
14240
|
+
}, async () => {
|
|
14241
|
+
if (gateway !== void 0) {
|
|
14242
|
+
await gateway.close();
|
|
14243
|
+
if (this.gatewayValue === gateway) this.gatewayValue = void 0;
|
|
14244
|
+
}
|
|
14245
|
+
}], "caddy cleanup failed");
|
|
14246
|
+
}
|
|
14247
|
+
};
|
|
14248
|
+
//#endregion
|
|
14249
|
+
//#region src/managed-origin.ts
|
|
14250
|
+
/** Children never independently restore a durable enabled switch. */
|
|
14251
|
+
function createVolatileRemoteControlStore() {
|
|
14252
|
+
let value = {
|
|
14253
|
+
version: 1,
|
|
14254
|
+
enabled: false
|
|
14255
|
+
};
|
|
14256
|
+
return {
|
|
14257
|
+
load: async () => value,
|
|
14258
|
+
save: async (state) => {
|
|
14259
|
+
value = Object.freeze({ ...state });
|
|
14260
|
+
}
|
|
14261
|
+
};
|
|
14262
|
+
}
|
|
14263
|
+
function parseMode(value) {
|
|
14264
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("origin_mode_invalid");
|
|
14265
|
+
const record = value;
|
|
14266
|
+
if (record.version !== 1 || record.mode !== "external" && record.mode !== "managed" || Reflect.ownKeys(record).some((key) => key !== "version" && key !== "mode")) throw new Error("origin_mode_invalid");
|
|
14267
|
+
return Object.freeze({
|
|
14268
|
+
version: 1,
|
|
14269
|
+
mode: record.mode
|
|
14270
|
+
});
|
|
14271
|
+
}
|
|
14272
|
+
/** Persist only origin's upstream mode, defaulting to the existing external-proxy behavior. */
|
|
14273
|
+
var JsonOriginModeStore = class {
|
|
14274
|
+
file;
|
|
14275
|
+
constructor(file) {
|
|
14276
|
+
this.file = file;
|
|
14277
|
+
if (!isAbsolute(file)) throw new Error("origin mode file must be absolute");
|
|
14278
|
+
}
|
|
14279
|
+
async load() {
|
|
14280
|
+
await assertCaddyParents(dirname(this.file), this.file);
|
|
14281
|
+
let entry;
|
|
14282
|
+
try {
|
|
14283
|
+
entry = await lstat(this.file);
|
|
14284
|
+
} catch (error) {
|
|
14285
|
+
if (error.code === "ENOENT") return {
|
|
14286
|
+
version: 1,
|
|
14287
|
+
mode: "external"
|
|
14288
|
+
};
|
|
14289
|
+
throw error;
|
|
14290
|
+
}
|
|
14291
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 1024) throw new Error("origin_mode_invalid");
|
|
14292
|
+
await restrictPrivateFile(this.file);
|
|
14293
|
+
return parseMode(JSON.parse(await readFile(this.file, "utf8")));
|
|
14294
|
+
}
|
|
14295
|
+
async save(value) {
|
|
14296
|
+
await writeCaddyPrivateFile(dirname(this.file), this.file, JSON.stringify(parseMode(value)) + "\n");
|
|
14297
|
+
}
|
|
14298
|
+
};
|
|
14299
|
+
/** One origin-provider owner selects exactly one upstream; external settings and shared device records remain untouched. */
|
|
14300
|
+
var ManagedOriginController = class {
|
|
14301
|
+
options;
|
|
14302
|
+
selected = "external";
|
|
14303
|
+
enabled = false;
|
|
14304
|
+
initialized = false;
|
|
14305
|
+
disposed = false;
|
|
14306
|
+
queue = Promise.resolve();
|
|
14307
|
+
closeTask;
|
|
14308
|
+
constructor(options) {
|
|
14309
|
+
this.options = options;
|
|
14310
|
+
}
|
|
14311
|
+
mode() {
|
|
14312
|
+
return this.selected;
|
|
14313
|
+
}
|
|
14314
|
+
status() {
|
|
14315
|
+
return Object.freeze({
|
|
14316
|
+
...this.controller().status(),
|
|
14317
|
+
enabled: this.enabled,
|
|
14318
|
+
mode: this.selected
|
|
14319
|
+
});
|
|
14320
|
+
}
|
|
14321
|
+
gateway() {
|
|
14322
|
+
return this.enabled ? this.controller().gateway() : void 0;
|
|
14323
|
+
}
|
|
14324
|
+
initialize() {
|
|
14325
|
+
return this.enqueue(async () => {
|
|
14326
|
+
if (this.initialized) return;
|
|
14327
|
+
if (this.disposed) throw new Error("origin_controller_unavailable");
|
|
14328
|
+
this.selected = (await this.options.modeStore.load()).mode;
|
|
14329
|
+
this.enabled = (await this.options.store.load()).enabled;
|
|
14330
|
+
await this.options.external.initialize();
|
|
14331
|
+
await this.options.managed.initialize();
|
|
14332
|
+
await this.options.external.setEnabled(false);
|
|
14333
|
+
await this.options.managed.setEnabled(false);
|
|
14334
|
+
this.initialized = true;
|
|
14335
|
+
if (this.enabled && !this.disposed) await this.controller().setEnabled(true);
|
|
14336
|
+
});
|
|
14337
|
+
}
|
|
14338
|
+
selectMode(mode) {
|
|
14339
|
+
if (mode !== this.selected) this.options.managed.cancelPendingStart?.();
|
|
14340
|
+
return this.enqueue(async () => {
|
|
14341
|
+
this.assertAvailable();
|
|
14342
|
+
if (mode !== "external" && mode !== "managed") throw new Error("origin_mode_invalid");
|
|
14343
|
+
if (mode === this.selected) return;
|
|
14344
|
+
const previous = this.controller();
|
|
14345
|
+
await previous.setEnabled(false);
|
|
14346
|
+
try {
|
|
14347
|
+
await this.options.modeStore.save({
|
|
14348
|
+
version: 1,
|
|
14349
|
+
mode
|
|
14350
|
+
});
|
|
14351
|
+
} catch (error) {
|
|
14352
|
+
if (this.enabled && !this.disposed) await previous.setEnabled(true);
|
|
14353
|
+
throw error;
|
|
14354
|
+
}
|
|
14355
|
+
this.selected = mode;
|
|
14356
|
+
if (this.enabled && !this.disposed) try {
|
|
14357
|
+
await this.controller().setEnabled(true);
|
|
14358
|
+
} catch (error) {
|
|
14359
|
+
await this.stopBoth();
|
|
14360
|
+
throw error;
|
|
14361
|
+
}
|
|
14362
|
+
}).then(() => this.status());
|
|
14363
|
+
}
|
|
14364
|
+
setEnabled(enabled) {
|
|
14365
|
+
if (!enabled) this.options.managed.cancelPendingStart?.();
|
|
14366
|
+
return this.enqueue(async () => {
|
|
14367
|
+
this.assertAvailable();
|
|
14368
|
+
if (!enabled) await this.stopBoth();
|
|
14369
|
+
await this.options.store.save({
|
|
14370
|
+
version: 1,
|
|
14371
|
+
enabled
|
|
14372
|
+
});
|
|
14373
|
+
this.enabled = enabled;
|
|
14374
|
+
if (enabled && !this.disposed) try {
|
|
14375
|
+
await this.controller().setEnabled(true);
|
|
14376
|
+
} catch (error) {
|
|
14377
|
+
await this.stopBoth();
|
|
14378
|
+
throw error;
|
|
14379
|
+
}
|
|
14380
|
+
}).then(() => this.status());
|
|
14381
|
+
}
|
|
14382
|
+
reconnect() {
|
|
14383
|
+
this.options.managed.cancelPendingStart?.();
|
|
14384
|
+
return this.enqueue(async () => {
|
|
14385
|
+
this.assertAvailable();
|
|
14386
|
+
await this.stopBoth();
|
|
14387
|
+
await this.options.store.save({
|
|
14388
|
+
version: 1,
|
|
14389
|
+
enabled: true
|
|
14390
|
+
});
|
|
14391
|
+
this.enabled = true;
|
|
14392
|
+
if (!this.disposed) try {
|
|
14393
|
+
await this.controller().reconnect();
|
|
14394
|
+
} catch (error) {
|
|
14395
|
+
await this.stopBoth();
|
|
14396
|
+
throw error;
|
|
14397
|
+
}
|
|
14398
|
+
}).then(() => this.status());
|
|
14399
|
+
}
|
|
14400
|
+
reset() {
|
|
14401
|
+
return this.setEnabled(false);
|
|
14402
|
+
}
|
|
14403
|
+
close() {
|
|
14404
|
+
this.disposed = true;
|
|
14405
|
+
this.closeTask ??= settleRemoteResources([() => this.options.external.close(), () => this.options.managed.close()]).then(() => this.queue).catch((error) => {
|
|
14406
|
+
this.closeTask = void 0;
|
|
14407
|
+
throw error;
|
|
14408
|
+
});
|
|
14409
|
+
return this.closeTask;
|
|
14410
|
+
}
|
|
14411
|
+
controller() {
|
|
14412
|
+
return this.options[this.selected];
|
|
14413
|
+
}
|
|
14414
|
+
assertAvailable() {
|
|
14415
|
+
if (!this.initialized || this.disposed) throw new Error("origin_controller_unavailable");
|
|
14416
|
+
}
|
|
14417
|
+
stopBoth() {
|
|
14418
|
+
return settleRemoteResources([async () => {
|
|
14419
|
+
await this.options.external.setEnabled(false);
|
|
14420
|
+
}, async () => {
|
|
14421
|
+
await this.options.managed.setEnabled(false);
|
|
14422
|
+
}]);
|
|
14423
|
+
}
|
|
14424
|
+
enqueue(operation) {
|
|
14425
|
+
const task = this.queue.then(operation, operation);
|
|
14426
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
14427
|
+
return task;
|
|
14428
|
+
}
|
|
14429
|
+
};
|
|
14430
|
+
//#endregion
|
|
14431
|
+
//#region src/release-update.ts
|
|
14432
|
+
const PACKAGE_NAME = "dsh-mobile";
|
|
14433
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/dsh-mobile/latest";
|
|
14434
|
+
const GITHUB_LATEST_URL = "https://github.com/saya-ch/dsh-mobile/releases/latest";
|
|
14435
|
+
const GITHUB_API_LATEST_URL = "https://api.github.com/repos/saya-ch/dsh-mobile/releases/latest";
|
|
14436
|
+
const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
|
|
14437
|
+
const RELEASE_NOTES_MAX_CHARS = 4e3;
|
|
14438
|
+
const STATUS_CACHE_MS = 6e5;
|
|
14439
|
+
const REQUEST_TIMEOUT_MS = 8e3;
|
|
14440
|
+
const UPDATE_TIMEOUT_MS = 12e4;
|
|
14441
|
+
const UPDATE_TERMINATION_GRACE_MS = 1500;
|
|
14442
|
+
const NUMERIC_VERSION_IDENTIFIER = "(?:0|[1-9]\\d*)";
|
|
14443
|
+
const WILDCARD_VERSION_IDENTIFIER = "(?:[xX*])";
|
|
14444
|
+
const RANGE_VERSION = `(?:${`${NUMERIC_VERSION_IDENTIFIER}\\.${NUMERIC_VERSION_IDENTIFIER}\\.${NUMERIC_VERSION_IDENTIFIER}(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?`}|${`(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}(?:\\.(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}(?:\\.(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}))?))?)`})`;
|
|
14445
|
+
const COMPARATOR = new RegExp(`^(?:<=|>=|<|>|=|~|\\^)?${RANGE_VERSION}$`, "u");
|
|
14446
|
+
const HYPHEN_RANGE = new RegExp(`^${RANGE_VERSION} +[-] +${RANGE_VERSION}$`, "u");
|
|
14447
|
+
const DIST_TAG = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/u;
|
|
14448
|
+
function parseSemver(value) {
|
|
14449
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
14450
|
+
if (match === null) return void 0;
|
|
14451
|
+
const core = [
|
|
14452
|
+
Number(match[1]),
|
|
14453
|
+
Number(match[2]),
|
|
14454
|
+
Number(match[3])
|
|
14455
|
+
];
|
|
14456
|
+
if (core.some((part) => !Number.isSafeInteger(part))) return void 0;
|
|
14457
|
+
const prerelease = match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/u.test(part) ? Number(part) : part);
|
|
14458
|
+
if (prerelease.some((part) => typeof part === "number" && !Number.isSafeInteger(part))) return void 0;
|
|
14459
|
+
return Object.freeze({
|
|
14460
|
+
core,
|
|
14461
|
+
prerelease: Object.freeze(prerelease)
|
|
14462
|
+
});
|
|
14463
|
+
}
|
|
14464
|
+
/** Compare two strict SemVer strings, including prerelease precedence. */
|
|
14465
|
+
function comparePluginVersions(left, right) {
|
|
14466
|
+
const a = parseSemver(left);
|
|
14467
|
+
const b = parseSemver(right);
|
|
14468
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
14469
|
+
for (let index = 0; index < a.core.length; index += 1) {
|
|
14470
|
+
const difference = a.core[index] - b.core[index];
|
|
14471
|
+
if (difference !== 0) return Math.sign(difference);
|
|
14472
|
+
}
|
|
14473
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
14474
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
14475
|
+
for (let index = 0; index < length; index += 1) {
|
|
14476
|
+
const leftPart = a.prerelease[index];
|
|
14477
|
+
const rightPart = b.prerelease[index];
|
|
14478
|
+
if (leftPart === void 0 || rightPart === void 0) return leftPart === void 0 ? -1 : 1;
|
|
14479
|
+
if (leftPart === rightPart) continue;
|
|
14480
|
+
if (typeof leftPart === "number" && typeof rightPart === "number") return Math.sign(leftPart - rightPart);
|
|
14481
|
+
if (typeof leftPart === "number") return -1;
|
|
14482
|
+
if (typeof rightPart === "number") return 1;
|
|
14483
|
+
return leftPart < rightPart ? -1 : 1;
|
|
14484
|
+
}
|
|
14485
|
+
return 0;
|
|
14486
|
+
}
|
|
14487
|
+
function isComparatorSet(value) {
|
|
14488
|
+
if (HYPHEN_RANGE.test(value)) return true;
|
|
14489
|
+
const comparators = value.replace(/(<=|>=|<|>|=|~|\^) +/gu, "$1").split(/ +/u);
|
|
14490
|
+
return comparators.length > 0 && comparators.every((comparator) => COMPARATOR.test(comparator));
|
|
14491
|
+
}
|
|
14492
|
+
function isNpmVersionRange(value) {
|
|
14493
|
+
if (!/^[0-9xX*<>=~^|.+\- ]+$/u.test(value)) return false;
|
|
14494
|
+
const alternatives = value.split(/ *\|\| */u);
|
|
14495
|
+
return alternatives.length > 0 && alternatives.every((alternative) => alternative !== "" && isComparatorSet(alternative));
|
|
14496
|
+
}
|
|
14497
|
+
/** Return whether pnpm may safely replace this profile dependency from an npm version, range, or tag. */
|
|
14498
|
+
function isRegistryPluginSpec(value) {
|
|
14499
|
+
if (typeof value !== "string" || value.trim() !== value || value === "" || /[\u0000-\u001f\u007f]/u.test(value)) return false;
|
|
14500
|
+
if (/\.(?:tgz|tar(?:\.gz)?)$/iu.test(value)) return false;
|
|
14501
|
+
return parseSemver(value) !== void 0 || isNpmVersionRange(value) || DIST_TAG.test(value);
|
|
14502
|
+
}
|
|
14503
|
+
/** Resolve the DSH profile named by the current launcher arguments. */
|
|
14504
|
+
function launchedProfileName(argv) {
|
|
14505
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14506
|
+
if (argv[index] === "--profile") {
|
|
14507
|
+
const candidate = argv[index + 1];
|
|
14508
|
+
if (candidate !== void 0 && /^[\w.-]+$/u.test(candidate)) return candidate;
|
|
14509
|
+
}
|
|
14510
|
+
const match = /^--profile=([\w.-]+)$/u.exec(argv[index] ?? "");
|
|
14511
|
+
if (match?.[1] !== void 0) return match[1];
|
|
14512
|
+
}
|
|
14513
|
+
return "web";
|
|
14514
|
+
}
|
|
14515
|
+
/** Resolve the launcher-owned Desktop profile, or the CLI profile outside Desktop. */
|
|
14516
|
+
function releaseProfileDirectory(ctx, dshHome, argv) {
|
|
14517
|
+
const desktopProfiles = ctx.get("desktopProfiles");
|
|
14518
|
+
const desktopDirectory = desktopProfiles?.current?.dir;
|
|
14519
|
+
if (typeof desktopDirectory === "string" && isAbsolute(desktopDirectory)) return desktopDirectory;
|
|
14520
|
+
if (desktopProfiles !== void 0 || ctx.get("desktopRuntime") !== void 0) return void 0;
|
|
14521
|
+
return join(dshHome, "profiles", launchedProfileName(argv));
|
|
14522
|
+
}
|
|
14523
|
+
async function profileDependencySpec(profileDirectory) {
|
|
14524
|
+
try {
|
|
14525
|
+
const value = JSON.parse(await readFile(join(profileDirectory, "package.json"), "utf8")).dependencies?.[PACKAGE_NAME];
|
|
14526
|
+
return typeof value === "string" ? value : void 0;
|
|
14527
|
+
} catch {
|
|
14528
|
+
return;
|
|
14529
|
+
}
|
|
14530
|
+
}
|
|
14531
|
+
async function fetchNpmVersion(fetcher) {
|
|
14532
|
+
const response = await fetcher(NPM_LATEST_URL, {
|
|
14533
|
+
headers: {
|
|
14534
|
+
accept: "application/json",
|
|
14535
|
+
"user-agent": "dsh-mobile-release-check"
|
|
14536
|
+
},
|
|
14537
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
14538
|
+
});
|
|
14539
|
+
if (!response.ok) return void 0;
|
|
14540
|
+
const payload = await response.json();
|
|
14541
|
+
return typeof payload.version === "string" && parseSemver(payload.version) !== void 0 ? payload.version : void 0;
|
|
14542
|
+
}
|
|
14543
|
+
function githubReleaseVersion(location, responseUrl) {
|
|
14544
|
+
let url;
|
|
14545
|
+
try {
|
|
14546
|
+
url = new URL(location ?? responseUrl, GITHUB_LATEST_URL);
|
|
14547
|
+
} catch {
|
|
14548
|
+
return;
|
|
14549
|
+
}
|
|
14550
|
+
if (url.origin !== "https://github.com" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return void 0;
|
|
14551
|
+
if (!url.pathname.startsWith("/saya-ch/dsh-mobile/releases/tag/v")) return void 0;
|
|
14552
|
+
let version;
|
|
14553
|
+
try {
|
|
14554
|
+
version = decodeURIComponent(url.pathname.slice(34));
|
|
14555
|
+
} catch {
|
|
14556
|
+
return;
|
|
14557
|
+
}
|
|
14558
|
+
return parseSemver(version) === void 0 ? void 0 : version;
|
|
14559
|
+
}
|
|
14560
|
+
function androidReleaseDownloadUrl(version) {
|
|
14561
|
+
if (version === void 0) return GITHUB_RELEASES_URL;
|
|
14562
|
+
const tag = `v${version}`;
|
|
14563
|
+
return `https://github.com/saya-ch/dsh-mobile/releases/download/${encodeURIComponent(tag)}/dsh-mobile-android-${encodeURIComponent(tag)}.apk`;
|
|
12767
14564
|
}
|
|
12768
14565
|
async function fetchAndroidVersion(fetcher) {
|
|
12769
14566
|
const response = await fetcher(GITHUB_LATEST_URL, {
|
|
@@ -13324,7 +15121,8 @@ async function loadSetup(config) {
|
|
|
13324
15121
|
if (record.version === 2) return {
|
|
13325
15122
|
kind: "managed",
|
|
13326
15123
|
config: withoutSetupKeys(config),
|
|
13327
|
-
setup: parseManagedSetup(record)
|
|
15124
|
+
setup: parseManagedSetup(record),
|
|
15125
|
+
setupFile: resolve(config.setupFile)
|
|
13328
15126
|
};
|
|
13329
15127
|
if (record.version !== 1 || Reflect.ownKeys(record).some((key) => typeof key !== "string" || !SETUP_KEYS.has(key))) throw new Error("mobile setup file has an unsupported format");
|
|
13330
15128
|
const { version: _version, ...setup } = record;
|
|
@@ -13432,7 +15230,7 @@ function originGatewayConfig(template, settings, stateFile, instanceId, listenPo
|
|
|
13432
15230
|
allowedCidrs: Object.freeze(validated.allowedCidrs.map(parseCidr))
|
|
13433
15231
|
});
|
|
13434
15232
|
}
|
|
13435
|
-
function remoteControlPayload(provider, status, gateway, providerStatuses, cpolarComponent, cloudflaredComponent, cloudflaredConfiguration, frpComponent, frpConfiguration, originConfiguration) {
|
|
15233
|
+
function remoteControlPayload(provider, status, gateway, providerStatuses, cpolarComponent, cloudflaredComponent, cloudflaredConfiguration, frpComponent, frpConfiguration, originConfiguration, caddyComponentStatus) {
|
|
13436
15234
|
return {
|
|
13437
15235
|
provider,
|
|
13438
15236
|
running: status.enabled,
|
|
@@ -13473,7 +15271,8 @@ function remoteControlPayload(provider, status, gateway, providerStatuses, cpola
|
|
|
13473
15271
|
bundled: true,
|
|
13474
15272
|
running: providerStatuses.origin.enabled,
|
|
13475
15273
|
state: providerStatuses.origin.state,
|
|
13476
|
-
configuration: originConfiguration
|
|
15274
|
+
configuration: originConfiguration,
|
|
15275
|
+
component: caddyComponentStatus
|
|
13477
15276
|
}
|
|
13478
15277
|
}
|
|
13479
15278
|
};
|
|
@@ -13513,6 +15312,10 @@ async function apply(ctx, config) {
|
|
|
13513
15312
|
await frpConfig.initialize();
|
|
13514
15313
|
const originConfig = new OriginConfigStore(join(remoteDirectory, "origin", "config"));
|
|
13515
15314
|
await originConfig.initialize();
|
|
15315
|
+
const caddyComponent = new CaddyComponentManager({ stateDirectory });
|
|
15316
|
+
await caddyComponent.initialize();
|
|
15317
|
+
const caddyConfig = new CaddyConfigStore(stateDirectory);
|
|
15318
|
+
await caddyConfig.initialize();
|
|
13516
15319
|
const cloudflaredTunnel = new CloudflaredTunnelStore(join(remoteDirectory, "cloudflared"));
|
|
13517
15320
|
await cloudflaredTunnel.initialize();
|
|
13518
15321
|
const unregisterBuiltin = mobileAccess.registerExtension({
|
|
@@ -13548,8 +15351,13 @@ async function apply(ctx, config) {
|
|
|
13548
15351
|
const webSocketPaths = new WebSocketPathStore(join(stateDirectory, "websocket-paths.json"));
|
|
13549
15352
|
await webSocketPaths.load();
|
|
13550
15353
|
const blockedUpgradePaths = new BlockedUpgradePathLog();
|
|
15354
|
+
const clientModulePreferences = new ClientModulePreferenceStore(join(stateDirectory, "client-modules.json"), template.excludedClientModules);
|
|
15355
|
+
await clientModulePreferences.initialize();
|
|
13551
15356
|
let lanGateway;
|
|
13552
15357
|
let preparedLanSetup;
|
|
15358
|
+
let trustedNetworksRestartRequired = false;
|
|
15359
|
+
let trustedNetworksWrite = Promise.resolve();
|
|
15360
|
+
let adminClosing = false;
|
|
13553
15361
|
const startGateway = async (candidateConfig) => {
|
|
13554
15362
|
const resolved = parseGatewayConfig({
|
|
13555
15363
|
...candidateConfig,
|
|
@@ -13557,7 +15365,7 @@ async function apply(ctx, config) {
|
|
|
13557
15365
|
});
|
|
13558
15366
|
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, (source, code) => {
|
|
13559
15367
|
logger.warn("%s discovery degraded while DSH remains available: %s", source, code);
|
|
13560
|
-
});
|
|
15368
|
+
}, clientModulePreferences);
|
|
13561
15369
|
await candidate.start();
|
|
13562
15370
|
lanGateway = candidate;
|
|
13563
15371
|
const removeTaskSink = taskEventHub.add(candidate);
|
|
@@ -13613,7 +15421,7 @@ async function apply(ctx, config) {
|
|
|
13613
15421
|
}
|
|
13614
15422
|
const createRemoteGateway = async (publicOrigin, listenPort = 0) => {
|
|
13615
15423
|
const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, instanceId, listenPort);
|
|
13616
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths);
|
|
15424
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences);
|
|
13617
15425
|
await candidate.start();
|
|
13618
15426
|
return candidate;
|
|
13619
15427
|
};
|
|
@@ -13629,7 +15437,7 @@ async function apply(ctx, config) {
|
|
|
13629
15437
|
if (!isFrpSelfSignedIngress(settings)) return createRemoteGateway(publicOrigin);
|
|
13630
15438
|
const ingress = await ensureFrpIngressCertificate(settings, remoteDeviceFile);
|
|
13631
15439
|
const resolved = frpIngressGatewayConfig(template, settings, remoteDeviceFile, ingress);
|
|
13632
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths);
|
|
15440
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences);
|
|
13633
15441
|
try {
|
|
13634
15442
|
await candidate.start();
|
|
13635
15443
|
} catch (error) {
|
|
@@ -13640,7 +15448,7 @@ async function apply(ctx, config) {
|
|
|
13640
15448
|
};
|
|
13641
15449
|
const createOriginGateway = async (settings) => {
|
|
13642
15450
|
const resolved = originGatewayConfig(template, settings, remoteDeviceFile, instanceId);
|
|
13643
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths);
|
|
15451
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences);
|
|
13644
15452
|
try {
|
|
13645
15453
|
await candidate.start();
|
|
13646
15454
|
} catch (error) {
|
|
@@ -13654,6 +15462,23 @@ async function apply(ctx, config) {
|
|
|
13654
15462
|
const cloudflaredStore = new JsonMobileAccessControlStore(join(remoteDirectory, "cloudflared", "control.json"), false);
|
|
13655
15463
|
const frpStore = new JsonMobileAccessControlStore(join(remoteDirectory, "frp", "control.json"), false);
|
|
13656
15464
|
const originStore = new JsonMobileAccessControlStore(join(remoteDirectory, "origin", "control.json"), false);
|
|
15465
|
+
const caddyController = new CaddyController({
|
|
15466
|
+
store: createVolatileRemoteControlStore(),
|
|
15467
|
+
component: caddyComponent,
|
|
15468
|
+
config: caddyConfig,
|
|
15469
|
+
instanceId,
|
|
15470
|
+
createGateway: (settings) => createRemoteGateway(settings.publicOrigin)
|
|
15471
|
+
});
|
|
15472
|
+
const originController = new ManagedOriginController({
|
|
15473
|
+
store: originStore,
|
|
15474
|
+
modeStore: new JsonOriginModeStore(join(remoteDirectory, "origin", "upstream.json")),
|
|
15475
|
+
external: new OriginController({
|
|
15476
|
+
store: createVolatileRemoteControlStore(),
|
|
15477
|
+
config: originConfig,
|
|
15478
|
+
createGateway: createOriginGateway
|
|
15479
|
+
}),
|
|
15480
|
+
managed: caddyController
|
|
15481
|
+
});
|
|
13657
15482
|
const remoteControllers = {
|
|
13658
15483
|
tailscale: new FunnelController({
|
|
13659
15484
|
store: tailscaleStore,
|
|
@@ -13692,11 +15517,7 @@ async function apply(ctx, config) {
|
|
|
13692
15517
|
await gateway.refreshProvidedTls();
|
|
13693
15518
|
}
|
|
13694
15519
|
}),
|
|
13695
|
-
origin:
|
|
13696
|
-
store: originStore,
|
|
13697
|
-
config: originConfig,
|
|
13698
|
-
createGateway: createOriginGateway
|
|
13699
|
-
})
|
|
15520
|
+
origin: originController
|
|
13700
15521
|
};
|
|
13701
15522
|
const remoteProviders = new RemoteProviderCoordinator(initialRemoteProvider, remoteControllers, remoteProviderStore);
|
|
13702
15523
|
const remoteController = () => remoteProviders.controller();
|
|
@@ -13706,13 +15527,18 @@ async function apply(ctx, config) {
|
|
|
13706
15527
|
controller.gateway()?.broadcastTaskEvent(event);
|
|
13707
15528
|
} });
|
|
13708
15529
|
}
|
|
13709
|
-
const remotePayload = () =>
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
15530
|
+
const remotePayload = () => ({
|
|
15531
|
+
...remoteControlPayload(remoteProviders.selected, remoteController().status(), remoteController().gateway(), {
|
|
15532
|
+
tailscale: remoteControllers.tailscale.status(),
|
|
15533
|
+
cpolar: remoteControllers.cpolar.status(),
|
|
15534
|
+
cloudflared: remoteControllers.cloudflared.status(),
|
|
15535
|
+
frp: remoteControllers.frp.status(),
|
|
15536
|
+
origin: remoteControllers.origin.status()
|
|
15537
|
+
}, cpolarComponent.status(), cloudflaredComponent.status(), cloudflaredTunnel.status(), frpComponent.status(), frpConfig.status(), originConfig.status(), caddyComponent.status()),
|
|
15538
|
+
originMode: originController.mode(),
|
|
15539
|
+
caddyConfiguration: caddyConfig.status(),
|
|
15540
|
+
caddyState: caddyController.status()
|
|
15541
|
+
});
|
|
13716
15542
|
const lanPayload = () => ({
|
|
13717
15543
|
...loaded.kind === "unconfigured" ? {
|
|
13718
15544
|
configured: preparedLanSetup !== void 0,
|
|
@@ -13743,6 +15569,15 @@ async function apply(ctx, config) {
|
|
|
13743
15569
|
windowsFirewall: process.platform === "win32"
|
|
13744
15570
|
};
|
|
13745
15571
|
};
|
|
15572
|
+
const trustedNetworksPayload = async () => {
|
|
15573
|
+
if (loaded.kind !== "managed") return { supported: false };
|
|
15574
|
+
return {
|
|
15575
|
+
supported: true,
|
|
15576
|
+
extraAllowedCidrs: parseManagedSetup(JSON.parse(await readFile(loaded.setupFile, "utf8"))).extraAllowedCidrs ?? [],
|
|
15577
|
+
restartRequired: trustedNetworksRestartRequired,
|
|
15578
|
+
windowsFirewall: process.platform === "win32"
|
|
15579
|
+
};
|
|
15580
|
+
};
|
|
13746
15581
|
const diagnosticsPayload = async () => {
|
|
13747
15582
|
let interfaceName;
|
|
13748
15583
|
let networkError;
|
|
@@ -13808,6 +15643,7 @@ async function apply(ctx, config) {
|
|
|
13808
15643
|
try {
|
|
13809
15644
|
const target = parseRequestTarget(request.url);
|
|
13810
15645
|
assertLocalAdminTrust(request, request.method === "POST", () => authenticateDesktopAdmin(request));
|
|
15646
|
+
if (adminClosing) throw new HttpError(503, "unavailable");
|
|
13811
15647
|
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
13812
15648
|
const lanControl = target.decodedPathname === `/api/mobile-access/control` || target.decodedPathname === `/api/mobile-access/lan/control`;
|
|
13813
15649
|
if (request.method === "GET" && lanControl) {
|
|
@@ -13818,6 +15654,59 @@ async function apply(ctx, config) {
|
|
|
13818
15654
|
sendJson(response, 200, await lanSetupPayload(), false);
|
|
13819
15655
|
return;
|
|
13820
15656
|
}
|
|
15657
|
+
if (target.decodedPathname === `/api/mobile-access/client-modules`) {
|
|
15658
|
+
const gateway = lanGateway ?? remoteController().gateway();
|
|
15659
|
+
if (gateway === void 0) throw new HttpError(409, "mobile_access_not_running");
|
|
15660
|
+
const operation = new AbortController();
|
|
15661
|
+
const abort = () => {
|
|
15662
|
+
operation.abort();
|
|
15663
|
+
};
|
|
15664
|
+
response.once("close", abort);
|
|
15665
|
+
try {
|
|
15666
|
+
if (request.method === "GET") {
|
|
15667
|
+
sendJson(response, 200, await gateway.describeClientModules(void 0, operation.signal), false);
|
|
15668
|
+
return;
|
|
15669
|
+
}
|
|
15670
|
+
if (request.method !== "POST") throw new HttpError(405, "method_not_allowed");
|
|
15671
|
+
const body = await readJsonObject(request, template.maxBodyBytes);
|
|
15672
|
+
if (Object.keys(body).length !== 1 || Object.keys(body).some((key) => key !== "reset" && key !== "excludedClientModules")) throw new HttpError(400, "bad_request");
|
|
15673
|
+
sendJson(response, 200, body.reset === true ? await gateway.resetClientModules(void 0, operation.signal) : await gateway.configureClientModules(body.excludedClientModules, void 0, operation.signal), false);
|
|
15674
|
+
} catch (error) {
|
|
15675
|
+
if (!(error instanceof ClientModuleConflictError)) throw error;
|
|
15676
|
+
sendJson(response, 409, {
|
|
15677
|
+
error: "excluded_client_modules_invalid",
|
|
15678
|
+
detail: error.detail
|
|
15679
|
+
}, false);
|
|
15680
|
+
} finally {
|
|
15681
|
+
response.removeListener("close", abort);
|
|
15682
|
+
operation.abort();
|
|
15683
|
+
}
|
|
15684
|
+
return;
|
|
15685
|
+
}
|
|
15686
|
+
if (target.decodedPathname === `/api/mobile-access/lan/trusted-networks`) {
|
|
15687
|
+
if (request.method === "GET") {
|
|
15688
|
+
sendJson(response, 200, await trustedNetworksPayload(), false);
|
|
15689
|
+
return;
|
|
15690
|
+
}
|
|
15691
|
+
if (request.method !== "POST") throw new HttpError(405, "method_not_allowed");
|
|
15692
|
+
if (loaded.kind !== "managed") throw new HttpError(409, "lan_trusted_networks_unavailable");
|
|
15693
|
+
const body = await readJsonObject(request, 4096);
|
|
15694
|
+
let values;
|
|
15695
|
+
try {
|
|
15696
|
+
values = parseAdditionalTrustedNetworks(body.extraAllowedCidrs);
|
|
15697
|
+
} catch {
|
|
15698
|
+
throw new HttpError(400, "lan_trusted_networks_invalid");
|
|
15699
|
+
}
|
|
15700
|
+
const write = trustedNetworksWrite.then(async () => {
|
|
15701
|
+
if (adminClosing) throw new HttpError(503, "unavailable");
|
|
15702
|
+
await saveManagedTrustedNetworks(loaded.setupFile, values);
|
|
15703
|
+
trustedNetworksRestartRequired = true;
|
|
15704
|
+
});
|
|
15705
|
+
trustedNetworksWrite = write.catch(() => void 0);
|
|
15706
|
+
await write;
|
|
15707
|
+
sendJson(response, 200, await trustedNetworksPayload(), false);
|
|
15708
|
+
return;
|
|
15709
|
+
}
|
|
13821
15710
|
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/diagnostics`) {
|
|
13822
15711
|
sendJson(response, 200, await diagnosticsPayload(), false);
|
|
13823
15712
|
return;
|
|
@@ -13945,6 +15834,63 @@ async function apply(ctx, config) {
|
|
|
13945
15834
|
sendJson(response, 200, remotePayload(), false);
|
|
13946
15835
|
return;
|
|
13947
15836
|
}
|
|
15837
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/caddy/component/install`) {
|
|
15838
|
+
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
15839
|
+
logger.info("caddy component install started");
|
|
15840
|
+
try {
|
|
15841
|
+
await remoteProviders.mutate(async () => caddyComponent.install());
|
|
15842
|
+
logger.info("caddy component install completed");
|
|
15843
|
+
} catch (error) {
|
|
15844
|
+
logger.error("caddy component install failed: %s", error instanceof Error ? error.stack ?? error.message : String(error));
|
|
15845
|
+
throw error;
|
|
15846
|
+
}
|
|
15847
|
+
sendJson(response, 200, remotePayload(), false);
|
|
15848
|
+
return;
|
|
15849
|
+
}
|
|
15850
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/caddy/settings`) {
|
|
15851
|
+
const body = await readJsonObject(request, 8192);
|
|
15852
|
+
if (body.connect !== void 0 && typeof body.connect !== "boolean") throw new HttpError(400, "bad_request");
|
|
15853
|
+
const configure = async () => {
|
|
15854
|
+
if (body.connect === true && !caddyComponent.status().installed) throw new HttpError(409, "caddy_component_unavailable");
|
|
15855
|
+
await caddyConfig.configure(body.settings, {
|
|
15856
|
+
secretId: body.secretId ?? "",
|
|
15857
|
+
secretKey: body.secretKey ?? ""
|
|
15858
|
+
});
|
|
15859
|
+
if (body.connect === true) {
|
|
15860
|
+
await originController.setEnabled(false);
|
|
15861
|
+
await originController.selectMode("managed");
|
|
15862
|
+
await originController.reconnect();
|
|
15863
|
+
} else if (originController.mode() === "managed" && originController.status().enabled) await originController.reconnect();
|
|
15864
|
+
};
|
|
15865
|
+
if (body.connect === true) await remoteProviders.mutateSelected("origin", configure);
|
|
15866
|
+
else await remoteProviders.mutate(configure);
|
|
15867
|
+
sendJson(response, 200, remotePayload(), false);
|
|
15868
|
+
return;
|
|
15869
|
+
}
|
|
15870
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/origin/mode`) {
|
|
15871
|
+
const body = await readJsonObject(request, 4096);
|
|
15872
|
+
if (body.mode !== "external" && body.mode !== "managed") throw new HttpError(400, "bad_request");
|
|
15873
|
+
const mode = body.mode;
|
|
15874
|
+
if (mode === "managed" && !caddyComponent.status().installed) throw new HttpError(409, "caddy_component_unavailable");
|
|
15875
|
+
await remoteProviders.mutate(async () => {
|
|
15876
|
+
await originController.selectMode(mode);
|
|
15877
|
+
});
|
|
15878
|
+
sendJson(response, 200, remotePayload(), false);
|
|
15879
|
+
return;
|
|
15880
|
+
}
|
|
15881
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/caddy/component/purge`) {
|
|
15882
|
+
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
15883
|
+
await remoteProviders.mutate(async () => {
|
|
15884
|
+
if (originController.mode() === "managed") {
|
|
15885
|
+
await originController.setEnabled(false);
|
|
15886
|
+
await originController.selectMode("external");
|
|
15887
|
+
}
|
|
15888
|
+
await caddyComponent.purge();
|
|
15889
|
+
await caddyConfig.purge();
|
|
15890
|
+
});
|
|
15891
|
+
sendJson(response, 200, remotePayload(), false);
|
|
15892
|
+
return;
|
|
15893
|
+
}
|
|
13948
15894
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cloudflared/tunnel`) {
|
|
13949
15895
|
const body = await readJsonObject(request, 8192);
|
|
13950
15896
|
await remoteProviders.mutate(async () => {
|
|
@@ -14003,7 +15949,8 @@ async function apply(ctx, config) {
|
|
|
14003
15949
|
const settings = frpConfig.settings();
|
|
14004
15950
|
if (settings === void 0) throw new HttpError(409, "frp_config_missing");
|
|
14005
15951
|
const check = await frpIngressSelfCheck(settings, remoteDeviceFile);
|
|
14006
|
-
const
|
|
15952
|
+
const entry = resolveFrpEntryProbe(settings);
|
|
15953
|
+
const [frpsReachable, entryReachable] = await Promise.all([probeTcpReachable(settings.serverAddress, settings.serverPort), probeTcpReachable(entry.host, entry.port)]);
|
|
14007
15954
|
sendJson(response, 200, {
|
|
14008
15955
|
...remotePayload(),
|
|
14009
15956
|
frpSelfCheck: {
|
|
@@ -14171,6 +16118,10 @@ async function apply(ctx, config) {
|
|
|
14171
16118
|
};
|
|
14172
16119
|
await ctx.effect(async () => {
|
|
14173
16120
|
const unregister = ctx.webServer.register(adminRoute);
|
|
16121
|
+
const stopAdmin = () => {
|
|
16122
|
+
adminClosing = true;
|
|
16123
|
+
unregister();
|
|
16124
|
+
};
|
|
14174
16125
|
const disposeMobileCommand = ctx.commands.register({
|
|
14175
16126
|
name: "mobile",
|
|
14176
16127
|
description: "定制手机界面或添加电脑能力",
|
|
@@ -14227,7 +16178,8 @@ async function apply(ctx, config) {
|
|
|
14227
16178
|
} catch (error) {
|
|
14228
16179
|
try {
|
|
14229
16180
|
await settleCleanupSteps([
|
|
14230
|
-
|
|
16181
|
+
stopAdmin,
|
|
16182
|
+
() => trustedNetworksWrite,
|
|
14231
16183
|
disposeMobileCommand,
|
|
14232
16184
|
disposeTaskEvents,
|
|
14233
16185
|
async () => {
|
|
@@ -14245,7 +16197,8 @@ async function apply(ctx, config) {
|
|
|
14245
16197
|
}
|
|
14246
16198
|
return async () => {
|
|
14247
16199
|
await settleCleanupSteps([
|
|
14248
|
-
|
|
16200
|
+
stopAdmin,
|
|
16201
|
+
() => trustedNetworksWrite,
|
|
14249
16202
|
disposeMobileCommand,
|
|
14250
16203
|
disposeTaskEvents,
|
|
14251
16204
|
async () => {
|
|
@@ -14260,6 +16213,6 @@ async function apply(ctx, config) {
|
|
|
14260
16213
|
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
14261
16214
|
}
|
|
14262
16215
|
//#endregion
|
|
14263
|
-
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
16216
|
+
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
14264
16217
|
|
|
14265
16218
|
//# sourceMappingURL=index.mjs.map
|