dsh-mobile 0.6.14 → 0.6.16
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 +32 -0
- package/CONTRIBUTORS.md +22 -2
- package/README.en.md +8 -6
- package/README.md +8 -6
- package/SECURITY.md +5 -1
- package/THIRD_PARTY_NOTICES.md +30 -2
- package/apps/mobile/README.md +35 -5
- package/apps/mobile/README.zh-CN.md +35 -5
- package/apps/mobile/release.json +3 -3
- package/docs/ATTACH_EXISTING_FRPS.en.md +8 -0
- package/docs/ATTACH_EXISTING_FRPS.md +8 -0
- package/docs/SLOW_CONNECTIONS.en.md +11 -1
- package/docs/SLOW_CONNECTIONS.md +11 -1
- package/lib/client.js +846 -60
- package/lib/client.js.map +1 -1
- package/lib/extension-worker-runtime.mjs.map +1 -1
- package/lib/index.d.mts +28 -2
- package/lib/index.mjs +499 -50
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +1 -1
- package/lib/mobile-compat.js.map +1 -1
- package/package.json +9 -2
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { promisify } from "node:util";
|
|
|
17
17
|
import { createGzip, gzip } from "node:zlib";
|
|
18
18
|
import Bonjour from "bonjour-service";
|
|
19
19
|
import * as QRCode from "qrcode";
|
|
20
|
+
import { createParser } from "eventsource-parser";
|
|
20
21
|
import WebSocket, { WebSocketServer } from "ws";
|
|
21
22
|
import { Logger, Service } from "@deepseek-ai/cordis";
|
|
22
23
|
import { Worker } from "node:worker_threads";
|
|
@@ -1810,6 +1811,7 @@ function setSecurityHeaders(response, tls, framing = "gateway") {
|
|
|
1810
1811
|
"font-src 'self' data:",
|
|
1811
1812
|
"connect-src 'self'",
|
|
1812
1813
|
"worker-src 'self' blob:",
|
|
1814
|
+
...proxied ? ["media-src 'self' blob:"] : [],
|
|
1813
1815
|
...proxied ? [PROXIED_FRAME_SRC] : []
|
|
1814
1816
|
].join("; "));
|
|
1815
1817
|
const microphone = proxied ? "microphone=(self)" : "microphone=()";
|
|
@@ -1899,6 +1901,7 @@ function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDeskto
|
|
|
1899
1901
|
if (request.socket.remoteAddress === void 0 || !isLoopbackAddress(request.socket.remoteAddress)) throw new HttpError(403, "forbidden");
|
|
1900
1902
|
const host = localAuthority(request.headers.host);
|
|
1901
1903
|
if (host === void 0 || !isLocalAdminHostname(host.hostname)) throw new HttpError(403, "forbidden");
|
|
1904
|
+
if (request.method === "OPTIONS") throw new HttpError(403, "forbidden");
|
|
1902
1905
|
const site = request.headers["sec-fetch-site"];
|
|
1903
1906
|
if (site !== void 0 && site !== "same-origin" && site !== "none") throw new HttpError(403, "forbidden");
|
|
1904
1907
|
const origin = request.headers.origin;
|
|
@@ -1910,7 +1913,9 @@ function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDeskto
|
|
|
1910
1913
|
throw new HttpError(403, "forbidden");
|
|
1911
1914
|
}
|
|
1912
1915
|
const desktopForward = site === void 0 && request.headers["x-dsh-mobile-desktop-admin"] === "dsh-app://app";
|
|
1913
|
-
|
|
1916
|
+
const contentType = request.headers["content-type"];
|
|
1917
|
+
const webRequest = request.method === "POST" && (site === void 0 || site === "same-origin") && request.headers["x-dsh-mobile-web-admin"] === "same-origin-json" && request.headers["x-dsh-mobile-desktop-admin"] === void 0 && typeof contentType === "string" && contentType.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
1918
|
+
if (requireBrowserOrigin && origin === void 0 && !webRequest && !(desktopForward && authenticateDesktop())) throw new HttpError(403, "forbidden");
|
|
1914
1919
|
if (requireBrowserOrigin && site !== void 0 && site !== "same-origin") throw new HttpError(403, "forbidden");
|
|
1915
1920
|
}
|
|
1916
1921
|
//#endregion
|
|
@@ -1968,7 +1973,7 @@ function authenticatedCookie(values, authenticate, accepts = () => true) {
|
|
|
1968
1973
|
}
|
|
1969
1974
|
//#endregion
|
|
1970
1975
|
//#region src/auth-failure-log.ts
|
|
1971
|
-
const LOG_INTERVAL_MS$
|
|
1976
|
+
const LOG_INTERVAL_MS$2 = 3e4;
|
|
1972
1977
|
const MAX_LOGGED_ORIGIN_LENGTH = 512;
|
|
1973
1978
|
function requestPathCategory(path) {
|
|
1974
1979
|
switch (path) {
|
|
@@ -2065,7 +2070,7 @@ var AuthFailureRecorder = class {
|
|
|
2065
2070
|
return;
|
|
2066
2071
|
}
|
|
2067
2072
|
const suppressed = window.suppressed;
|
|
2068
|
-
window.nextLogAt = now + LOG_INTERVAL_MS$
|
|
2073
|
+
window.nextLogAt = now + LOG_INTERVAL_MS$2;
|
|
2069
2074
|
window.suppressed = 0;
|
|
2070
2075
|
try {
|
|
2071
2076
|
this.observe({
|
|
@@ -2119,7 +2124,7 @@ function classifyHttpParserFailure(nodeCode) {
|
|
|
2119
2124
|
parserCode
|
|
2120
2125
|
};
|
|
2121
2126
|
}
|
|
2122
|
-
const LOG_INTERVAL_MS = 3e4;
|
|
2127
|
+
const LOG_INTERVAL_MS$1 = 3e4;
|
|
2123
2128
|
/**
|
|
2124
2129
|
* Per-Gateway diagnostics with three fixed sampling slots and no timers or request-selected keys.
|
|
2125
2130
|
* Each kind records once immediately, then at most once per 30 seconds with suppressed counts.
|
|
@@ -2188,7 +2193,7 @@ var HttpFailureRecorder = class {
|
|
|
2188
2193
|
return;
|
|
2189
2194
|
}
|
|
2190
2195
|
const suppressed = window.suppressed;
|
|
2191
|
-
window.nextLogAt = now + LOG_INTERVAL_MS;
|
|
2196
|
+
window.nextLogAt = now + LOG_INTERVAL_MS$1;
|
|
2192
2197
|
window.suppressed = 0;
|
|
2193
2198
|
try {
|
|
2194
2199
|
this.observe({
|
|
@@ -2205,6 +2210,72 @@ var HttpFailureRecorder = class {
|
|
|
2205
2210
|
}
|
|
2206
2211
|
};
|
|
2207
2212
|
//#endregion
|
|
2213
|
+
//#region src/upstream-auth-log.ts
|
|
2214
|
+
const LOG_INTERVAL_MS = 3e4;
|
|
2215
|
+
/** Two fixed sampling slots keep a first failure visible even after a successful exchange. */
|
|
2216
|
+
var UpstreamAuthRecorder = class {
|
|
2217
|
+
observe;
|
|
2218
|
+
now;
|
|
2219
|
+
reportObserverFailure;
|
|
2220
|
+
sequence = 0;
|
|
2221
|
+
windows = {
|
|
2222
|
+
success: {
|
|
2223
|
+
nextLogAt: 0,
|
|
2224
|
+
suppressed: 0
|
|
2225
|
+
},
|
|
2226
|
+
failure: {
|
|
2227
|
+
nextLogAt: 0,
|
|
2228
|
+
suppressed: 0
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
observerFailureReported = false;
|
|
2232
|
+
constructor(observe, now = () => performance.now(), reportObserverFailure = () => {
|
|
2233
|
+
process.emitWarning("Mobile access upstream authentication diagnostics could not be recorded", { code: "DSH_MOBILE_UPSTREAM_AUTH_LOG_FAILED" });
|
|
2234
|
+
}) {
|
|
2235
|
+
this.observe = observe;
|
|
2236
|
+
this.now = now;
|
|
2237
|
+
this.reportObserverFailure = reportObserverFailure;
|
|
2238
|
+
}
|
|
2239
|
+
/** Begin only a physical exchange, not every consumer sharing its promise or cached Cookie. */
|
|
2240
|
+
begin() {
|
|
2241
|
+
if (this.observe === void 0) return void 0;
|
|
2242
|
+
this.sequence = this.sequence >= Number.MAX_SAFE_INTEGER ? 1 : this.sequence + 1;
|
|
2243
|
+
return {
|
|
2244
|
+
sequence: this.sequence,
|
|
2245
|
+
startedAt: this.now()
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
/** Record the completed exchange without changing authentication, retrying it or retaining its inputs. */
|
|
2249
|
+
record(attempt, result) {
|
|
2250
|
+
if (attempt === void 0 || this.observe === void 0) return;
|
|
2251
|
+
const now = this.now();
|
|
2252
|
+
const window = this.windows[result.outcome === "accepted" ? "success" : "failure"];
|
|
2253
|
+
if (now < window.nextLogAt) {
|
|
2254
|
+
window.suppressed = Math.min(Number.MAX_SAFE_INTEGER, window.suppressed + 1);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const suppressed = window.suppressed;
|
|
2258
|
+
window.nextLogAt = now + LOG_INTERVAL_MS;
|
|
2259
|
+
window.suppressed = 0;
|
|
2260
|
+
try {
|
|
2261
|
+
this.observe({
|
|
2262
|
+
sequence: attempt.sequence,
|
|
2263
|
+
outcome: result.outcome,
|
|
2264
|
+
stage: result.stage,
|
|
2265
|
+
durationMs: Math.max(0, Math.floor(now - attempt.startedAt)),
|
|
2266
|
+
...result.status === void 0 ? {} : { status: result.status },
|
|
2267
|
+
...suppressed === 0 ? {} : { suppressed }
|
|
2268
|
+
});
|
|
2269
|
+
} catch (observerError) {
|
|
2270
|
+
if (this.observerFailureReported) return;
|
|
2271
|
+
this.observerFailureReported = true;
|
|
2272
|
+
try {
|
|
2273
|
+
this.reportObserverFailure();
|
|
2274
|
+
} catch (reporterError) {}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2278
|
+
//#endregion
|
|
2208
2279
|
//#region src/client-module-recovery.ts
|
|
2209
2280
|
const CLIENT_MODULE_RECOVERY_COPY = {
|
|
2210
2281
|
zh: {
|
|
@@ -2384,6 +2455,196 @@ function weakenEntityTag(etag) {
|
|
|
2384
2455
|
return opaque === void 0 ? void 0 : `W/${opaque}`;
|
|
2385
2456
|
}
|
|
2386
2457
|
//#endregion
|
|
2458
|
+
//#region src/mobile-graph-events.ts
|
|
2459
|
+
/** Bounded SSE framing for the dedicated Mobile client graph stream. */
|
|
2460
|
+
/** Maximum retained UTF-8 frame/output bytes, with CRLF normalized to LF. */
|
|
2461
|
+
const MOBILE_GRAPH_EVENT_MAX_BYTES = 4194304;
|
|
2462
|
+
const LINE_FEED = Buffer.from("\n");
|
|
2463
|
+
/** Fixed failures contain neither an upstream payload nor a projection exception. */
|
|
2464
|
+
var MobileGraphEventError = class extends Error {
|
|
2465
|
+
code;
|
|
2466
|
+
constructor(code) {
|
|
2467
|
+
super(`Mobile graph event stream: ${code}`);
|
|
2468
|
+
this.code = code;
|
|
2469
|
+
this.name = "MobileGraphEventError";
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
var MobileGraphEventTransform = class extends Transform {
|
|
2473
|
+
project;
|
|
2474
|
+
frame = Buffer.allocUnsafe(16384);
|
|
2475
|
+
used = 0;
|
|
2476
|
+
lineBytes = 0;
|
|
2477
|
+
skipLf = false;
|
|
2478
|
+
firstFrame = true;
|
|
2479
|
+
disposed = false;
|
|
2480
|
+
readWait;
|
|
2481
|
+
constructor(project) {
|
|
2482
|
+
super();
|
|
2483
|
+
this.project = project;
|
|
2484
|
+
}
|
|
2485
|
+
_transform(chunk, _encoding, callback) {
|
|
2486
|
+
this.consume(chunk).then(() => callback(), (error) => callback(error));
|
|
2487
|
+
}
|
|
2488
|
+
_read(size) {
|
|
2489
|
+
super._read(size);
|
|
2490
|
+
const waiting = this.readWait;
|
|
2491
|
+
this.readWait = void 0;
|
|
2492
|
+
waiting?.resolve();
|
|
2493
|
+
}
|
|
2494
|
+
_flush(callback) {
|
|
2495
|
+
if (this.used !== 0) {
|
|
2496
|
+
try {
|
|
2497
|
+
this.decodeFrame();
|
|
2498
|
+
} catch {
|
|
2499
|
+
callback(new MobileGraphEventError("invalid_utf8"));
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
callback(new MobileGraphEventError("incomplete_frame"));
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
callback();
|
|
2506
|
+
}
|
|
2507
|
+
_destroy(error, callback) {
|
|
2508
|
+
this.disposed = true;
|
|
2509
|
+
const waiting = this.readWait;
|
|
2510
|
+
this.readWait = void 0;
|
|
2511
|
+
waiting?.reject(error ?? new MobileGraphEventError("stream_closed"));
|
|
2512
|
+
this.frame = Buffer.alloc(0);
|
|
2513
|
+
this.used = 0;
|
|
2514
|
+
callback(error);
|
|
2515
|
+
}
|
|
2516
|
+
requireOpen() {
|
|
2517
|
+
if (this.disposed || this.destroyed) throw new MobileGraphEventError("stream_closed");
|
|
2518
|
+
}
|
|
2519
|
+
append(bytes) {
|
|
2520
|
+
const required = this.used + bytes.length;
|
|
2521
|
+
if (required > 4194304) throw new MobileGraphEventError("frame_too_large");
|
|
2522
|
+
if (required > this.frame.length) {
|
|
2523
|
+
const next = Buffer.allocUnsafe(Math.min(MOBILE_GRAPH_EVENT_MAX_BYTES, Math.max(required, this.frame.length * 2)));
|
|
2524
|
+
this.frame.copy(next, 0, 0, this.used);
|
|
2525
|
+
this.frame = next;
|
|
2526
|
+
}
|
|
2527
|
+
bytes.copy(this.frame, this.used);
|
|
2528
|
+
this.used = required;
|
|
2529
|
+
}
|
|
2530
|
+
async consume(chunk) {
|
|
2531
|
+
let offset = 0;
|
|
2532
|
+
while (offset < chunk.length) {
|
|
2533
|
+
this.requireOpen();
|
|
2534
|
+
if (this.skipLf) {
|
|
2535
|
+
this.skipLf = false;
|
|
2536
|
+
if (chunk[offset] === 10) {
|
|
2537
|
+
offset += 1;
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
let contentEnd = offset;
|
|
2542
|
+
while (contentEnd < chunk.length && chunk[contentEnd] !== 13 && chunk[contentEnd] !== 10) contentEnd += 1;
|
|
2543
|
+
const content = chunk.subarray(offset, contentEnd);
|
|
2544
|
+
this.append(content);
|
|
2545
|
+
this.lineBytes += content.length;
|
|
2546
|
+
if (contentEnd === chunk.length) return;
|
|
2547
|
+
this.append(LINE_FEED);
|
|
2548
|
+
this.skipLf = chunk[contentEnd] === 13;
|
|
2549
|
+
const emptyLine = this.lineBytes === 0;
|
|
2550
|
+
this.lineBytes = 0;
|
|
2551
|
+
offset = contentEnd + 1;
|
|
2552
|
+
if (emptyLine) await this.completeFrame();
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
decodeFrame() {
|
|
2556
|
+
try {
|
|
2557
|
+
return new TextDecoder("utf-8", {
|
|
2558
|
+
fatal: true,
|
|
2559
|
+
ignoreBOM: true
|
|
2560
|
+
}).decode(this.frame.subarray(0, this.used));
|
|
2561
|
+
} catch {
|
|
2562
|
+
throw new MobileGraphEventError("invalid_utf8");
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
async completeFrame() {
|
|
2566
|
+
let text = this.decodeFrame();
|
|
2567
|
+
this.used = 0;
|
|
2568
|
+
if (this.firstFrame) {
|
|
2569
|
+
this.firstFrame = false;
|
|
2570
|
+
if (text.startsWith("")) text = text.slice(1);
|
|
2571
|
+
}
|
|
2572
|
+
let event;
|
|
2573
|
+
createParser({
|
|
2574
|
+
maxBufferSize: MOBILE_GRAPH_EVENT_MAX_BYTES,
|
|
2575
|
+
onEvent: (value) => {
|
|
2576
|
+
event = value;
|
|
2577
|
+
},
|
|
2578
|
+
onError: () => {
|
|
2579
|
+
throw new MobileGraphEventError("invalid_sse");
|
|
2580
|
+
}
|
|
2581
|
+
}).feed(text);
|
|
2582
|
+
const lines = text.split("\n");
|
|
2583
|
+
for (const line of lines) {
|
|
2584
|
+
if (!line.startsWith("event:")) continue;
|
|
2585
|
+
const type = line.slice(line[6] === " " ? 7 : 6);
|
|
2586
|
+
if (type !== "" && type !== "message") throw new MobileGraphEventError("unsupported_event");
|
|
2587
|
+
}
|
|
2588
|
+
if (event === void 0) {
|
|
2589
|
+
await this.forward(Buffer.from(text, "utf8"));
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
if (event.event !== void 0 && event.event !== "message") throw new MobileGraphEventError("unsupported_event");
|
|
2593
|
+
let value;
|
|
2594
|
+
try {
|
|
2595
|
+
value = JSON.parse(event.data);
|
|
2596
|
+
} catch {
|
|
2597
|
+
throw new MobileGraphEventError("invalid_json");
|
|
2598
|
+
}
|
|
2599
|
+
let projected;
|
|
2600
|
+
try {
|
|
2601
|
+
projected = await this.project(value);
|
|
2602
|
+
} catch {
|
|
2603
|
+
throw new MobileGraphEventError("projection_failed");
|
|
2604
|
+
}
|
|
2605
|
+
this.requireOpen();
|
|
2606
|
+
if (projected === void 0) return;
|
|
2607
|
+
let encoded;
|
|
2608
|
+
try {
|
|
2609
|
+
encoded = JSON.stringify(projected);
|
|
2610
|
+
} catch {
|
|
2611
|
+
throw new MobileGraphEventError("invalid_projection");
|
|
2612
|
+
}
|
|
2613
|
+
if (encoded === void 0) throw new MobileGraphEventError("invalid_projection");
|
|
2614
|
+
const metadata = lines.filter((line) => line !== "" && line !== "data" && !line.startsWith("data:"));
|
|
2615
|
+
const result = `${metadata.length === 0 ? "" : `${metadata.join("\n")}\n`}data: ${encoded}\n\n`;
|
|
2616
|
+
if (Buffer.byteLength(result) > 4194304) throw new MobileGraphEventError("frame_too_large");
|
|
2617
|
+
await this.forward(Buffer.from(result, "utf8"));
|
|
2618
|
+
}
|
|
2619
|
+
async forward(frame) {
|
|
2620
|
+
this.requireOpen();
|
|
2621
|
+
if (frame.length === 0 || this.push(frame)) return;
|
|
2622
|
+
await new Promise((resolve, reject) => {
|
|
2623
|
+
this.readWait = {
|
|
2624
|
+
resolve,
|
|
2625
|
+
reject
|
|
2626
|
+
};
|
|
2627
|
+
if (this.destroyed || this.disposed) {
|
|
2628
|
+
this.readWait = void 0;
|
|
2629
|
+
reject(new MobileGraphEventError("stream_closed"));
|
|
2630
|
+
} else if (this.readableLength < this.readableHighWaterMark) {
|
|
2631
|
+
this.readWait = void 0;
|
|
2632
|
+
resolve();
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
/**
|
|
2638
|
+
* Project each complete JSON message serially without forwarding unprojected data.
|
|
2639
|
+
* Comments and valid SSE metadata are retained; only the default message event is supported.
|
|
2640
|
+
* Undefined suppresses that message. Malformed, oversized or truncated input terminates the stream.
|
|
2641
|
+
* @param project - Projects one message; caller-owned cancellation must stop any external work.
|
|
2642
|
+
* @returns A byte-mode Transform that honors downstream backpressure and fences late results after destroy.
|
|
2643
|
+
*/
|
|
2644
|
+
function createMobileGraphEventTransform(project) {
|
|
2645
|
+
return new MobileGraphEventTransform(project);
|
|
2646
|
+
}
|
|
2647
|
+
//#endregion
|
|
2387
2648
|
//#region src/websocket-compression.ts
|
|
2388
2649
|
function payloadBytes(data) {
|
|
2389
2650
|
if (Array.isArray(data)) return data.reduce((bytes, part) => bytes + part.byteLength, 0);
|
|
@@ -4650,6 +4911,26 @@ function renderLoginScript(locale) {
|
|
|
4650
4911
|
`;
|
|
4651
4912
|
}
|
|
4652
4913
|
//#endregion
|
|
4914
|
+
//#region src/browser-ui-locale.ts
|
|
4915
|
+
const UI_LOCALE_COOKIE = "dsh_ma_ui_locale";
|
|
4916
|
+
/** Read presentation data only; malformed or ambiguous cookies keep browser negotiation. */
|
|
4917
|
+
function readBrowserUiLocale(cookieHeader) {
|
|
4918
|
+
if (cookieHeader === void 0 || cookieHeader.length > 8192) return void 0;
|
|
4919
|
+
let locale;
|
|
4920
|
+
for (const part of cookieHeader.split(";")) {
|
|
4921
|
+
const separator = part.indexOf("=");
|
|
4922
|
+
if ((separator === -1 ? part : part.slice(0, separator)).trim() !== UI_LOCALE_COOKIE) continue;
|
|
4923
|
+
const value = separator === -1 ? "" : part.slice(separator + 1).trim();
|
|
4924
|
+
if (locale !== void 0 || value !== "en" && value !== "zh" && value !== "it") return void 0;
|
|
4925
|
+
locale = value;
|
|
4926
|
+
}
|
|
4927
|
+
return locale;
|
|
4928
|
+
}
|
|
4929
|
+
/** Prefer the last DSH display language over the browser's default language. */
|
|
4930
|
+
function resolveBrowserUiLocale(cookieHeader, acceptLanguage) {
|
|
4931
|
+
return readBrowserUiLocale(cookieHeader) ?? resolveAuthPageLocale(acceptLanguage);
|
|
4932
|
+
}
|
|
4933
|
+
//#endregion
|
|
4653
4934
|
//#region src/gateway.ts
|
|
4654
4935
|
const MAX_CONTROL_BODY_BYTES = 16384;
|
|
4655
4936
|
const MAX_HEADER_BYTES = 16384;
|
|
@@ -4660,6 +4941,7 @@ const MDNS_SERVICE_TYPE = "dsh-mobile";
|
|
|
4660
4941
|
const MOBILE_LAYOUT_MODULE = "@deepseek-ai/dsh-client-ui-layout";
|
|
4661
4942
|
const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
4662
4943
|
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
4944
|
+
const MOBILE_DOCUMENT_VIEW_HEADER = "x-dsh-mobile-view";
|
|
4663
4945
|
const MAX_MOBILE_BOOT_BATCH_BYTES = 33554432;
|
|
4664
4946
|
const MAX_MOBILE_BOOT_ENTRY_BYTES = 8388608;
|
|
4665
4947
|
/**
|
|
@@ -4741,7 +5023,9 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
|
4741
5023
|
function mobileCsrfFetchBootstrap(cookieName) {
|
|
4742
5024
|
return `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${cookieName}=`)};const values=[...new Set(document.cookie.split(';').map(value=>value.trim()).filter(value=>value.startsWith(prefix)).map(value=>value.slice(prefix.length)))];if(values.length===1)headers.set(${JSON.stringify(CSRF_HEADER)},values[0])}return nativeFetch(input,{...init,headers})};})();`;
|
|
4743
5025
|
}
|
|
4744
|
-
|
|
5026
|
+
function mobileAuthenticatedTransportBootstrap(viewId) {
|
|
5027
|
+
return `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:${viewId === void 0 ? "(input,init)=>window.fetch(input,init)" : `(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw!==undefined&&method==='GET'){const target=new URL(raw,location.href);if(target.origin===location.origin&&target.pathname==='/plugins/events'){const headers=new Headers(init?.headers??source?.headers);headers.set(${JSON.stringify(MOBILE_DOCUMENT_VIEW_HEADER)},${JSON.stringify(viewId)});return window.fetch(input,{...init,headers})}}return window.fetch(input,init)}`},ownsHost:true},window.__DSH_FILE_UPLOAD__={fetch:(input,init)=>window.fetch(input,init)}})();`;
|
|
5028
|
+
}
|
|
4745
5029
|
/** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
|
|
4746
5030
|
const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
|
|
4747
5031
|
const view=globalThis;
|
|
@@ -4911,6 +5195,14 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4911
5195
|
const scriptEnd = html.indexOf("<\/script>", valueStart);
|
|
4912
5196
|
if (scriptEnd < 0) throw new Error("upstream DSH boot manifest script is incomplete");
|
|
4913
5197
|
const source = html.slice(valueStart, scriptEnd).trim().replace(/;$/u, "");
|
|
5198
|
+
return Object.freeze({
|
|
5199
|
+
...parseMobileBootGraph(source, excludedClientModules),
|
|
5200
|
+
assignment: assignment[0],
|
|
5201
|
+
replaceStart,
|
|
5202
|
+
replaceEnd: scriptEnd
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
function parseMobileBootGraph(source, excludedClientModules) {
|
|
4914
5206
|
const parsed = JSON.parse(source);
|
|
4915
5207
|
if (typeof parsed.rev !== "string" || !Array.isArray(parsed.entries)) throw new Error("upstream DSH boot manifest is malformed");
|
|
4916
5208
|
const entries = parsed.entries;
|
|
@@ -4927,10 +5219,7 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4927
5219
|
return Object.freeze({
|
|
4928
5220
|
parsed,
|
|
4929
5221
|
entries,
|
|
4930
|
-
slotProvider: dependencyProfile.slots
|
|
4931
|
-
assignment: assignment[0],
|
|
4932
|
-
replaceStart,
|
|
4933
|
-
replaceEnd: scriptEnd
|
|
5222
|
+
slotProvider: dependencyProfile.slots
|
|
4934
5223
|
});
|
|
4935
5224
|
}
|
|
4936
5225
|
if (!Array.isArray(parsedBatches)) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
@@ -4969,9 +5258,6 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4969
5258
|
entries,
|
|
4970
5259
|
batches,
|
|
4971
5260
|
slotProvider: dependencyProfile.slots,
|
|
4972
|
-
assignment: assignment[0],
|
|
4973
|
-
replaceStart,
|
|
4974
|
-
replaceEnd: scriptEnd,
|
|
4975
5261
|
layoutBatch,
|
|
4976
5262
|
planEntries,
|
|
4977
5263
|
originalApplicationBatches,
|
|
@@ -5055,6 +5341,17 @@ function rewriteMobileApplicationPreloads(html, replacements) {
|
|
|
5055
5341
|
}
|
|
5056
5342
|
function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
5057
5343
|
const plan = parseMobileBootPlan(html, options.excludedClientModules);
|
|
5344
|
+
const projected = projectMobileBootGraph(plan, options);
|
|
5345
|
+
const transportBootstrap = projected.graphTransport ? mobileAuthenticatedTransportBootstrap(options.viewId) : "";
|
|
5346
|
+
const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${transportBootstrap}${mobileCsrfFetchBootstrap(options.csrfCookie ?? "dsh_ma_csrf")}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
|
|
5347
|
+
const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
|
|
5348
|
+
return Object.freeze({
|
|
5349
|
+
html: ensureMobileGatewayMarker(ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, projected.preloadReplacements)))),
|
|
5350
|
+
batches: projected.batches,
|
|
5351
|
+
graphTransport: projected.graphTransport
|
|
5352
|
+
});
|
|
5353
|
+
}
|
|
5354
|
+
function projectMobileBootGraph(plan, options) {
|
|
5058
5355
|
const remoteSettings = orderAuthenticatedSettings(plan.entries, plan.slotProvider);
|
|
5059
5356
|
let batches = [];
|
|
5060
5357
|
let preloadReplacements = /* @__PURE__ */ new Map();
|
|
@@ -5080,11 +5377,10 @@ function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
|
5080
5377
|
batches: plan.batches
|
|
5081
5378
|
})).digest("hex").slice(0, 16);
|
|
5082
5379
|
}
|
|
5083
|
-
const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${mobileCsrfFetchBootstrap(options.csrfCookie ?? "dsh_ma_csrf")}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
|
|
5084
|
-
const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
|
|
5085
5380
|
return Object.freeze({
|
|
5086
|
-
|
|
5087
|
-
|
|
5381
|
+
batches,
|
|
5382
|
+
preloadReplacements,
|
|
5383
|
+
graphTransport: remoteSettings
|
|
5088
5384
|
});
|
|
5089
5385
|
}
|
|
5090
5386
|
/** Rewrite the dedicated mobile graph; exclusions are opt-in and leave stock DSH unchanged. */
|
|
@@ -5515,6 +5811,8 @@ var MobileAccessGateway = class {
|
|
|
5515
5811
|
activeRequests = /* @__PURE__ */ new Map();
|
|
5516
5812
|
activeWebSockets = /* @__PURE__ */ new Map();
|
|
5517
5813
|
mobileBootBatches = /* @__PURE__ */ new Map();
|
|
5814
|
+
mobileDocumentViews = /* @__PURE__ */ new Map();
|
|
5815
|
+
mobileGraphTasks = /* @__PURE__ */ new Set();
|
|
5518
5816
|
/** Cached upstream bundle byte sizes (HEAD probes), keyed by resource URL. */
|
|
5519
5817
|
bootEntrySizeCache = /* @__PURE__ */ new Map();
|
|
5520
5818
|
extensionEventListeners = /* @__PURE__ */ new Set();
|
|
@@ -5547,7 +5845,8 @@ var MobileAccessGateway = class {
|
|
|
5547
5845
|
clientModuleRevocations = /* @__PURE__ */ new Set();
|
|
5548
5846
|
authFailures;
|
|
5549
5847
|
httpFailures;
|
|
5550
|
-
|
|
5848
|
+
upstreamAuthDiagnostics;
|
|
5849
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded, clientModulePreferences, authFailureLog, httpFailureLog, upstreamAuthLog) {
|
|
5551
5850
|
this.config = config;
|
|
5552
5851
|
this.extensions = extensions;
|
|
5553
5852
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
@@ -5556,6 +5855,7 @@ var MobileAccessGateway = class {
|
|
|
5556
5855
|
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
5557
5856
|
this.authFailures = new AuthFailureRecorder(authFailureLog);
|
|
5558
5857
|
this.httpFailures = new HttpFailureRecorder(httpFailureLog);
|
|
5858
|
+
this.upstreamAuthDiagnostics = new UpstreamAuthRecorder(upstreamAuthLog);
|
|
5559
5859
|
this.clientModulePreferences = clientModulePreferences ?? new ClientModulePreferenceStore(join(dirname(config.stateFile), `${basename(config.stateFile)}.client-modules.json`), config.excludedClientModules);
|
|
5560
5860
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
5561
5861
|
this.tlsEnabled = config.publicTls;
|
|
@@ -5578,6 +5878,7 @@ var MobileAccessGateway = class {
|
|
|
5578
5878
|
this.probeFailureLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5579
5879
|
this.probeDeviceLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5580
5880
|
this.removeSessionListener = this.access.onSessionEnded((authorization, reason) => {
|
|
5881
|
+
for (const view of this.mobileDocumentViews.values()) if (view.sessionKey === authorization.sessionKey) this.removeMobileDocumentView(view);
|
|
5581
5882
|
if (reason === "revoked") {
|
|
5582
5883
|
const cleanup = this.clientModulePreferences.removeDevice(authorization.deviceId).catch(() => {
|
|
5583
5884
|
process.emitWarning("Unable to remove revoked device client preferences", { code: "DSH_MOBILE_PREFERENCE_CLEANUP_FAILED" });
|
|
@@ -5976,12 +6277,17 @@ var MobileAccessGateway = class {
|
|
|
5976
6277
|
nativeResponseInstanceId(result) {
|
|
5977
6278
|
return this.config.pairingCaFile === void 0 ? result.nativeInstanceId ?? this.config.instanceId : this.config.instanceId;
|
|
5978
6279
|
}
|
|
6280
|
+
/** Older Apps require exact wire keys; expose this hint only to explicit opt-in clients after authentication. */
|
|
6281
|
+
nativeResponseInstallationIdentity(request) {
|
|
6282
|
+
return this.config.installationId === void 0 || request.headers["x-dsh-mobile-capabilities"] !== "installation-id" ? {} : { installationId: this.config.installationId };
|
|
6283
|
+
}
|
|
5979
6284
|
async handleNativePair(request, response) {
|
|
5980
6285
|
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5981
6286
|
if (typeof body.token !== "string" || body.label !== void 0 && typeof body.label !== "string") throw new HttpError(400, "bad_request");
|
|
5982
6287
|
const result = await this.access.pair(request.socket.remoteAddress ?? "unknown", body.token, body.label);
|
|
5983
6288
|
sendJson(response, 201, {
|
|
5984
6289
|
instanceId: this.nativeResponseInstanceId(result),
|
|
6290
|
+
...this.nativeResponseInstallationIdentity(request),
|
|
5985
6291
|
deviceId: result.deviceId,
|
|
5986
6292
|
deviceToken: result.deviceToken,
|
|
5987
6293
|
deviceExpiresAt: result.deviceExpiresAt,
|
|
@@ -6002,6 +6308,7 @@ var MobileAccessGateway = class {
|
|
|
6002
6308
|
const result = await this.access.renew(selected.token);
|
|
6003
6309
|
sendJson(response, 200, {
|
|
6004
6310
|
instanceId: this.nativeResponseInstanceId(result),
|
|
6311
|
+
...this.nativeResponseInstallationIdentity(request),
|
|
6005
6312
|
deviceId: result.deviceId,
|
|
6006
6313
|
sessionToken: result.sessionToken,
|
|
6007
6314
|
csrfToken: result.csrfToken,
|
|
@@ -6021,6 +6328,7 @@ var MobileAccessGateway = class {
|
|
|
6021
6328
|
const result = await this.access.probe(selected.token);
|
|
6022
6329
|
sendJson(response, 200, {
|
|
6023
6330
|
instanceId: this.nativeResponseInstanceId(result),
|
|
6331
|
+
...this.nativeResponseInstallationIdentity(request),
|
|
6024
6332
|
deviceId: result.deviceId,
|
|
6025
6333
|
deviceExpiresAt: result.deviceExpiresAt
|
|
6026
6334
|
}, this.tlsEnabled);
|
|
@@ -6109,9 +6417,9 @@ var MobileAccessGateway = class {
|
|
|
6109
6417
|
if (target.search === "" && request.method === "GET" && (target.decodedPathname === `/mobile-access/pair` || target.decodedPathname === `/mobile-access/pair.js`)) {
|
|
6110
6418
|
if (!this.access.pairingStatus().open) throw new HttpError(404, "not_found");
|
|
6111
6419
|
const languageHeader = request.headers["accept-language"];
|
|
6112
|
-
const locale =
|
|
6420
|
+
const locale = resolveBrowserUiLocale(request.headers.cookie, typeof languageHeader === "string" ? languageHeader : void 0);
|
|
6113
6421
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
6114
|
-
response.setHeader("Vary", "Accept-Language");
|
|
6422
|
+
response.setHeader("Vary", "Accept-Language, Cookie");
|
|
6115
6423
|
const body = target.decodedPathname.endsWith(".js") ? renderPairScript(locale) : renderPairPage(locale);
|
|
6116
6424
|
response.writeHead(200, {
|
|
6117
6425
|
"Content-Type": target.decodedPathname.endsWith(".js") ? "text/javascript; charset=utf-8" : "text/html; charset=utf-8",
|
|
@@ -6123,9 +6431,9 @@ var MobileAccessGateway = class {
|
|
|
6123
6431
|
if (request.method === "GET" && (target.decodedPathname === `/mobile-access/login` || target.decodedPathname === `/mobile-access/login.js`)) {
|
|
6124
6432
|
if (target.decodedPathname.endsWith(".js") && target.search !== "") throw new HttpError(400, "bad_request");
|
|
6125
6433
|
const languageHeader = request.headers["accept-language"];
|
|
6126
|
-
const locale =
|
|
6434
|
+
const locale = resolveBrowserUiLocale(request.headers.cookie, typeof languageHeader === "string" ? languageHeader : void 0);
|
|
6127
6435
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
6128
|
-
response.setHeader("Vary", "Accept-Language");
|
|
6436
|
+
response.setHeader("Vary", "Accept-Language, Cookie");
|
|
6129
6437
|
const body = target.decodedPathname.endsWith(".js") ? renderLoginScript(locale) : renderLoginPage(locale);
|
|
6130
6438
|
response.writeHead(200, {
|
|
6131
6439
|
"Content-Type": target.decodedPathname.endsWith(".js") ? "text/javascript; charset=utf-8" : "text/html; charset=utf-8",
|
|
@@ -6209,6 +6517,8 @@ var MobileAccessGateway = class {
|
|
|
6209
6517
|
}
|
|
6210
6518
|
throw error;
|
|
6211
6519
|
}
|
|
6520
|
+
const viewId = headerValue(request.headers, MOBILE_DOCUMENT_VIEW_HEADER);
|
|
6521
|
+
const documentView = viewId === void 0 ? void 0 : this.requireMobileDocumentView(viewId, authorization, request);
|
|
6212
6522
|
if (isMutation) try {
|
|
6213
6523
|
this.requireCsrf(request, authorization);
|
|
6214
6524
|
} catch (error) {
|
|
@@ -6336,7 +6646,7 @@ var MobileAccessGateway = class {
|
|
|
6336
6646
|
return;
|
|
6337
6647
|
}
|
|
6338
6648
|
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
6339
|
-
await this.proxyHttp(request, response, authorization);
|
|
6649
|
+
await this.proxyHttp(request, response, authorization, documentView);
|
|
6340
6650
|
}
|
|
6341
6651
|
async handleExtensionRequest(targetInfo, target, request, response, authorization) {
|
|
6342
6652
|
const extensions = this.extensions;
|
|
@@ -6568,16 +6878,23 @@ var MobileAccessGateway = class {
|
|
|
6568
6878
|
}
|
|
6569
6879
|
}
|
|
6570
6880
|
async exchangeUpstreamCookie() {
|
|
6571
|
-
const
|
|
6572
|
-
|
|
6573
|
-
let target;
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
} catch {
|
|
6577
|
-
throw new HttpError(502, "upstream_unavailable");
|
|
6578
|
-
}
|
|
6579
|
-
if (target.origin !== this.config.upstreamOrigin.origin || target.pathname !== "/" || target.hash !== "" || target.search === "") throw new HttpError(502, "upstream_unavailable");
|
|
6881
|
+
const attempt = this.upstreamAuthDiagnostics.begin();
|
|
6882
|
+
let stage = "target";
|
|
6883
|
+
let outcome = "invalid-target";
|
|
6884
|
+
let status;
|
|
6885
|
+
let timedOut = false;
|
|
6580
6886
|
try {
|
|
6887
|
+
const authenticatedUrl = this.upstreamAuthenticatedUrl;
|
|
6888
|
+
if (authenticatedUrl === void 0) throw new HttpError(502, "upstream_unavailable");
|
|
6889
|
+
let target;
|
|
6890
|
+
try {
|
|
6891
|
+
target = new URL(authenticatedUrl);
|
|
6892
|
+
} catch {
|
|
6893
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
6894
|
+
}
|
|
6895
|
+
if (target.origin !== this.config.upstreamOrigin.origin || target.pathname !== "/" || target.hash !== "" || target.search === "") throw new HttpError(502, "upstream_unavailable");
|
|
6896
|
+
stage = "request";
|
|
6897
|
+
outcome = "transport-error";
|
|
6581
6898
|
const proxied = await new Promise((resolve, reject) => {
|
|
6582
6899
|
const upstreamRequest = request({
|
|
6583
6900
|
protocol: "http:",
|
|
@@ -6595,27 +6912,46 @@ var MobileAccessGateway = class {
|
|
|
6595
6912
|
});
|
|
6596
6913
|
this.upstreamAuthRequest = upstreamRequest;
|
|
6597
6914
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
6915
|
+
timedOut = true;
|
|
6598
6916
|
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
6599
6917
|
});
|
|
6600
6918
|
upstreamRequest.once("response", resolve);
|
|
6601
6919
|
upstreamRequest.once("error", reject);
|
|
6602
6920
|
upstreamRequest.end();
|
|
6603
6921
|
});
|
|
6922
|
+
stage = "response";
|
|
6923
|
+
status = proxied.statusCode;
|
|
6604
6924
|
await new Promise((resolve, reject) => {
|
|
6605
6925
|
proxied.once("end", resolve);
|
|
6606
6926
|
proxied.once("error", reject);
|
|
6607
6927
|
proxied.resume();
|
|
6608
6928
|
});
|
|
6929
|
+
if (status !== 303) {
|
|
6930
|
+
outcome = "unexpected-status";
|
|
6931
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
6932
|
+
}
|
|
6933
|
+
stage = "cookie";
|
|
6934
|
+
outcome = "invalid-cookie";
|
|
6609
6935
|
const setCookie = proxied.headers["set-cookie"]?.[0];
|
|
6610
6936
|
const pair = setCookie?.split(";", 1)[0];
|
|
6611
6937
|
const maxAgeText = setCookie === void 0 ? void 0 : /(?:^|;\s*)Max-Age=(\d+)(?:;|$)/iu.exec(setCookie)?.[1];
|
|
6612
6938
|
const maxAgeSeconds = maxAgeText === void 0 ? NaN : Number(maxAgeText);
|
|
6613
6939
|
const expiresAt = Date.now() + maxAgeSeconds * 1e3;
|
|
6614
|
-
if (
|
|
6940
|
+
if (pair === void 0 || pair.length > 4096 || !UPSTREAM_COOKIE_PAIR.test(pair) || !Number.isSafeInteger(expiresAt) || maxAgeSeconds <= 0) throw new HttpError(502, "upstream_unavailable");
|
|
6615
6941
|
this.upstreamCookie = pair;
|
|
6616
6942
|
this.upstreamCookieExpiresAt = expiresAt;
|
|
6943
|
+
this.upstreamAuthDiagnostics.record(attempt, {
|
|
6944
|
+
outcome: "accepted",
|
|
6945
|
+
stage,
|
|
6946
|
+
status
|
|
6947
|
+
});
|
|
6617
6948
|
return pair;
|
|
6618
6949
|
} catch (error) {
|
|
6950
|
+
this.upstreamAuthDiagnostics.record(attempt, {
|
|
6951
|
+
outcome: timedOut || error instanceof HttpError && error.status === 504 ? "timeout" : outcome,
|
|
6952
|
+
stage,
|
|
6953
|
+
...status === void 0 ? {} : { status }
|
|
6954
|
+
});
|
|
6619
6955
|
if (error instanceof HttpError) throw error;
|
|
6620
6956
|
throw new HttpError(502, "upstream_unavailable");
|
|
6621
6957
|
} finally {
|
|
@@ -6789,11 +7125,15 @@ var MobileAccessGateway = class {
|
|
|
6789
7125
|
};
|
|
6790
7126
|
const cookies = browserAuthCookies(request.headers.cookie);
|
|
6791
7127
|
const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
|
|
7128
|
+
const viewId = randomBytes(16).toString("hex");
|
|
6792
7129
|
const rewritten = rewriteMobileIndexWithBatch(html, {
|
|
6793
7130
|
...options,
|
|
6794
|
-
csrfCookie
|
|
7131
|
+
csrfCookie,
|
|
7132
|
+
viewId
|
|
6795
7133
|
});
|
|
7134
|
+
operation.signal.throwIfAborted();
|
|
6796
7135
|
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
7136
|
+
if (rewritten.graphTransport) this.rememberMobileDocumentView(viewId, authorization, excludedClientModules);
|
|
6797
7137
|
body = Buffer.from(rewritten.html);
|
|
6798
7138
|
} catch (error) {
|
|
6799
7139
|
if (error instanceof ExcludedClientModulesError) {
|
|
@@ -6845,6 +7185,53 @@ var MobileAccessGateway = class {
|
|
|
6845
7185
|
operation.release();
|
|
6846
7186
|
}
|
|
6847
7187
|
}
|
|
7188
|
+
removeMobileDocumentView(view) {
|
|
7189
|
+
if (this.mobileDocumentViews.get(view.id) !== view) return;
|
|
7190
|
+
this.mobileDocumentViews.delete(view.id);
|
|
7191
|
+
for (const abort of view.abortStreams) abort();
|
|
7192
|
+
view.abortStreams.clear();
|
|
7193
|
+
}
|
|
7194
|
+
rememberMobileDocumentView(id, authorization, excluded) {
|
|
7195
|
+
for (const view of this.mobileDocumentViews.values()) if (view.expiresAt <= Date.now()) this.removeMobileDocumentView(view);
|
|
7196
|
+
const view = Object.freeze({
|
|
7197
|
+
...authorization,
|
|
7198
|
+
id,
|
|
7199
|
+
excludedClientModules: Object.freeze([...excluded]),
|
|
7200
|
+
abortStreams: /* @__PURE__ */ new Set()
|
|
7201
|
+
});
|
|
7202
|
+
this.mobileDocumentViews.set(id, view);
|
|
7203
|
+
while (this.mobileDocumentViews.size > this.config.maxSessions) {
|
|
7204
|
+
const oldest = this.mobileDocumentViews.values().next().value;
|
|
7205
|
+
if (oldest === void 0) break;
|
|
7206
|
+
this.removeMobileDocumentView(oldest);
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
requireMobileDocumentView(id, authorization, request) {
|
|
7210
|
+
const view = /^[a-f0-9]{32}$/u.test(id) ? this.mobileDocumentViews.get(id) : void 0;
|
|
7211
|
+
if (view === void 0 || view.expiresAt <= Date.now() || view.sessionKey !== authorization.sessionKey || view.deviceId !== authorization.deviceId || request.method !== "GET" || parseRequestTarget(request.url).decodedPathname !== "/plugins/events" || request.headers["transfer-encoding"] !== void 0 || request.headers["content-length"] !== void 0 && Number(request.headers["content-length"]) !== 0) throw new HttpError(409, "mobile_document_view_invalid");
|
|
7212
|
+
return view;
|
|
7213
|
+
}
|
|
7214
|
+
async projectMobileDocumentEvent(view, value, signal) {
|
|
7215
|
+
signal.throwIfAborted();
|
|
7216
|
+
if (this.mobileDocumentViews.get(view.id) !== view) throw new HttpError(409, "mobile_document_view_invalid");
|
|
7217
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new HttpError(502, "upstream_unavailable");
|
|
7218
|
+
const frame = value;
|
|
7219
|
+
if (frame.type === "rebuilt") {
|
|
7220
|
+
if (typeof frame.id !== "string" || typeof frame.rev !== "string") throw new HttpError(502, "upstream_unavailable");
|
|
7221
|
+
return frame.id === MOBILE_LAYOUT_MODULE ? void 0 : frame;
|
|
7222
|
+
}
|
|
7223
|
+
if (frame.type !== "graph" || frame.graph === null || typeof frame.graph !== "object" || Array.isArray(frame.graph)) throw new HttpError(502, "upstream_unavailable");
|
|
7224
|
+
const plan = parseMobileBootGraph(JSON.stringify(frame.graph), view.excludedClientModules);
|
|
7225
|
+
const projected = projectMobileBootGraph(plan, plan.rewrittenBatchEntries === void 0 ? {} : await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries), signal));
|
|
7226
|
+
if (!projected.graphTransport) throw new HttpError(502, "upstream_unavailable");
|
|
7227
|
+
signal.throwIfAborted();
|
|
7228
|
+
if (this.mobileDocumentViews.get(view.id) !== view) throw new HttpError(409, "mobile_document_view_invalid");
|
|
7229
|
+
for (const batch of projected.batches) this.rememberMobileBootBatch(batch);
|
|
7230
|
+
return {
|
|
7231
|
+
...frame,
|
|
7232
|
+
graph: plan.parsed
|
|
7233
|
+
};
|
|
7234
|
+
}
|
|
6848
7235
|
rememberMobileBootBatch(plan) {
|
|
6849
7236
|
const existing = this.mobileBootBatches.get(plan.key);
|
|
6850
7237
|
this.mobileBootBatches.delete(plan.key);
|
|
@@ -6877,7 +7264,8 @@ var MobileAccessGateway = class {
|
|
|
6877
7264
|
else sizes.set(source, size);
|
|
6878
7265
|
}
|
|
6879
7266
|
};
|
|
6880
|
-
await Promise.
|
|
7267
|
+
const failed = (await Promise.allSettled(Array.from({ length: Math.min(8, candidates.length) }, worker))).find((result) => result.status === "rejected");
|
|
7268
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
6881
7269
|
return {
|
|
6882
7270
|
sizes,
|
|
6883
7271
|
passThrough
|
|
@@ -6896,6 +7284,7 @@ var MobileAccessGateway = class {
|
|
|
6896
7284
|
const cached = this.bootEntrySizeCache.get(source);
|
|
6897
7285
|
if (cached !== void 0 && Date.now() - cached.at < MOBILE_BOOT_SIZE_CACHE_TTL_MS) return cached.size;
|
|
6898
7286
|
let upstreamRequest;
|
|
7287
|
+
let upstreamClosed;
|
|
6899
7288
|
const abort = () => {
|
|
6900
7289
|
upstreamRequest?.destroy(requestAbortedError());
|
|
6901
7290
|
};
|
|
@@ -6919,6 +7308,12 @@ var MobileAccessGateway = class {
|
|
|
6919
7308
|
},
|
|
6920
7309
|
agent: false
|
|
6921
7310
|
});
|
|
7311
|
+
const opened = upstreamRequest;
|
|
7312
|
+
upstreamClosed = new Promise((closed) => {
|
|
7313
|
+
opened.once("close", () => {
|
|
7314
|
+
closed();
|
|
7315
|
+
});
|
|
7316
|
+
});
|
|
6922
7317
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
6923
7318
|
upstreamRequest?.destroy(upstreamTimeoutError());
|
|
6924
7319
|
});
|
|
@@ -6954,6 +7349,7 @@ var MobileAccessGateway = class {
|
|
|
6954
7349
|
} finally {
|
|
6955
7350
|
signal?.removeEventListener("abort", abort);
|
|
6956
7351
|
upstreamRequest?.destroy();
|
|
7352
|
+
await upstreamClosed;
|
|
6957
7353
|
}
|
|
6958
7354
|
}
|
|
6959
7355
|
async serveMobileBootBatch(key, request, response, authorization) {
|
|
@@ -7152,14 +7548,15 @@ var MobileAccessGateway = class {
|
|
|
7152
7548
|
* One request slot and cancellation signal cover attempts and backoff together.
|
|
7153
7549
|
* API requests and requests with bodies are forwarded once.
|
|
7154
7550
|
*/
|
|
7155
|
-
async proxyHttp(request, response, authorization) {
|
|
7551
|
+
async proxyHttp(request, response, authorization, documentView) {
|
|
7156
7552
|
const declared = request.headers["content-length"];
|
|
7157
7553
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
7158
7554
|
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);
|
|
7159
7555
|
const holder = {};
|
|
7160
7556
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
7557
|
+
documentView?.abortStreams.add(operation.abort);
|
|
7161
7558
|
const onClose = () => {
|
|
7162
|
-
operation.release();
|
|
7559
|
+
if (documentView === void 0) operation.release();
|
|
7163
7560
|
operation.abort();
|
|
7164
7561
|
};
|
|
7165
7562
|
response.once("close", onClose);
|
|
@@ -7169,7 +7566,7 @@ var MobileAccessGateway = class {
|
|
|
7169
7566
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
7170
7567
|
operation.signal.throwIfAborted();
|
|
7171
7568
|
try {
|
|
7172
|
-
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable);
|
|
7569
|
+
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView);
|
|
7173
7570
|
return;
|
|
7174
7571
|
} catch (error) {
|
|
7175
7572
|
if (operation.signal.aborted || response.destroyed) return;
|
|
@@ -7186,13 +7583,18 @@ var MobileAccessGateway = class {
|
|
|
7186
7583
|
if (!operation.signal.aborted) throw error;
|
|
7187
7584
|
} finally {
|
|
7188
7585
|
response.removeListener("close", onClose);
|
|
7586
|
+
documentView?.abortStreams.delete(operation.abort);
|
|
7189
7587
|
operation.release();
|
|
7190
7588
|
}
|
|
7191
7589
|
}
|
|
7192
|
-
async proxyHttpOnce(request$2, response, holder, signal, replayable) {
|
|
7590
|
+
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView) {
|
|
7193
7591
|
let bodyDone;
|
|
7592
|
+
const projections = /* @__PURE__ */ new Set();
|
|
7194
7593
|
try {
|
|
7594
|
+
const pathname = parseRequestTarget(request$2.url).decodedPathname;
|
|
7195
7595
|
const upstreamHeaders = sanitizeRequestHeaders(request$2, this.config.upstreamOrigin);
|
|
7596
|
+
delete upstreamHeaders[MOBILE_DOCUMENT_VIEW_HEADER];
|
|
7597
|
+
if (documentView !== void 0) upstreamHeaders["accept-encoding"] = "identity";
|
|
7196
7598
|
if (request$2.headers["transfer-encoding"] !== void 0) upstreamHeaders["transfer-encoding"] = request$2.headers["transfer-encoding"];
|
|
7197
7599
|
const upstreamCookie = await this.upstreamCookieHeader();
|
|
7198
7600
|
signal.throwIfAborted();
|
|
@@ -7211,7 +7613,6 @@ var MobileAccessGateway = class {
|
|
|
7211
7613
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
7212
7614
|
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
7213
7615
|
});
|
|
7214
|
-
const pathname = parseRequestTarget(request$2.url).decodedPathname;
|
|
7215
7616
|
if (pathname === "/api" || pathname.startsWith("/api/")) upstreamRequest.once("finish", () => {
|
|
7216
7617
|
upstreamRequest.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
7217
7618
|
});
|
|
@@ -7223,13 +7624,28 @@ var MobileAccessGateway = class {
|
|
|
7223
7624
|
bodyDone.catch(reject);
|
|
7224
7625
|
}
|
|
7225
7626
|
});
|
|
7627
|
+
const graphEvents = documentView !== void 0 && replayable && pathname === "/plugins/events" && proxied.statusCode === 200 && proxied.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream";
|
|
7628
|
+
if (documentView !== void 0 && !graphEvents) {
|
|
7629
|
+
proxied.destroy();
|
|
7630
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
7631
|
+
}
|
|
7632
|
+
if (graphEvents) holder.request?.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
7226
7633
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
7227
7634
|
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
7635
|
+
if (graphEvents) {
|
|
7636
|
+
if (proxied.headers["content-encoding"] !== void 0 && proxied.headers["content-encoding"] !== "identity") throw new HttpError(502, "upstream_unavailable");
|
|
7637
|
+
delete headers["content-length"];
|
|
7638
|
+
delete headers["content-encoding"];
|
|
7639
|
+
delete headers.etag;
|
|
7640
|
+
delete headers["last-modified"];
|
|
7641
|
+
delete headers["content-range"];
|
|
7642
|
+
delete headers["accept-ranges"];
|
|
7643
|
+
}
|
|
7228
7644
|
const statusCode = proxied.statusCode ?? 502;
|
|
7229
7645
|
this.httpFailures.recordUpstream(request$2.method, request$2.url?.split("?", 1)[0] ?? "", statusCode);
|
|
7230
|
-
const cacheControl = revisionedStaticCacheControl(request$2, statusCode);
|
|
7646
|
+
const cacheControl = graphEvents ? void 0 : revisionedStaticCacheControl(request$2, statusCode);
|
|
7231
7647
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
7232
|
-
const compressible = canCompressResponse(request$2, proxied);
|
|
7648
|
+
const compressible = !graphEvents && canCompressResponse(request$2, proxied);
|
|
7233
7649
|
const compressed = compressible && acceptsGzip(request$2.headers["accept-encoding"]);
|
|
7234
7650
|
if (compressible) addVaryAcceptEncoding(headers);
|
|
7235
7651
|
if (compressed) {
|
|
@@ -7250,7 +7666,18 @@ var MobileAccessGateway = class {
|
|
|
7250
7666
|
addVaryAcceptEncoding(headers);
|
|
7251
7667
|
}
|
|
7252
7668
|
response.writeHead(statusCode, headers);
|
|
7253
|
-
|
|
7669
|
+
const eventTransform = !graphEvents || documentView === void 0 ? void 0 : createMobileGraphEventTransform((value) => {
|
|
7670
|
+
const task = this.projectMobileDocumentEvent(documentView, value, signal);
|
|
7671
|
+
projections.add(task);
|
|
7672
|
+
this.mobileGraphTasks.add(task);
|
|
7673
|
+
const settled = () => {
|
|
7674
|
+
projections.delete(task);
|
|
7675
|
+
this.mobileGraphTasks.delete(task);
|
|
7676
|
+
};
|
|
7677
|
+
task.then(settled, settled);
|
|
7678
|
+
return task;
|
|
7679
|
+
});
|
|
7680
|
+
await Promise.all([bodyDone, eventTransform !== void 0 ? pipeline(proxied, eventTransform, response) : compressed && request$2.method !== "HEAD" ? pipeline(proxied, createGzip(), response) : pipeline(proxied, response)]);
|
|
7254
7681
|
} catch (error) {
|
|
7255
7682
|
holder.request?.destroy();
|
|
7256
7683
|
await bodyDone?.catch(() => void 0);
|
|
@@ -7258,6 +7685,7 @@ var MobileAccessGateway = class {
|
|
|
7258
7685
|
if (response.headersSent) response.destroy();
|
|
7259
7686
|
throw error;
|
|
7260
7687
|
} finally {
|
|
7688
|
+
await Promise.allSettled(projections);
|
|
7261
7689
|
delete holder.request;
|
|
7262
7690
|
}
|
|
7263
7691
|
}
|
|
@@ -7648,9 +8076,13 @@ var MobileAccessGateway = class {
|
|
|
7648
8076
|
this.upstreamAuthRequest = void 0;
|
|
7649
8077
|
this.removeSessionListener();
|
|
7650
8078
|
this.pendingDeviceRevocations.clear();
|
|
8079
|
+
for (const view of this.mobileDocumentViews.values()) this.removeMobileDocumentView(view);
|
|
7651
8080
|
const accessClose = this.access.close();
|
|
7652
8081
|
for (const stored of this.mobileBootBatches.values()) stored.assembly?.controller.abort();
|
|
7653
|
-
for (const request of this.activeRequests.values())
|
|
8082
|
+
for (const request of this.activeRequests.values()) {
|
|
8083
|
+
clearTimeout(request.timer);
|
|
8084
|
+
request.abort();
|
|
8085
|
+
}
|
|
7654
8086
|
for (const websocket of this.activeWebSockets.values()) {
|
|
7655
8087
|
websocket.client.destroy();
|
|
7656
8088
|
websocket.upstream.destroy();
|
|
@@ -7675,6 +8107,7 @@ var MobileAccessGateway = class {
|
|
|
7675
8107
|
await accessClose;
|
|
7676
8108
|
await Promise.allSettled(this.clientModuleTasks);
|
|
7677
8109
|
await Promise.allSettled(this.clientModuleRevocations);
|
|
8110
|
+
await Promise.allSettled(this.mobileGraphTasks);
|
|
7678
8111
|
this.activeRequests.clear();
|
|
7679
8112
|
this.activeWebSockets.clear();
|
|
7680
8113
|
this.connectedSockets.clear();
|
|
@@ -17264,16 +17697,20 @@ async function loadOrCreateInstallationId(file) {
|
|
|
17264
17697
|
//#endregion
|
|
17265
17698
|
//#region src/gateway-identity.ts
|
|
17266
17699
|
/**
|
|
17267
|
-
*
|
|
17700
|
+
* Attach the authenticated installation identity while retaining existing entry identity rules.
|
|
17268
17701
|
* @param config - Validated gateway configuration containing the legacy identity.
|
|
17269
17702
|
* @param installationId - Confirmed random identity read from this installation's state.
|
|
17270
17703
|
* @param explicitIdentity - Whether the operator supplied the gateway identity.
|
|
17271
17704
|
* @returns Configuration retaining certificate/operator identities or the new and legacy public identities.
|
|
17272
17705
|
*/
|
|
17273
17706
|
function withInstallationIdentity(config, installationId, explicitIdentity) {
|
|
17274
|
-
if (config.pairingCaFile !== void 0 || explicitIdentity) return
|
|
17707
|
+
if (config.pairingCaFile !== void 0 || explicitIdentity) return Object.freeze({
|
|
17708
|
+
...config,
|
|
17709
|
+
installationId
|
|
17710
|
+
});
|
|
17275
17711
|
return Object.freeze({
|
|
17276
17712
|
...config,
|
|
17713
|
+
installationId,
|
|
17277
17714
|
instanceId: installationId,
|
|
17278
17715
|
legacyInstanceId: config.instanceId
|
|
17279
17716
|
});
|
|
@@ -17659,10 +18096,10 @@ async function apply(ctx, config) {
|
|
|
17659
18096
|
const dshVersion = installedDshVersion();
|
|
17660
18097
|
const loaded = await loadSetup(config);
|
|
17661
18098
|
const mobileAccess = createMobileAccessService(ctx);
|
|
17662
|
-
const
|
|
17663
|
-
const upstreamLoginUrl = upstreamAuthenticatedUrl(ctx,
|
|
17664
|
-
const instanceId = await stableInstanceId(loaded,
|
|
17665
|
-
const stateDirectory = dirname(
|
|
18099
|
+
const baseTemplate = loopbackTemplate(loaded, ctx.webServer.port);
|
|
18100
|
+
const upstreamLoginUrl = upstreamAuthenticatedUrl(ctx, baseTemplate.upstreamOrigin);
|
|
18101
|
+
const instanceId = await stableInstanceId(loaded, baseTemplate);
|
|
18102
|
+
const stateDirectory = dirname(baseTemplate.stateFile);
|
|
17666
18103
|
const logFile = await installMobileFileLogger(ctx, stateDirectory);
|
|
17667
18104
|
const logger = ctx.logger("dsh-mobile");
|
|
17668
18105
|
logger.info("logging initialized file=%s", logFile);
|
|
@@ -17685,6 +18122,10 @@ async function apply(ctx, config) {
|
|
|
17685
18122
|
const frpComponent = new FrpComponentManager({ stateDirectory });
|
|
17686
18123
|
await frpComponent.initialize();
|
|
17687
18124
|
const frpIdentity = await loadOrCreateInstallationId(join(stateDirectory, "installation-id"));
|
|
18125
|
+
const template = Object.freeze({
|
|
18126
|
+
...baseTemplate,
|
|
18127
|
+
installationId: frpIdentity
|
|
18128
|
+
});
|
|
17688
18129
|
const remoteInstanceId = loaded.kind === "managed" ? frpIdentity : loaded.config.instanceId ?? frpIdentity;
|
|
17689
18130
|
const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"), frpProxyName(frpIdentity), stateDirectory);
|
|
17690
18131
|
await frpConfig.initialize();
|
|
@@ -17747,6 +18188,8 @@ async function apply(ctx, config) {
|
|
|
17747
18188
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17748
18189
|
}, (entry) => {
|
|
17749
18190
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18191
|
+
}, (entry) => {
|
|
18192
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17750
18193
|
});
|
|
17751
18194
|
await candidate.start();
|
|
17752
18195
|
lanGateway = candidate;
|
|
@@ -17807,6 +18250,8 @@ async function apply(ctx, config) {
|
|
|
17807
18250
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17808
18251
|
}, (entry) => {
|
|
17809
18252
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18253
|
+
}, (entry) => {
|
|
18254
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17810
18255
|
});
|
|
17811
18256
|
await candidate.start();
|
|
17812
18257
|
return candidate;
|
|
@@ -17827,6 +18272,8 @@ async function apply(ctx, config) {
|
|
|
17827
18272
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17828
18273
|
}, (entry) => {
|
|
17829
18274
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18275
|
+
}, (entry) => {
|
|
18276
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17830
18277
|
});
|
|
17831
18278
|
try {
|
|
17832
18279
|
await candidate.start();
|
|
@@ -17842,6 +18289,8 @@ async function apply(ctx, config) {
|
|
|
17842
18289
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17843
18290
|
}, (entry) => {
|
|
17844
18291
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18292
|
+
}, (entry) => {
|
|
18293
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17845
18294
|
});
|
|
17846
18295
|
try {
|
|
17847
18296
|
await candidate.start();
|