@linxin666/dsh-remote-web-ui 0.3.12 → 0.3.14
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/README.i18n.yaml +2 -2
- package/README.md +26 -6
- package/README.zh.md +26 -6
- package/lib/client.js +77 -12
- package/lib/client.js.map +1 -1
- package/lib/index.js +412 -52
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +1 -0
- package/lib/types/client/RemotePanel.d.ts +3 -1
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +1 -1
- package/lib/types/client/RemoteSettingsCard.d.ts +16 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +6 -2
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +16 -4
- package/lib/types/client/pair-api.d.ts +10 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/remote-channel.d.ts +0 -2
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +0 -4
- package/lib/types/index.d.ts +28 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +138 -23
- package/lib/types/pairing.d.ts +17 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -0
- package/lib/types/relay-registry.d.ts +97 -0
- package/lib/types/relay-registry.d.ts.map +1 -0
- package/lib/types/relay-registry.js +191 -0
- package/lib/types/remote-api.d.ts +1 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +1 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +0 -1
- package/lib/types/remote-channel-rules.d.ts +0 -1
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +0 -1
- package/lib/types/remote-methods.d.ts +1 -4
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -5
- package/lib/types/tunnel-plan.d.ts +36 -0
- package/lib/types/tunnel-plan.d.ts.map +1 -0
- package/lib/types/tunnel-plan.js +43 -0
- package/lib/types/tunnel.d.ts +66 -13
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +93 -21
- package/package.json +18 -18
- package/src/client/RemoteEntry.tsx +2 -1
- package/src/client/RemotePanel.tsx +11 -0
- package/src/client/RemoteSettingsCard.tsx +43 -1
- package/src/client/locales.ts +16 -4
- package/src/client/pair-api.ts +11 -0
- package/src/client/remote-channel.ts +0 -3
- package/src/client/settings-card.module.css +14 -8
- package/src/index.ts +153 -25
- package/src/pairing.ts +30 -0
- package/src/relay-registry.ts +232 -0
- package/src/remote-api.ts +0 -1
- package/src/remote-channel-boot.ts +0 -1
- package/src/remote-channel-rules.ts +0 -2
- package/src/remote-methods.ts +1 -6
- package/src/tunnel-plan.ts +51 -0
- package/src/tunnel.ts +120 -24
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { z as z$1 } from "zod";
|
|
|
11
11
|
import http, { request } from "node:http";
|
|
12
12
|
import { connect } from "node:net";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { EventEmitter } from "node:events";
|
|
14
15
|
import { Tunnel, bin, install } from "cloudflared";
|
|
15
16
|
//#region src/pairing.ts
|
|
16
17
|
/**
|
|
@@ -86,6 +87,7 @@ var PairingService = class {
|
|
|
86
87
|
publicBase;
|
|
87
88
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
88
89
|
tunnelStatus;
|
|
90
|
+
relayStatus;
|
|
89
91
|
posture;
|
|
90
92
|
/** True when lastSeenAt changed since the last persist (flushed on sweep). */
|
|
91
93
|
dirty = false;
|
|
@@ -201,6 +203,11 @@ var PairingService = class {
|
|
|
201
203
|
this.tunnelStatus = status;
|
|
202
204
|
this.notify();
|
|
203
205
|
}
|
|
206
|
+
/** Set or clear the relay-registry status frame (undefined when not in play). */
|
|
207
|
+
setRelayStatus(status) {
|
|
208
|
+
this.relayStatus = status;
|
|
209
|
+
this.notify();
|
|
210
|
+
}
|
|
204
211
|
/** Set the latest /api posture probe result (see posture.ts). */
|
|
205
212
|
setPosture(snapshot) {
|
|
206
213
|
this.posture = snapshot;
|
|
@@ -349,6 +356,7 @@ var PairingService = class {
|
|
|
349
356
|
lanAddresses: [...this.lanBases.keys()],
|
|
350
357
|
...this.publicBase !== void 0 ? { publicUrl: this.publicBase } : {},
|
|
351
358
|
...this.tunnelStatus !== void 0 ? { tunnel: this.tunnelStatus } : {},
|
|
359
|
+
...this.relayStatus !== void 0 ? { relay: this.relayStatus } : {},
|
|
352
360
|
...this.posture !== void 0 ? { posture: this.posture } : {},
|
|
353
361
|
...token !== void 0 ? {
|
|
354
362
|
tokenId: token.record.id,
|
|
@@ -430,7 +438,7 @@ var PairingService = class {
|
|
|
430
438
|
};
|
|
431
439
|
/** Structural equality over the snapshot's wire fields. */
|
|
432
440
|
function snapshotsEqual(a, b) {
|
|
433
|
-
return a.phase === b.phase && a.lanAvailable === b.lanAvailable && sameStrings(a.lanAddresses, b.lanAddresses) && a.publicUrl === b.publicUrl && tunnelEqual(a.tunnel, b.tunnel) && a.tokenId === b.tokenId && a.tokenExpiresAt === b.tokenExpiresAt && a.deviceCount === b.deviceCount && a.onlineCount === b.onlineCount && devicesEqual(a.devices, b.devices);
|
|
441
|
+
return a.phase === b.phase && a.lanAvailable === b.lanAvailable && sameStrings(a.lanAddresses, b.lanAddresses) && a.publicUrl === b.publicUrl && tunnelEqual(a.tunnel, b.tunnel) && relayEqual(a.relay, b.relay) && a.tokenId === b.tokenId && a.tokenExpiresAt === b.tokenExpiresAt && a.deviceCount === b.deviceCount && a.onlineCount === b.onlineCount && devicesEqual(a.devices, b.devices);
|
|
434
442
|
}
|
|
435
443
|
/** Per-device roster equality (order is pairing time). */
|
|
436
444
|
function devicesEqual(a, b) {
|
|
@@ -443,6 +451,10 @@ function devicesEqual(a, b) {
|
|
|
443
451
|
function tunnelEqual(a, b) {
|
|
444
452
|
return a === b || a !== void 0 && b !== void 0 && a.state === b.state && a.url === b.url && a.error === b.error;
|
|
445
453
|
}
|
|
454
|
+
/** Relay frame equality (same shape as the tunnel frame). */
|
|
455
|
+
function relayEqual(a, b) {
|
|
456
|
+
return a === b || a !== void 0 && b !== void 0 && a.state === b.state && a.url === b.url && a.error === b.error;
|
|
457
|
+
}
|
|
446
458
|
/** Element-wise string list equality (interface order is meaningful). */
|
|
447
459
|
function sameStrings(a, b) {
|
|
448
460
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
@@ -1565,8 +1577,6 @@ const REMOTE_UPGRADE_PATHS = [
|
|
|
1565
1577
|
];
|
|
1566
1578
|
/** Plugin-manager HTTP prefix: install/remove stay physically local. */
|
|
1567
1579
|
const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
|
|
1568
|
-
/** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
|
|
1569
|
-
const DESKTOP_LAUNCHER_PATH = "/api/dsh-desktop-launcher";
|
|
1570
1580
|
/**
|
|
1571
1581
|
* The cookieless device credential: the boot patch reads the device id from
|
|
1572
1582
|
* the /pair-app URL, keeps it in sessionStorage, and attaches it to every
|
|
@@ -1583,14 +1593,12 @@ const REMOTE_DEVICE_QUERY = "device";
|
|
|
1583
1593
|
* paired remote desktop may use the full host API (chat, sessions,
|
|
1584
1594
|
* settings, credentials, presets — it is a full-control credential), but it
|
|
1585
1595
|
* must not reach the machine-control planes: pairing control itself, the
|
|
1586
|
-
* dsh-web self-update installer, plugin install/remove
|
|
1587
|
-
* launcher actions (host shutdown, shortcuts).
|
|
1596
|
+
* dsh-web self-update installer, and plugin install/remove.
|
|
1588
1597
|
*/
|
|
1589
1598
|
const LOCAL_ONLY_PREFIXES = [
|
|
1590
1599
|
"/api/pair",
|
|
1591
1600
|
"/api/update",
|
|
1592
|
-
PLUGIN_MANAGER_PATH
|
|
1593
|
-
DESKTOP_LAUNCHER_PATH
|
|
1601
|
+
PLUGIN_MANAGER_PATH
|
|
1594
1602
|
];
|
|
1595
1603
|
/**
|
|
1596
1604
|
* Whether a paired inner path must stay physically local.
|
|
@@ -2308,6 +2316,224 @@ function writeLanBind(host, port, profile, home = dshHome()) {
|
|
|
2308
2316
|
renameSync(temp, file);
|
|
2309
2317
|
}
|
|
2310
2318
|
//#endregion
|
|
2319
|
+
//#region src/tunnel-plan.ts
|
|
2320
|
+
/**
|
|
2321
|
+
* Pure planner for the settings-sync tunnel branch: given the resolved
|
|
2322
|
+
* settings slice, decide which tunnel mode runs and with what target. Kept
|
|
2323
|
+
* free of cordis types so the precedence matrix is unit-testable without a
|
|
2324
|
+
* host context.
|
|
2325
|
+
*/
|
|
2326
|
+
/** Whether a configured public base is a parseable http(s) URL with a host. */
|
|
2327
|
+
function isHttpUrl(value) {
|
|
2328
|
+
try {
|
|
2329
|
+
const url = new URL(value);
|
|
2330
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.hostname !== "";
|
|
2331
|
+
} catch {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Decide the tunnel mode and target from one resolved settings value.
|
|
2337
|
+
* Precedence: the quick tunnel wins when `autoTunnel` is on (anything else
|
|
2338
|
+
* configured is reported as ignored); the named tunnel runs only with a
|
|
2339
|
+
* token AND a valid public hostname (the token does not carry the hostname,
|
|
2340
|
+
* so the QR base and the fence trust would have no source without it);
|
|
2341
|
+
* otherwise no tunnel and the manual public base applies.
|
|
2342
|
+
* @param value - the resolved settings slice that drives the tunnel.
|
|
2343
|
+
* @param port - the local webServer port the tunnel forwards to.
|
|
2344
|
+
*/
|
|
2345
|
+
function tunnelPlanOf(value, port) {
|
|
2346
|
+
if (value.autoTunnel === true) {
|
|
2347
|
+
const ignored = [];
|
|
2348
|
+
if (value.tunnelToken !== void 0 && value.tunnelToken !== "") ignored.push("tunnelToken");
|
|
2349
|
+
if (value.publicBaseUrl !== void 0 && value.publicBaseUrl !== "") ignored.push("publicBaseUrl");
|
|
2350
|
+
return {
|
|
2351
|
+
mode: "quick",
|
|
2352
|
+
targetUrl: `http://127.0.0.1:${String(port)}`,
|
|
2353
|
+
ignored
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
if (value.tunnelToken !== void 0 && value.tunnelToken !== "") {
|
|
2357
|
+
if (value.publicBaseUrl !== void 0 && isHttpUrl(value.publicBaseUrl)) return {
|
|
2358
|
+
mode: "named",
|
|
2359
|
+
token: value.tunnelToken,
|
|
2360
|
+
publicUrl: value.publicBaseUrl
|
|
2361
|
+
};
|
|
2362
|
+
return { mode: "off" };
|
|
2363
|
+
}
|
|
2364
|
+
return { mode: "off" };
|
|
2365
|
+
}
|
|
2366
|
+
const RELAY_UNREGISTER_URL = "https://dsh-market.com/api/relay/unregister";
|
|
2367
|
+
/**
|
|
2368
|
+
* The public origin template: one stable single-label subdomain per
|
|
2369
|
+
* registration id. Single label on purpose — Universal SSL covers exactly
|
|
2370
|
+
* one subdomain level, so a two-level hostname would get no certificate.
|
|
2371
|
+
*/
|
|
2372
|
+
const RELAY_BASE_SUFFIX = ".dsh-market.com";
|
|
2373
|
+
const RELAY_ID_RE = /^[a-z0-9]{16}$/;
|
|
2374
|
+
const RELAY_SECRET_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
2375
|
+
/** Registry storage layout: one file per profile under the relay directory. */
|
|
2376
|
+
function relayIdentityFile(profile, home = dshHome()) {
|
|
2377
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) throw new Error(`relay registry: profile ${JSON.stringify(profile)} is not a safe path segment`);
|
|
2378
|
+
return join(home, "remote-web-ui-registry", `${profile}.json`);
|
|
2379
|
+
}
|
|
2380
|
+
/** Generate one identity pair (id: 8-byte hex slug; secret: 32-byte base64url). */
|
|
2381
|
+
function generateRelayIdentity(random = randomBytes) {
|
|
2382
|
+
return {
|
|
2383
|
+
id: random(8).toString("hex"),
|
|
2384
|
+
secret: random(32).toString("base64url"),
|
|
2385
|
+
fresh: true
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Load (or lazily mint) the per-profile relay identity. The file is written
|
|
2390
|
+
* owner-only on first mint; a corrupt file is reminted (the phone then
|
|
2391
|
+
* re-pairs once — the same cost as losing the devices file).
|
|
2392
|
+
*/
|
|
2393
|
+
function loadRelayIdentity(profile, home = dshHome(), random = randomBytes) {
|
|
2394
|
+
const file = relayIdentityFile(profile, home);
|
|
2395
|
+
try {
|
|
2396
|
+
if (existsSync(file)) {
|
|
2397
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
2398
|
+
if (typeof parsed.id === "string" && RELAY_ID_RE.test(parsed.id) && typeof parsed.secret === "string" && RELAY_SECRET_RE.test(parsed.secret)) return {
|
|
2399
|
+
id: parsed.id,
|
|
2400
|
+
secret: parsed.secret
|
|
2401
|
+
};
|
|
2402
|
+
console.warn(`relay registry: ${file} is malformed — reminting the identity (paired devices must scan the QR once more)`);
|
|
2403
|
+
}
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
console.warn(`relay registry: could not read ${file} (${error instanceof Error ? error.message : String(error)}) — reminting`);
|
|
2406
|
+
}
|
|
2407
|
+
const identity = generateRelayIdentity(random);
|
|
2408
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
2409
|
+
writeFileSync(file, JSON.stringify({
|
|
2410
|
+
id: identity.id,
|
|
2411
|
+
secret: identity.secret
|
|
2412
|
+
}, null, 2) + "\n", { mode: 384 });
|
|
2413
|
+
return identity;
|
|
2414
|
+
}
|
|
2415
|
+
/** Build the registration request body for one identity + tunnel target. */
|
|
2416
|
+
function registrationBody(identity, target) {
|
|
2417
|
+
return JSON.stringify({
|
|
2418
|
+
id: identity.id,
|
|
2419
|
+
secret: identity.secret,
|
|
2420
|
+
new_secret: identity.fresh === true ? identity.secret : void 0,
|
|
2421
|
+
target
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
/** Best-effort removal of the mapping (used on disable; errors are swallowed). */
|
|
2425
|
+
async function unregisterRelay(identity, endpoint = RELAY_UNREGISTER_URL, fetchFn = fetch) {
|
|
2426
|
+
await fetchFn(endpoint, {
|
|
2427
|
+
method: "POST",
|
|
2428
|
+
headers: { "content-type": "application/json" },
|
|
2429
|
+
body: JSON.stringify({
|
|
2430
|
+
id: identity.id,
|
|
2431
|
+
secret: identity.secret
|
|
2432
|
+
})
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* The stable relay base for one identity — the origin the QR and the phone
|
|
2437
|
+
* bookmark use while (or after) the registration is accepted.
|
|
2438
|
+
*/
|
|
2439
|
+
function relayBaseOf(identity) {
|
|
2440
|
+
return `https://${identity.id}${RELAY_BASE_SUFFIX}`;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Registration loop with capped exponential backoff. Re-registration happens
|
|
2444
|
+
* on every tunnel start (the quick URL can change after a crash-restart);
|
|
2445
|
+
* failures retry with backoff and surface the last error to the card.
|
|
2446
|
+
*/
|
|
2447
|
+
var RelayRegistrar = class {
|
|
2448
|
+
identity;
|
|
2449
|
+
setState;
|
|
2450
|
+
options;
|
|
2451
|
+
timer;
|
|
2452
|
+
attempts = 0;
|
|
2453
|
+
lastError;
|
|
2454
|
+
disposed = false;
|
|
2455
|
+
constructor(identity, setState, options = {}) {
|
|
2456
|
+
this.identity = identity;
|
|
2457
|
+
this.setState = setState;
|
|
2458
|
+
this.options = options;
|
|
2459
|
+
}
|
|
2460
|
+
/** The stable relay base for the QR while registered. */
|
|
2461
|
+
get baseUrl() {
|
|
2462
|
+
return relayBaseOf(this.identity);
|
|
2463
|
+
}
|
|
2464
|
+
/** The last registration failure detail (undefined while healthy). */
|
|
2465
|
+
get error() {
|
|
2466
|
+
return this.lastError;
|
|
2467
|
+
}
|
|
2468
|
+
/** Push one tunnel target; resolves the stable base once accepted. */
|
|
2469
|
+
async announce(target) {
|
|
2470
|
+
if (this.disposed) return void 0;
|
|
2471
|
+
if (this.timer !== void 0) {
|
|
2472
|
+
clearTimeout(this.timer);
|
|
2473
|
+
this.timer = void 0;
|
|
2474
|
+
}
|
|
2475
|
+
this.setState({ state: "registering" });
|
|
2476
|
+
try {
|
|
2477
|
+
const response = await (this.options.fetchFn ?? fetch)(this.options.registerUrl ?? "https://dsh-market.com/api/relay/register", {
|
|
2478
|
+
method: "PUT",
|
|
2479
|
+
headers: { "content-type": "application/json" },
|
|
2480
|
+
body: registrationBody(this.identity, target)
|
|
2481
|
+
});
|
|
2482
|
+
if (!response.ok) {
|
|
2483
|
+
const detail = await response.json().catch(() => void 0);
|
|
2484
|
+
const error = detail && typeof detail.error === "string" ? detail.error : `HTTP ${String(response.status)}`;
|
|
2485
|
+
throw new Error(error);
|
|
2486
|
+
}
|
|
2487
|
+
this.identity.fresh = false;
|
|
2488
|
+
this.attempts = 0;
|
|
2489
|
+
this.lastError = void 0;
|
|
2490
|
+
this.setState({
|
|
2491
|
+
state: "running",
|
|
2492
|
+
url: this.baseUrl
|
|
2493
|
+
});
|
|
2494
|
+
return this.baseUrl;
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
2497
|
+
if (this.lastError === "invalid-params" && this.identity.fresh !== true) this.identity.fresh = true;
|
|
2498
|
+
this.setState({
|
|
2499
|
+
state: "failed",
|
|
2500
|
+
error: this.lastError
|
|
2501
|
+
});
|
|
2502
|
+
this.scheduleRetry(target);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
/** Best-effort removal of the registry row (the relay toggle turned off). */
|
|
2507
|
+
async unregister() {
|
|
2508
|
+
await unregisterRelay(this.identity, this.options.unregisterUrl, this.options.fetchFn);
|
|
2509
|
+
}
|
|
2510
|
+
scheduleRetry(target) {
|
|
2511
|
+
if (this.disposed) return;
|
|
2512
|
+
const base = this.options.baseDelayMs ?? 5e3;
|
|
2513
|
+
const max = this.options.maxDelayMs ?? 6e4;
|
|
2514
|
+
const delay = Math.min(base * 2 ** this.attempts, max);
|
|
2515
|
+
this.attempts += 1;
|
|
2516
|
+
const timerFn = this.options.timerFn ?? ((fn, ms) => {
|
|
2517
|
+
const timer = setTimeout(fn, ms);
|
|
2518
|
+
timer.unref?.();
|
|
2519
|
+
return timer;
|
|
2520
|
+
});
|
|
2521
|
+
this.timer = timerFn(() => {
|
|
2522
|
+
this.timer = void 0;
|
|
2523
|
+
this.announce(target);
|
|
2524
|
+
}, delay);
|
|
2525
|
+
}
|
|
2526
|
+
/** Stop retrying and clear the card state (tunnel stopped or disabled). */
|
|
2527
|
+
dispose() {
|
|
2528
|
+
this.disposed = true;
|
|
2529
|
+
if (this.timer !== void 0) {
|
|
2530
|
+
clearTimeout(this.timer);
|
|
2531
|
+
this.timer = void 0;
|
|
2532
|
+
}
|
|
2533
|
+
this.setState({ state: "off" });
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
//#endregion
|
|
2311
2537
|
//#region src/lan-bind-plan.ts
|
|
2312
2538
|
/**
|
|
2313
2539
|
* The bind host the managed block should pin: an explicit CLI --host wins,
|
|
@@ -2406,11 +2632,14 @@ function createInnerAuth(launchUrl, fetchImpl = fetch) {
|
|
|
2406
2632
|
//#endregion
|
|
2407
2633
|
//#region src/tunnel.ts
|
|
2408
2634
|
/**
|
|
2409
|
-
* Auto-tunnel manager: spawns a Cloudflare
|
|
2410
|
-
*
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2635
|
+
* Auto-tunnel manager: spawns a Cloudflare tunnel through the `cloudflared`
|
|
2636
|
+
* npm package — its postinstall downloads the platform binary, so no
|
|
2637
|
+
* user-side tooling is involved — surfaces the public URL, and restarts the
|
|
2638
|
+
* process after unexpected exits with exponential backoff. Two modes: the
|
|
2639
|
+
* accountless quick tunnel (`https://xxx.trycloudflare.com`, hostname minted
|
|
2640
|
+
* per start) and an account named tunnel (`cloudflared tunnel run --token`,
|
|
2641
|
+
* fixed public hostname — the mode that lets a paired phone keep its
|
|
2642
|
+
* bookmark and pairing cookie across restarts).
|
|
2414
2643
|
*
|
|
2415
2644
|
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
2416
2645
|
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
@@ -2418,17 +2647,75 @@ function createInnerAuth(launchUrl, fetchImpl = fetch) {
|
|
|
2418
2647
|
* timers — are injectable so the whole lifecycle is unit-testable without
|
|
2419
2648
|
* a real binary or network.
|
|
2420
2649
|
*/
|
|
2650
|
+
/** Compare two targets for the start idempotence check. */
|
|
2651
|
+
function sameTarget(left, right) {
|
|
2652
|
+
if (left === void 0) return false;
|
|
2653
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Wrap a named-tunnel process so it fits the quick-tunnel handle shape: the
|
|
2657
|
+
* fixed public URL is reported through the same `url` event once the first
|
|
2658
|
+
* edge connection registers (cloudflared fires `connected` per connection,
|
|
2659
|
+
* so only the first is taken), and exit/error pass through. The manager's
|
|
2660
|
+
* URL timeout, crash-restart backoff, and stop semantics then stay fully
|
|
2661
|
+
* mode-agnostic.
|
|
2662
|
+
* @param inner - the running named-tunnel process.
|
|
2663
|
+
* @param publicUrl - the fixed public hostname ingress maps to this server.
|
|
2664
|
+
* @returns a handle emitting `url` once the tunnel is reachable.
|
|
2665
|
+
*/
|
|
2666
|
+
function namedTunnelHandle(inner, publicUrl) {
|
|
2667
|
+
const handle = new EventEmitter();
|
|
2668
|
+
let registered = false;
|
|
2669
|
+
const onConnected = () => {
|
|
2670
|
+
if (registered) return;
|
|
2671
|
+
registered = true;
|
|
2672
|
+
handle.emit("url", publicUrl);
|
|
2673
|
+
};
|
|
2674
|
+
const onExit = (code, signal) => {
|
|
2675
|
+
handle.emit("exit", code, signal);
|
|
2676
|
+
};
|
|
2677
|
+
const onError = (value) => {
|
|
2678
|
+
handle.emit("error", value);
|
|
2679
|
+
};
|
|
2680
|
+
inner.on("connected", onConnected);
|
|
2681
|
+
inner.on("exit", onExit);
|
|
2682
|
+
inner.on("error", onError);
|
|
2683
|
+
handle.stop = () => {
|
|
2684
|
+
inner.off("connected", onConnected);
|
|
2685
|
+
inner.off("exit", onExit);
|
|
2686
|
+
inner.off("error", onError);
|
|
2687
|
+
return inner.stop();
|
|
2688
|
+
};
|
|
2689
|
+
return handle;
|
|
2690
|
+
}
|
|
2421
2691
|
/** Default binary readiness: download the platform binary on first use. */
|
|
2422
2692
|
async function defaultEnsureBinary() {
|
|
2423
2693
|
if (existsSync(bin)) return;
|
|
2424
2694
|
await install(bin);
|
|
2425
2695
|
}
|
|
2426
|
-
/**
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2696
|
+
/** The flags every tunnel mode shares (see the quick factory comment). */
|
|
2697
|
+
const SHARED_TUNNEL_FLAGS = {
|
|
2698
|
+
"--no-autoupdate": true,
|
|
2699
|
+
"--protocol": "http2"
|
|
2700
|
+
};
|
|
2701
|
+
/**
|
|
2702
|
+
* Spawn flags for one quick target. `originHostHeader` stamps the Host the
|
|
2703
|
+
* local webserver sees: the plugin's phone-facing fence trusts the configured
|
|
2704
|
+
* public host (the stable relay origin, Host-bound) and the harness
|
|
2705
|
+
* browser-auth binds cookies to the Host authority, while the Workers relay
|
|
2706
|
+
* cannot control the origin-side Host at all (fetch forces it to the URL
|
|
2707
|
+
* authority) — so the connector must restamp it with the stable origin.
|
|
2708
|
+
*/
|
|
2709
|
+
function quickTunnelFlags(originHostHeader) {
|
|
2710
|
+
return originHostHeader === void 0 ? { ...SHARED_TUNNEL_FLAGS } : {
|
|
2711
|
+
...SHARED_TUNNEL_FLAGS,
|
|
2712
|
+
"--http-host-header": originHostHeader
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
/** Default factory: the cloudflared package's quick and named tunnels. */
|
|
2716
|
+
function defaultFactory(target) {
|
|
2717
|
+
if (target.kind === "quick") return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader));
|
|
2718
|
+
return namedTunnelHandle(Tunnel.withToken(target.token, { ...SHARED_TUNNEL_FLAGS }), target.publicUrl);
|
|
2432
2719
|
}
|
|
2433
2720
|
/** Node timers. */
|
|
2434
2721
|
const nodeTimer = {
|
|
@@ -2449,7 +2736,7 @@ var TunnelManager = class {
|
|
|
2449
2736
|
phase = "stopped";
|
|
2450
2737
|
url;
|
|
2451
2738
|
error;
|
|
2452
|
-
|
|
2739
|
+
target;
|
|
2453
2740
|
handle;
|
|
2454
2741
|
urlTimer;
|
|
2455
2742
|
restartTimer;
|
|
@@ -2478,16 +2765,24 @@ var TunnelManager = class {
|
|
|
2478
2765
|
};
|
|
2479
2766
|
}
|
|
2480
2767
|
/**
|
|
2481
|
-
* Start (or keep) a
|
|
2482
|
-
*
|
|
2483
|
-
*
|
|
2484
|
-
*
|
|
2768
|
+
* Start (or keep) a tunnel toward `target`. Restarting with a different
|
|
2769
|
+
* target tears the old tunnel down first; restarting with the same target
|
|
2770
|
+
* while running is a no-op. A string is the quick mode's local target URL.
|
|
2771
|
+
*
|
|
2772
|
+
* In quick mode the URL surfaces when cloudflared mints the ephemeral
|
|
2773
|
+
* trycloudflare hostname; in named mode the fixed `publicUrl` surfaces
|
|
2774
|
+
* once the first edge connection registers (see {@link namedTunnelHandle}).
|
|
2775
|
+
* @param target - what to run (a string means quick toward that local URL).
|
|
2485
2776
|
*/
|
|
2486
|
-
start(
|
|
2487
|
-
|
|
2777
|
+
start(target) {
|
|
2778
|
+
const resolved = typeof target === "string" ? {
|
|
2779
|
+
kind: "quick",
|
|
2780
|
+
targetUrl: target
|
|
2781
|
+
} : target;
|
|
2782
|
+
if (sameTarget(this.target, resolved) && (this.phase === "starting" || this.phase === "running")) return;
|
|
2488
2783
|
this.teardown();
|
|
2489
2784
|
this.stopping = false;
|
|
2490
|
-
this.
|
|
2785
|
+
this.target = resolved;
|
|
2491
2786
|
this.attempts = 0;
|
|
2492
2787
|
this.generation += 1;
|
|
2493
2788
|
this.attempt();
|
|
@@ -2496,7 +2791,7 @@ var TunnelManager = class {
|
|
|
2496
2791
|
stop() {
|
|
2497
2792
|
this.teardown();
|
|
2498
2793
|
this.stopping = false;
|
|
2499
|
-
this.
|
|
2794
|
+
this.target = void 0;
|
|
2500
2795
|
this.setPhase("stopped");
|
|
2501
2796
|
}
|
|
2502
2797
|
/** Alias of {@link stop} for plugin-effect disposal. */
|
|
@@ -2518,15 +2813,15 @@ var TunnelManager = class {
|
|
|
2518
2813
|
};
|
|
2519
2814
|
}
|
|
2520
2815
|
attempt() {
|
|
2521
|
-
if (this.stopping || this.
|
|
2816
|
+
if (this.stopping || this.target === void 0) return;
|
|
2522
2817
|
const gen = this.generation;
|
|
2523
2818
|
this.setPhase("starting");
|
|
2524
2819
|
this.handle = void 0;
|
|
2525
2820
|
this.url = void 0;
|
|
2526
2821
|
this.error = void 0;
|
|
2527
2822
|
this.ensureBinary().then(() => {
|
|
2528
|
-
if (this.stopping || this.
|
|
2529
|
-
const handle = this.factory(this.
|
|
2823
|
+
if (this.stopping || this.target === void 0 || gen !== this.generation) return;
|
|
2824
|
+
const handle = this.factory(this.target);
|
|
2530
2825
|
this.handle = handle;
|
|
2531
2826
|
this.urlTimer = this.timer.setTimeout(() => {
|
|
2532
2827
|
this.fail("timed out waiting for the tunnel URL");
|
|
@@ -2544,7 +2839,7 @@ var TunnelManager = class {
|
|
|
2544
2839
|
this.error = value instanceof Error ? value.message : String(value);
|
|
2545
2840
|
});
|
|
2546
2841
|
}).catch((value) => {
|
|
2547
|
-
if (this.stopping || this.
|
|
2842
|
+
if (this.stopping || this.target === void 0 || gen !== this.generation) return;
|
|
2548
2843
|
const message = value instanceof Error ? value.message : String(value);
|
|
2549
2844
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
|
2550
2845
|
});
|
|
@@ -3281,7 +3576,6 @@ const REMOTE_CHANNEL_RULES = {
|
|
|
3281
3576
|
apiPrefix: "/api/",
|
|
3282
3577
|
pairPrefix: "/api/pair/",
|
|
3283
3578
|
updatePrefix: "/api/update/",
|
|
3284
|
-
desktopLauncherPrefix: "/api/dsh-desktop-launcher",
|
|
3285
3579
|
settingsBridgePrefix: "/api/dsh-web-ui-settings",
|
|
3286
3580
|
sidebarPrefix: "/sidebar/",
|
|
3287
3581
|
gitPrefix: "/git/",
|
|
@@ -3331,7 +3625,7 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
|
3331
3625
|
function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
3332
3626
|
const json = JSON.stringify(rules);
|
|
3333
3627
|
const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
|
|
3334
|
-
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.
|
|
3628
|
+
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
3335
3629
|
}
|
|
3336
3630
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
3337
3631
|
const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
|
|
@@ -3418,6 +3712,8 @@ const Config = z.object({
|
|
|
3418
3712
|
publicBaseUrl: z.string(),
|
|
3419
3713
|
devicesFile: z.string(),
|
|
3420
3714
|
autoTunnel: z.boolean().default(false),
|
|
3715
|
+
tunnelToken: z.string().role("secret"),
|
|
3716
|
+
relay: z.boolean().default(true),
|
|
3421
3717
|
lanBind: z.boolean(),
|
|
3422
3718
|
profile: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
|
|
3423
3719
|
enabled: z.boolean().default(true)
|
|
@@ -3455,6 +3751,8 @@ const DEFAULTS = {
|
|
|
3455
3751
|
publicBaseUrl: void 0,
|
|
3456
3752
|
devicesFile: defaultDevicesFile(),
|
|
3457
3753
|
autoTunnel: false,
|
|
3754
|
+
tunnelToken: void 0,
|
|
3755
|
+
relay: true,
|
|
3458
3756
|
lanBind: void 0,
|
|
3459
3757
|
profile: process.env.DSH_PROFILE ?? "web",
|
|
3460
3758
|
enabled: true
|
|
@@ -3476,6 +3774,8 @@ function applyImpl(ctx, config) {
|
|
|
3476
3774
|
publicBaseUrl: config?.publicBaseUrl,
|
|
3477
3775
|
devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
|
|
3478
3776
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
3777
|
+
tunnelToken: config?.tunnelToken,
|
|
3778
|
+
relay: config?.relay ?? DEFAULTS.relay,
|
|
3479
3779
|
lanBind: config?.lanBind,
|
|
3480
3780
|
profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
3481
3781
|
enabled: config?.enabled ?? DEFAULTS.enabled
|
|
@@ -3493,6 +3793,8 @@ function applyImpl(ctx, config) {
|
|
|
3493
3793
|
publicBaseUrl: value.publicBaseUrl,
|
|
3494
3794
|
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
3495
3795
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
3796
|
+
tunnelToken: value.tunnelToken,
|
|
3797
|
+
relay: value.relay ?? DEFAULTS.relay,
|
|
3496
3798
|
lanBind: value.lanBind,
|
|
3497
3799
|
profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
3498
3800
|
enabled: value.enabled ?? DEFAULTS.enabled
|
|
@@ -3500,21 +3802,64 @@ function applyImpl(ctx, config) {
|
|
|
3500
3802
|
};
|
|
3501
3803
|
const service = new PairingService(pairingConfigOf(resolved));
|
|
3502
3804
|
const tunnel = new TunnelManager();
|
|
3503
|
-
let
|
|
3805
|
+
let relayRegistrar;
|
|
3806
|
+
let relayUrl;
|
|
3807
|
+
let rawTunnelUrl;
|
|
3808
|
+
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
3809
|
+
let relayAnnouncedFor;
|
|
3810
|
+
const setPublicBase = () => {
|
|
3811
|
+
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
3812
|
+
};
|
|
3813
|
+
const disposeRelayRegistrar = (unregister = false) => {
|
|
3814
|
+
const registrar = relayRegistrar;
|
|
3815
|
+
relayRegistrar = void 0;
|
|
3816
|
+
relayUrl = void 0;
|
|
3817
|
+
relayAnnouncedFor = void 0;
|
|
3818
|
+
if (registrar === void 0) return;
|
|
3819
|
+
if (unregister) registrar.unregister().catch(() => void 0);
|
|
3820
|
+
registrar.dispose();
|
|
3821
|
+
};
|
|
3822
|
+
const ensureRelayRegistrar = () => {
|
|
3823
|
+
if (resolve().relay === false) return void 0;
|
|
3824
|
+
if (relayRegistrar === void 0) try {
|
|
3825
|
+
relayRegistrar = new RelayRegistrar(loadRelayIdentity(resolve().profile), (state) => {
|
|
3826
|
+
service.setRelayStatus(state.state === "off" ? void 0 : state);
|
|
3827
|
+
if (state.state === "running") relayUrl = state.url;
|
|
3828
|
+
else if (state.state === "off") relayUrl = void 0;
|
|
3829
|
+
else if (state.state === "failed") console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
3830
|
+
setPublicBase();
|
|
3831
|
+
});
|
|
3832
|
+
} catch (error) {
|
|
3833
|
+
console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
|
|
3834
|
+
}
|
|
3835
|
+
return relayRegistrar;
|
|
3836
|
+
};
|
|
3837
|
+
const announceRelay = (registrar, target) => {
|
|
3838
|
+
if (relayAnnouncedFor === target) return;
|
|
3839
|
+
relayAnnouncedFor = target;
|
|
3840
|
+
registrar.announce(target);
|
|
3841
|
+
};
|
|
3842
|
+
let tunnelMode = resolved.autoTunnel ? "quick" : "off";
|
|
3504
3843
|
tunnel.onPhase((info) => {
|
|
3505
|
-
if (
|
|
3844
|
+
if (tunnelMode === "off") return;
|
|
3506
3845
|
if (info.phase === "running" && info.url !== void 0) {
|
|
3507
|
-
|
|
3846
|
+
rawTunnelUrl = info.url;
|
|
3847
|
+
setPublicBase();
|
|
3508
3848
|
service.setTunnelStatus({
|
|
3509
3849
|
state: "running",
|
|
3510
3850
|
url: info.url
|
|
3511
3851
|
});
|
|
3852
|
+
const registrar = tunnelMode === "quick" ? ensureRelayRegistrar() : void 0;
|
|
3853
|
+
if (registrar !== void 0) announceRelay(registrar, info.url);
|
|
3512
3854
|
runPostureProbe();
|
|
3513
3855
|
} else if (info.phase === "starting") {
|
|
3514
|
-
|
|
3856
|
+
rawTunnelUrl = void 0;
|
|
3857
|
+
relayUrl = void 0;
|
|
3858
|
+
setPublicBase();
|
|
3515
3859
|
service.setTunnelStatus({ state: "starting" });
|
|
3516
3860
|
} else if (info.phase === "failed") {
|
|
3517
|
-
|
|
3861
|
+
rawTunnelUrl = void 0;
|
|
3862
|
+
setPublicBase();
|
|
3518
3863
|
service.setTunnelStatus(info.error === void 0 ? { state: "failed" } : {
|
|
3519
3864
|
state: "failed",
|
|
3520
3865
|
error: info.error
|
|
@@ -3522,6 +3867,7 @@ function applyImpl(ctx, config) {
|
|
|
3522
3867
|
}
|
|
3523
3868
|
});
|
|
3524
3869
|
ctx.effect(() => () => {
|
|
3870
|
+
disposeRelayRegistrar();
|
|
3525
3871
|
tunnel.dispose();
|
|
3526
3872
|
}, "remote-web-ui: auto tunnel");
|
|
3527
3873
|
if ((ctx.webServer.host === "0.0.0.0" || ctx.webServer.host === "127.0.0.1") && Number.isFinite(ctx.webServer.port)) {
|
|
@@ -3762,12 +4108,35 @@ function applyImpl(ctx, config) {
|
|
|
3762
4108
|
else console.error("remote-web-ui: the host firewall rule could not be updated (admin rights required on managed platforms)");
|
|
3763
4109
|
}
|
|
3764
4110
|
}
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4111
|
+
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
4112
|
+
tunnelMode = plan.mode;
|
|
4113
|
+
if (plan.mode !== "quick") {
|
|
4114
|
+
disposeRelayRegistrar();
|
|
4115
|
+
if (plan.mode !== "named") setPublicBase();
|
|
4116
|
+
} else if (value.relay === false) {
|
|
4117
|
+
disposeRelayRegistrar(true);
|
|
4118
|
+
setPublicBase();
|
|
4119
|
+
} else if (rawTunnelUrl !== void 0) {
|
|
4120
|
+
const registrar = ensureRelayRegistrar();
|
|
4121
|
+
if (registrar !== void 0) announceRelay(registrar, rawTunnelUrl);
|
|
4122
|
+
}
|
|
4123
|
+
if (plan.mode === "quick") {
|
|
4124
|
+
for (const ignored of plan.ignored) console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
|
|
4125
|
+
const registrar = ensureRelayRegistrar();
|
|
4126
|
+
const originHostHeader = registrar === void 0 ? void 0 : new URL(registrar.baseUrl).host;
|
|
4127
|
+
tunnel.start(originHostHeader === void 0 ? plan.targetUrl : {
|
|
4128
|
+
kind: "quick",
|
|
4129
|
+
targetUrl: plan.targetUrl,
|
|
4130
|
+
originHostHeader
|
|
4131
|
+
});
|
|
4132
|
+
} else if (plan.mode === "named") tunnel.start({
|
|
4133
|
+
kind: "named",
|
|
4134
|
+
token: plan.token,
|
|
4135
|
+
publicUrl: plan.publicUrl
|
|
4136
|
+
});
|
|
4137
|
+
else {
|
|
3770
4138
|
tunnel.stop();
|
|
4139
|
+
if (value.tunnelToken !== void 0 && value.tunnelToken !== "") console.warn("remote-web-ui: tunnelToken is set but publicBaseUrl is missing or not a valid URL — fill the fixed public hostname of the named tunnel (e.g. https://dsh.example.com) to run it");
|
|
3771
4140
|
if (value.publicBaseUrl !== void 0 && !isHttpUrl(value.publicBaseUrl)) {
|
|
3772
4141
|
console.warn(`remote-web-ui: ignoring malformed publicBaseUrl ${JSON.stringify(value.publicBaseUrl)} (expected https://host[:port])`);
|
|
3773
4142
|
service.setPublicBaseUrl(void 0);
|
|
@@ -3827,14 +4196,5 @@ function applyImpl(ctx, config) {
|
|
|
3827
4196
|
});
|
|
3828
4197
|
sync();
|
|
3829
4198
|
}
|
|
3830
|
-
/** Whether a configured public base is a parseable http(s) URL with a host. */
|
|
3831
|
-
function isHttpUrl(value) {
|
|
3832
|
-
try {
|
|
3833
|
-
const url = new URL(value);
|
|
3834
|
-
return (url.protocol === "http:" || url.protocol === "https:") && url.hostname !== "";
|
|
3835
|
-
} catch {
|
|
3836
|
-
return false;
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
4199
|
//#endregion
|
|
3840
4200
|
export { Config, REMOTE_WEB_UI_SETTINGS_NAMESPACE, apply, defaultDevicesFile, inject, name, pairingConfigOf };
|