dsh-mobile 0.6.13 → 0.6.15
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 +27 -4
- package/CONTRIBUTORS.md +20 -2
- package/README.en.md +13 -7
- package/README.md +13 -7
- package/SECURITY.md +5 -2
- package/THIRD_PARTY_NOTICES.md +30 -2
- package/apps/mobile/README.md +10 -4
- package/apps/mobile/README.zh-CN.md +10 -4
- package/apps/mobile/release.json +3 -3
- package/docs/SELF_HOSTED_ORIGIN.en.md +34 -6
- package/docs/SELF_HOSTED_ORIGIN.md +34 -6
- package/docs/SLOW_CONNECTIONS.en.md +14 -4
- package/docs/SLOW_CONNECTIONS.md +14 -4
- package/lib/client.js +102 -32
- package/lib/client.js.map +1 -1
- package/lib/extension-worker-runtime.mjs.map +1 -1
- package/lib/index.d.mts +25 -3
- package/lib/index.mjs +478 -54
- 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 +3 -1
- 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);
|
|
@@ -4660,6 +4921,7 @@ const MDNS_SERVICE_TYPE = "dsh-mobile";
|
|
|
4660
4921
|
const MOBILE_LAYOUT_MODULE = "@deepseek-ai/dsh-client-ui-layout";
|
|
4661
4922
|
const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
4662
4923
|
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
4924
|
+
const MOBILE_DOCUMENT_VIEW_HEADER = "x-dsh-mobile-view";
|
|
4663
4925
|
const MAX_MOBILE_BOOT_BATCH_BYTES = 33554432;
|
|
4664
4926
|
const MAX_MOBILE_BOOT_ENTRY_BYTES = 8388608;
|
|
4665
4927
|
/**
|
|
@@ -4741,7 +5003,9 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
|
4741
5003
|
function mobileCsrfFetchBootstrap(cookieName) {
|
|
4742
5004
|
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
5005
|
}
|
|
4744
|
-
|
|
5006
|
+
function mobileAuthenticatedTransportBootstrap(viewId) {
|
|
5007
|
+
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)}})();`;
|
|
5008
|
+
}
|
|
4745
5009
|
/** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
|
|
4746
5010
|
const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
|
|
4747
5011
|
const view=globalThis;
|
|
@@ -4911,6 +5175,14 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4911
5175
|
const scriptEnd = html.indexOf("<\/script>", valueStart);
|
|
4912
5176
|
if (scriptEnd < 0) throw new Error("upstream DSH boot manifest script is incomplete");
|
|
4913
5177
|
const source = html.slice(valueStart, scriptEnd).trim().replace(/;$/u, "");
|
|
5178
|
+
return Object.freeze({
|
|
5179
|
+
...parseMobileBootGraph(source, excludedClientModules),
|
|
5180
|
+
assignment: assignment[0],
|
|
5181
|
+
replaceStart,
|
|
5182
|
+
replaceEnd: scriptEnd
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
5185
|
+
function parseMobileBootGraph(source, excludedClientModules) {
|
|
4914
5186
|
const parsed = JSON.parse(source);
|
|
4915
5187
|
if (typeof parsed.rev !== "string" || !Array.isArray(parsed.entries)) throw new Error("upstream DSH boot manifest is malformed");
|
|
4916
5188
|
const entries = parsed.entries;
|
|
@@ -4927,10 +5199,7 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4927
5199
|
return Object.freeze({
|
|
4928
5200
|
parsed,
|
|
4929
5201
|
entries,
|
|
4930
|
-
slotProvider: dependencyProfile.slots
|
|
4931
|
-
assignment: assignment[0],
|
|
4932
|
-
replaceStart,
|
|
4933
|
-
replaceEnd: scriptEnd
|
|
5202
|
+
slotProvider: dependencyProfile.slots
|
|
4934
5203
|
});
|
|
4935
5204
|
}
|
|
4936
5205
|
if (!Array.isArray(parsedBatches)) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
@@ -4969,9 +5238,6 @@ function parseMobileBootPlan(html, excludedClientModules = []) {
|
|
|
4969
5238
|
entries,
|
|
4970
5239
|
batches,
|
|
4971
5240
|
slotProvider: dependencyProfile.slots,
|
|
4972
|
-
assignment: assignment[0],
|
|
4973
|
-
replaceStart,
|
|
4974
|
-
replaceEnd: scriptEnd,
|
|
4975
5241
|
layoutBatch,
|
|
4976
5242
|
planEntries,
|
|
4977
5243
|
originalApplicationBatches,
|
|
@@ -5055,6 +5321,17 @@ function rewriteMobileApplicationPreloads(html, replacements) {
|
|
|
5055
5321
|
}
|
|
5056
5322
|
function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
5057
5323
|
const plan = parseMobileBootPlan(html, options.excludedClientModules);
|
|
5324
|
+
const projected = projectMobileBootGraph(plan, options);
|
|
5325
|
+
const transportBootstrap = projected.graphTransport ? mobileAuthenticatedTransportBootstrap(options.viewId) : "";
|
|
5326
|
+
const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${transportBootstrap}${mobileCsrfFetchBootstrap(options.csrfCookie ?? "dsh_ma_csrf")}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
|
|
5327
|
+
const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
|
|
5328
|
+
return Object.freeze({
|
|
5329
|
+
html: ensureMobileGatewayMarker(ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, projected.preloadReplacements)))),
|
|
5330
|
+
batches: projected.batches,
|
|
5331
|
+
graphTransport: projected.graphTransport
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
5334
|
+
function projectMobileBootGraph(plan, options) {
|
|
5058
5335
|
const remoteSettings = orderAuthenticatedSettings(plan.entries, plan.slotProvider);
|
|
5059
5336
|
let batches = [];
|
|
5060
5337
|
let preloadReplacements = /* @__PURE__ */ new Map();
|
|
@@ -5080,11 +5357,10 @@ function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
|
5080
5357
|
batches: plan.batches
|
|
5081
5358
|
})).digest("hex").slice(0, 16);
|
|
5082
5359
|
}
|
|
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
5360
|
return Object.freeze({
|
|
5086
|
-
|
|
5087
|
-
|
|
5361
|
+
batches,
|
|
5362
|
+
preloadReplacements,
|
|
5363
|
+
graphTransport: remoteSettings
|
|
5088
5364
|
});
|
|
5089
5365
|
}
|
|
5090
5366
|
/** Rewrite the dedicated mobile graph; exclusions are opt-in and leave stock DSH unchanged. */
|
|
@@ -5515,6 +5791,8 @@ var MobileAccessGateway = class {
|
|
|
5515
5791
|
activeRequests = /* @__PURE__ */ new Map();
|
|
5516
5792
|
activeWebSockets = /* @__PURE__ */ new Map();
|
|
5517
5793
|
mobileBootBatches = /* @__PURE__ */ new Map();
|
|
5794
|
+
mobileDocumentViews = /* @__PURE__ */ new Map();
|
|
5795
|
+
mobileGraphTasks = /* @__PURE__ */ new Set();
|
|
5518
5796
|
/** Cached upstream bundle byte sizes (HEAD probes), keyed by resource URL. */
|
|
5519
5797
|
bootEntrySizeCache = /* @__PURE__ */ new Map();
|
|
5520
5798
|
extensionEventListeners = /* @__PURE__ */ new Set();
|
|
@@ -5547,7 +5825,8 @@ var MobileAccessGateway = class {
|
|
|
5547
5825
|
clientModuleRevocations = /* @__PURE__ */ new Set();
|
|
5548
5826
|
authFailures;
|
|
5549
5827
|
httpFailures;
|
|
5550
|
-
|
|
5828
|
+
upstreamAuthDiagnostics;
|
|
5829
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded, clientModulePreferences, authFailureLog, httpFailureLog, upstreamAuthLog) {
|
|
5551
5830
|
this.config = config;
|
|
5552
5831
|
this.extensions = extensions;
|
|
5553
5832
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
@@ -5556,6 +5835,7 @@ var MobileAccessGateway = class {
|
|
|
5556
5835
|
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
5557
5836
|
this.authFailures = new AuthFailureRecorder(authFailureLog);
|
|
5558
5837
|
this.httpFailures = new HttpFailureRecorder(httpFailureLog);
|
|
5838
|
+
this.upstreamAuthDiagnostics = new UpstreamAuthRecorder(upstreamAuthLog);
|
|
5559
5839
|
this.clientModulePreferences = clientModulePreferences ?? new ClientModulePreferenceStore(join(dirname(config.stateFile), `${basename(config.stateFile)}.client-modules.json`), config.excludedClientModules);
|
|
5560
5840
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
5561
5841
|
this.tlsEnabled = config.publicTls;
|
|
@@ -5578,6 +5858,7 @@ var MobileAccessGateway = class {
|
|
|
5578
5858
|
this.probeFailureLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5579
5859
|
this.probeDeviceLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5580
5860
|
this.removeSessionListener = this.access.onSessionEnded((authorization, reason) => {
|
|
5861
|
+
for (const view of this.mobileDocumentViews.values()) if (view.sessionKey === authorization.sessionKey) this.removeMobileDocumentView(view);
|
|
5581
5862
|
if (reason === "revoked") {
|
|
5582
5863
|
const cleanup = this.clientModulePreferences.removeDevice(authorization.deviceId).catch(() => {
|
|
5583
5864
|
process.emitWarning("Unable to remove revoked device client preferences", { code: "DSH_MOBILE_PREFERENCE_CLEANUP_FAILED" });
|
|
@@ -6209,6 +6490,8 @@ var MobileAccessGateway = class {
|
|
|
6209
6490
|
}
|
|
6210
6491
|
throw error;
|
|
6211
6492
|
}
|
|
6493
|
+
const viewId = headerValue(request.headers, MOBILE_DOCUMENT_VIEW_HEADER);
|
|
6494
|
+
const documentView = viewId === void 0 ? void 0 : this.requireMobileDocumentView(viewId, authorization, request);
|
|
6212
6495
|
if (isMutation) try {
|
|
6213
6496
|
this.requireCsrf(request, authorization);
|
|
6214
6497
|
} catch (error) {
|
|
@@ -6336,7 +6619,7 @@ var MobileAccessGateway = class {
|
|
|
6336
6619
|
return;
|
|
6337
6620
|
}
|
|
6338
6621
|
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
6339
|
-
await this.proxyHttp(request, response, authorization);
|
|
6622
|
+
await this.proxyHttp(request, response, authorization, documentView);
|
|
6340
6623
|
}
|
|
6341
6624
|
async handleExtensionRequest(targetInfo, target, request, response, authorization) {
|
|
6342
6625
|
const extensions = this.extensions;
|
|
@@ -6568,16 +6851,23 @@ var MobileAccessGateway = class {
|
|
|
6568
6851
|
}
|
|
6569
6852
|
}
|
|
6570
6853
|
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");
|
|
6854
|
+
const attempt = this.upstreamAuthDiagnostics.begin();
|
|
6855
|
+
let stage = "target";
|
|
6856
|
+
let outcome = "invalid-target";
|
|
6857
|
+
let status;
|
|
6858
|
+
let timedOut = false;
|
|
6580
6859
|
try {
|
|
6860
|
+
const authenticatedUrl = this.upstreamAuthenticatedUrl;
|
|
6861
|
+
if (authenticatedUrl === void 0) throw new HttpError(502, "upstream_unavailable");
|
|
6862
|
+
let target;
|
|
6863
|
+
try {
|
|
6864
|
+
target = new URL(authenticatedUrl);
|
|
6865
|
+
} catch {
|
|
6866
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
6867
|
+
}
|
|
6868
|
+
if (target.origin !== this.config.upstreamOrigin.origin || target.pathname !== "/" || target.hash !== "" || target.search === "") throw new HttpError(502, "upstream_unavailable");
|
|
6869
|
+
stage = "request";
|
|
6870
|
+
outcome = "transport-error";
|
|
6581
6871
|
const proxied = await new Promise((resolve, reject) => {
|
|
6582
6872
|
const upstreamRequest = request({
|
|
6583
6873
|
protocol: "http:",
|
|
@@ -6595,27 +6885,46 @@ var MobileAccessGateway = class {
|
|
|
6595
6885
|
});
|
|
6596
6886
|
this.upstreamAuthRequest = upstreamRequest;
|
|
6597
6887
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
6888
|
+
timedOut = true;
|
|
6598
6889
|
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
6599
6890
|
});
|
|
6600
6891
|
upstreamRequest.once("response", resolve);
|
|
6601
6892
|
upstreamRequest.once("error", reject);
|
|
6602
6893
|
upstreamRequest.end();
|
|
6603
6894
|
});
|
|
6895
|
+
stage = "response";
|
|
6896
|
+
status = proxied.statusCode;
|
|
6604
6897
|
await new Promise((resolve, reject) => {
|
|
6605
6898
|
proxied.once("end", resolve);
|
|
6606
6899
|
proxied.once("error", reject);
|
|
6607
6900
|
proxied.resume();
|
|
6608
6901
|
});
|
|
6902
|
+
if (status !== 303) {
|
|
6903
|
+
outcome = "unexpected-status";
|
|
6904
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
6905
|
+
}
|
|
6906
|
+
stage = "cookie";
|
|
6907
|
+
outcome = "invalid-cookie";
|
|
6609
6908
|
const setCookie = proxied.headers["set-cookie"]?.[0];
|
|
6610
6909
|
const pair = setCookie?.split(";", 1)[0];
|
|
6611
6910
|
const maxAgeText = setCookie === void 0 ? void 0 : /(?:^|;\s*)Max-Age=(\d+)(?:;|$)/iu.exec(setCookie)?.[1];
|
|
6612
6911
|
const maxAgeSeconds = maxAgeText === void 0 ? NaN : Number(maxAgeText);
|
|
6613
6912
|
const expiresAt = Date.now() + maxAgeSeconds * 1e3;
|
|
6614
|
-
if (
|
|
6913
|
+
if (pair === void 0 || pair.length > 4096 || !UPSTREAM_COOKIE_PAIR.test(pair) || !Number.isSafeInteger(expiresAt) || maxAgeSeconds <= 0) throw new HttpError(502, "upstream_unavailable");
|
|
6615
6914
|
this.upstreamCookie = pair;
|
|
6616
6915
|
this.upstreamCookieExpiresAt = expiresAt;
|
|
6916
|
+
this.upstreamAuthDiagnostics.record(attempt, {
|
|
6917
|
+
outcome: "accepted",
|
|
6918
|
+
stage,
|
|
6919
|
+
status
|
|
6920
|
+
});
|
|
6617
6921
|
return pair;
|
|
6618
6922
|
} catch (error) {
|
|
6923
|
+
this.upstreamAuthDiagnostics.record(attempt, {
|
|
6924
|
+
outcome: timedOut || error instanceof HttpError && error.status === 504 ? "timeout" : outcome,
|
|
6925
|
+
stage,
|
|
6926
|
+
...status === void 0 ? {} : { status }
|
|
6927
|
+
});
|
|
6619
6928
|
if (error instanceof HttpError) throw error;
|
|
6620
6929
|
throw new HttpError(502, "upstream_unavailable");
|
|
6621
6930
|
} finally {
|
|
@@ -6789,11 +7098,15 @@ var MobileAccessGateway = class {
|
|
|
6789
7098
|
};
|
|
6790
7099
|
const cookies = browserAuthCookies(request.headers.cookie);
|
|
6791
7100
|
const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
|
|
7101
|
+
const viewId = randomBytes(16).toString("hex");
|
|
6792
7102
|
const rewritten = rewriteMobileIndexWithBatch(html, {
|
|
6793
7103
|
...options,
|
|
6794
|
-
csrfCookie
|
|
7104
|
+
csrfCookie,
|
|
7105
|
+
viewId
|
|
6795
7106
|
});
|
|
7107
|
+
operation.signal.throwIfAborted();
|
|
6796
7108
|
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
7109
|
+
if (rewritten.graphTransport) this.rememberMobileDocumentView(viewId, authorization, excludedClientModules);
|
|
6797
7110
|
body = Buffer.from(rewritten.html);
|
|
6798
7111
|
} catch (error) {
|
|
6799
7112
|
if (error instanceof ExcludedClientModulesError) {
|
|
@@ -6845,6 +7158,53 @@ var MobileAccessGateway = class {
|
|
|
6845
7158
|
operation.release();
|
|
6846
7159
|
}
|
|
6847
7160
|
}
|
|
7161
|
+
removeMobileDocumentView(view) {
|
|
7162
|
+
if (this.mobileDocumentViews.get(view.id) !== view) return;
|
|
7163
|
+
this.mobileDocumentViews.delete(view.id);
|
|
7164
|
+
for (const abort of view.abortStreams) abort();
|
|
7165
|
+
view.abortStreams.clear();
|
|
7166
|
+
}
|
|
7167
|
+
rememberMobileDocumentView(id, authorization, excluded) {
|
|
7168
|
+
for (const view of this.mobileDocumentViews.values()) if (view.expiresAt <= Date.now()) this.removeMobileDocumentView(view);
|
|
7169
|
+
const view = Object.freeze({
|
|
7170
|
+
...authorization,
|
|
7171
|
+
id,
|
|
7172
|
+
excludedClientModules: Object.freeze([...excluded]),
|
|
7173
|
+
abortStreams: /* @__PURE__ */ new Set()
|
|
7174
|
+
});
|
|
7175
|
+
this.mobileDocumentViews.set(id, view);
|
|
7176
|
+
while (this.mobileDocumentViews.size > this.config.maxSessions) {
|
|
7177
|
+
const oldest = this.mobileDocumentViews.values().next().value;
|
|
7178
|
+
if (oldest === void 0) break;
|
|
7179
|
+
this.removeMobileDocumentView(oldest);
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
requireMobileDocumentView(id, authorization, request) {
|
|
7183
|
+
const view = /^[a-f0-9]{32}$/u.test(id) ? this.mobileDocumentViews.get(id) : void 0;
|
|
7184
|
+
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");
|
|
7185
|
+
return view;
|
|
7186
|
+
}
|
|
7187
|
+
async projectMobileDocumentEvent(view, value, signal) {
|
|
7188
|
+
signal.throwIfAborted();
|
|
7189
|
+
if (this.mobileDocumentViews.get(view.id) !== view) throw new HttpError(409, "mobile_document_view_invalid");
|
|
7190
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new HttpError(502, "upstream_unavailable");
|
|
7191
|
+
const frame = value;
|
|
7192
|
+
if (frame.type === "rebuilt") {
|
|
7193
|
+
if (typeof frame.id !== "string" || typeof frame.rev !== "string") throw new HttpError(502, "upstream_unavailable");
|
|
7194
|
+
return frame.id === MOBILE_LAYOUT_MODULE ? void 0 : frame;
|
|
7195
|
+
}
|
|
7196
|
+
if (frame.type !== "graph" || frame.graph === null || typeof frame.graph !== "object" || Array.isArray(frame.graph)) throw new HttpError(502, "upstream_unavailable");
|
|
7197
|
+
const plan = parseMobileBootGraph(JSON.stringify(frame.graph), view.excludedClientModules);
|
|
7198
|
+
const projected = projectMobileBootGraph(plan, plan.rewrittenBatchEntries === void 0 ? {} : await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries), signal));
|
|
7199
|
+
if (!projected.graphTransport) throw new HttpError(502, "upstream_unavailable");
|
|
7200
|
+
signal.throwIfAborted();
|
|
7201
|
+
if (this.mobileDocumentViews.get(view.id) !== view) throw new HttpError(409, "mobile_document_view_invalid");
|
|
7202
|
+
for (const batch of projected.batches) this.rememberMobileBootBatch(batch);
|
|
7203
|
+
return {
|
|
7204
|
+
...frame,
|
|
7205
|
+
graph: plan.parsed
|
|
7206
|
+
};
|
|
7207
|
+
}
|
|
6848
7208
|
rememberMobileBootBatch(plan) {
|
|
6849
7209
|
const existing = this.mobileBootBatches.get(plan.key);
|
|
6850
7210
|
this.mobileBootBatches.delete(plan.key);
|
|
@@ -6877,7 +7237,8 @@ var MobileAccessGateway = class {
|
|
|
6877
7237
|
else sizes.set(source, size);
|
|
6878
7238
|
}
|
|
6879
7239
|
};
|
|
6880
|
-
await Promise.
|
|
7240
|
+
const failed = (await Promise.allSettled(Array.from({ length: Math.min(8, candidates.length) }, worker))).find((result) => result.status === "rejected");
|
|
7241
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
6881
7242
|
return {
|
|
6882
7243
|
sizes,
|
|
6883
7244
|
passThrough
|
|
@@ -6896,6 +7257,7 @@ var MobileAccessGateway = class {
|
|
|
6896
7257
|
const cached = this.bootEntrySizeCache.get(source);
|
|
6897
7258
|
if (cached !== void 0 && Date.now() - cached.at < MOBILE_BOOT_SIZE_CACHE_TTL_MS) return cached.size;
|
|
6898
7259
|
let upstreamRequest;
|
|
7260
|
+
let upstreamClosed;
|
|
6899
7261
|
const abort = () => {
|
|
6900
7262
|
upstreamRequest?.destroy(requestAbortedError());
|
|
6901
7263
|
};
|
|
@@ -6919,6 +7281,12 @@ var MobileAccessGateway = class {
|
|
|
6919
7281
|
},
|
|
6920
7282
|
agent: false
|
|
6921
7283
|
});
|
|
7284
|
+
const opened = upstreamRequest;
|
|
7285
|
+
upstreamClosed = new Promise((closed) => {
|
|
7286
|
+
opened.once("close", () => {
|
|
7287
|
+
closed();
|
|
7288
|
+
});
|
|
7289
|
+
});
|
|
6922
7290
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
6923
7291
|
upstreamRequest?.destroy(upstreamTimeoutError());
|
|
6924
7292
|
});
|
|
@@ -6954,6 +7322,7 @@ var MobileAccessGateway = class {
|
|
|
6954
7322
|
} finally {
|
|
6955
7323
|
signal?.removeEventListener("abort", abort);
|
|
6956
7324
|
upstreamRequest?.destroy();
|
|
7325
|
+
await upstreamClosed;
|
|
6957
7326
|
}
|
|
6958
7327
|
}
|
|
6959
7328
|
async serveMobileBootBatch(key, request, response, authorization) {
|
|
@@ -7152,14 +7521,15 @@ var MobileAccessGateway = class {
|
|
|
7152
7521
|
* One request slot and cancellation signal cover attempts and backoff together.
|
|
7153
7522
|
* API requests and requests with bodies are forwarded once.
|
|
7154
7523
|
*/
|
|
7155
|
-
async proxyHttp(request, response, authorization) {
|
|
7524
|
+
async proxyHttp(request, response, authorization, documentView) {
|
|
7156
7525
|
const declared = request.headers["content-length"];
|
|
7157
7526
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
7158
7527
|
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
7528
|
const holder = {};
|
|
7160
7529
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
7530
|
+
documentView?.abortStreams.add(operation.abort);
|
|
7161
7531
|
const onClose = () => {
|
|
7162
|
-
operation.release();
|
|
7532
|
+
if (documentView === void 0) operation.release();
|
|
7163
7533
|
operation.abort();
|
|
7164
7534
|
};
|
|
7165
7535
|
response.once("close", onClose);
|
|
@@ -7169,7 +7539,7 @@ var MobileAccessGateway = class {
|
|
|
7169
7539
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
7170
7540
|
operation.signal.throwIfAborted();
|
|
7171
7541
|
try {
|
|
7172
|
-
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable);
|
|
7542
|
+
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView);
|
|
7173
7543
|
return;
|
|
7174
7544
|
} catch (error) {
|
|
7175
7545
|
if (operation.signal.aborted || response.destroyed) return;
|
|
@@ -7186,13 +7556,18 @@ var MobileAccessGateway = class {
|
|
|
7186
7556
|
if (!operation.signal.aborted) throw error;
|
|
7187
7557
|
} finally {
|
|
7188
7558
|
response.removeListener("close", onClose);
|
|
7559
|
+
documentView?.abortStreams.delete(operation.abort);
|
|
7189
7560
|
operation.release();
|
|
7190
7561
|
}
|
|
7191
7562
|
}
|
|
7192
|
-
async proxyHttpOnce(request$2, response, holder, signal, replayable) {
|
|
7563
|
+
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView) {
|
|
7193
7564
|
let bodyDone;
|
|
7565
|
+
const projections = /* @__PURE__ */ new Set();
|
|
7194
7566
|
try {
|
|
7567
|
+
const pathname = parseRequestTarget(request$2.url).decodedPathname;
|
|
7195
7568
|
const upstreamHeaders = sanitizeRequestHeaders(request$2, this.config.upstreamOrigin);
|
|
7569
|
+
delete upstreamHeaders[MOBILE_DOCUMENT_VIEW_HEADER];
|
|
7570
|
+
if (documentView !== void 0) upstreamHeaders["accept-encoding"] = "identity";
|
|
7196
7571
|
if (request$2.headers["transfer-encoding"] !== void 0) upstreamHeaders["transfer-encoding"] = request$2.headers["transfer-encoding"];
|
|
7197
7572
|
const upstreamCookie = await this.upstreamCookieHeader();
|
|
7198
7573
|
signal.throwIfAborted();
|
|
@@ -7211,7 +7586,6 @@ var MobileAccessGateway = class {
|
|
|
7211
7586
|
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
7212
7587
|
upstreamRequest.destroy(new HttpError(504, "upstream_timeout"));
|
|
7213
7588
|
});
|
|
7214
|
-
const pathname = parseRequestTarget(request$2.url).decodedPathname;
|
|
7215
7589
|
if (pathname === "/api" || pathname.startsWith("/api/")) upstreamRequest.once("finish", () => {
|
|
7216
7590
|
upstreamRequest.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
7217
7591
|
});
|
|
@@ -7223,13 +7597,28 @@ var MobileAccessGateway = class {
|
|
|
7223
7597
|
bodyDone.catch(reject);
|
|
7224
7598
|
}
|
|
7225
7599
|
});
|
|
7600
|
+
const graphEvents = documentView !== void 0 && replayable && pathname === "/plugins/events" && proxied.statusCode === 200 && proxied.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream";
|
|
7601
|
+
if (documentView !== void 0 && !graphEvents) {
|
|
7602
|
+
proxied.destroy();
|
|
7603
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
7604
|
+
}
|
|
7605
|
+
if (graphEvents) holder.request?.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
7226
7606
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
7227
7607
|
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
7608
|
+
if (graphEvents) {
|
|
7609
|
+
if (proxied.headers["content-encoding"] !== void 0 && proxied.headers["content-encoding"] !== "identity") throw new HttpError(502, "upstream_unavailable");
|
|
7610
|
+
delete headers["content-length"];
|
|
7611
|
+
delete headers["content-encoding"];
|
|
7612
|
+
delete headers.etag;
|
|
7613
|
+
delete headers["last-modified"];
|
|
7614
|
+
delete headers["content-range"];
|
|
7615
|
+
delete headers["accept-ranges"];
|
|
7616
|
+
}
|
|
7228
7617
|
const statusCode = proxied.statusCode ?? 502;
|
|
7229
7618
|
this.httpFailures.recordUpstream(request$2.method, request$2.url?.split("?", 1)[0] ?? "", statusCode);
|
|
7230
|
-
const cacheControl = revisionedStaticCacheControl(request$2, statusCode);
|
|
7619
|
+
const cacheControl = graphEvents ? void 0 : revisionedStaticCacheControl(request$2, statusCode);
|
|
7231
7620
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
7232
|
-
const compressible = canCompressResponse(request$2, proxied);
|
|
7621
|
+
const compressible = !graphEvents && canCompressResponse(request$2, proxied);
|
|
7233
7622
|
const compressed = compressible && acceptsGzip(request$2.headers["accept-encoding"]);
|
|
7234
7623
|
if (compressible) addVaryAcceptEncoding(headers);
|
|
7235
7624
|
if (compressed) {
|
|
@@ -7250,7 +7639,18 @@ var MobileAccessGateway = class {
|
|
|
7250
7639
|
addVaryAcceptEncoding(headers);
|
|
7251
7640
|
}
|
|
7252
7641
|
response.writeHead(statusCode, headers);
|
|
7253
|
-
|
|
7642
|
+
const eventTransform = !graphEvents || documentView === void 0 ? void 0 : createMobileGraphEventTransform((value) => {
|
|
7643
|
+
const task = this.projectMobileDocumentEvent(documentView, value, signal);
|
|
7644
|
+
projections.add(task);
|
|
7645
|
+
this.mobileGraphTasks.add(task);
|
|
7646
|
+
const settled = () => {
|
|
7647
|
+
projections.delete(task);
|
|
7648
|
+
this.mobileGraphTasks.delete(task);
|
|
7649
|
+
};
|
|
7650
|
+
task.then(settled, settled);
|
|
7651
|
+
return task;
|
|
7652
|
+
});
|
|
7653
|
+
await Promise.all([bodyDone, eventTransform !== void 0 ? pipeline(proxied, eventTransform, response) : compressed && request$2.method !== "HEAD" ? pipeline(proxied, createGzip(), response) : pipeline(proxied, response)]);
|
|
7254
7654
|
} catch (error) {
|
|
7255
7655
|
holder.request?.destroy();
|
|
7256
7656
|
await bodyDone?.catch(() => void 0);
|
|
@@ -7258,6 +7658,7 @@ var MobileAccessGateway = class {
|
|
|
7258
7658
|
if (response.headersSent) response.destroy();
|
|
7259
7659
|
throw error;
|
|
7260
7660
|
} finally {
|
|
7661
|
+
await Promise.allSettled(projections);
|
|
7261
7662
|
delete holder.request;
|
|
7262
7663
|
}
|
|
7263
7664
|
}
|
|
@@ -7648,9 +8049,13 @@ var MobileAccessGateway = class {
|
|
|
7648
8049
|
this.upstreamAuthRequest = void 0;
|
|
7649
8050
|
this.removeSessionListener();
|
|
7650
8051
|
this.pendingDeviceRevocations.clear();
|
|
8052
|
+
for (const view of this.mobileDocumentViews.values()) this.removeMobileDocumentView(view);
|
|
7651
8053
|
const accessClose = this.access.close();
|
|
7652
8054
|
for (const stored of this.mobileBootBatches.values()) stored.assembly?.controller.abort();
|
|
7653
|
-
for (const request of this.activeRequests.values())
|
|
8055
|
+
for (const request of this.activeRequests.values()) {
|
|
8056
|
+
clearTimeout(request.timer);
|
|
8057
|
+
request.abort();
|
|
8058
|
+
}
|
|
7654
8059
|
for (const websocket of this.activeWebSockets.values()) {
|
|
7655
8060
|
websocket.client.destroy();
|
|
7656
8061
|
websocket.upstream.destroy();
|
|
@@ -7675,6 +8080,7 @@ var MobileAccessGateway = class {
|
|
|
7675
8080
|
await accessClose;
|
|
7676
8081
|
await Promise.allSettled(this.clientModuleTasks);
|
|
7677
8082
|
await Promise.allSettled(this.clientModuleRevocations);
|
|
8083
|
+
await Promise.allSettled(this.mobileGraphTasks);
|
|
7678
8084
|
this.activeRequests.clear();
|
|
7679
8085
|
this.activeWebSockets.clear();
|
|
7680
8086
|
this.connectedSockets.clear();
|
|
@@ -11754,12 +12160,16 @@ const MIN_ORIGIN_LISTEN_PORT = 1024;
|
|
|
11754
12160
|
const RESERVED_LAN_GATEWAY_PORT$1 = 3443;
|
|
11755
12161
|
/** DSH's own WebServer port; the plugin cannot function while it is taken. */
|
|
11756
12162
|
const RESERVED_DSH_WEB_PORT = 3080;
|
|
11757
|
-
const
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
12163
|
+
const TRUSTED_NETWORKS = [
|
|
12164
|
+
...[
|
|
12165
|
+
"10.0.0.0/8",
|
|
12166
|
+
"172.16.0.0/12",
|
|
12167
|
+
"192.168.0.0/16",
|
|
12168
|
+
"fc00::/7"
|
|
12169
|
+
].map(parseCidr),
|
|
12170
|
+
parseCidr("127.0.0.0/8"),
|
|
12171
|
+
parseCidr("::1/128")
|
|
12172
|
+
];
|
|
11763
12173
|
/** Require a public HTTPS origin; custom external ports are supported. */
|
|
11764
12174
|
function validateOriginPublicOrigin(value) {
|
|
11765
12175
|
if (typeof value !== "string" || value.length > 512 || /[\s\u0000-\u001f\u007f\\@?#]/u.test(value)) throw new Error("origin_public_origin_invalid");
|
|
@@ -11781,10 +12191,15 @@ function validateOriginPublicOrigin(value) {
|
|
|
11781
12191
|
if (url.protocol !== "https:" || url.pathname !== "/" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.port !== "" && Number(url.port) < 1 || (isIP(host) === 4 ? !isGloballyRoutableIpv4(host) : isIP(host) !== 0 || !validHostname)) throw new Error("origin_public_origin_invalid");
|
|
11782
12192
|
return url.origin;
|
|
11783
12193
|
}
|
|
11784
|
-
/** Bind
|
|
12194
|
+
/** Bind one explicit loopback, RFC1918 IPv4 or IPv6 ULA interface; zone IDs are unsupported. */
|
|
11785
12195
|
function validateOriginListenHost(value) {
|
|
11786
|
-
|
|
11787
|
-
|
|
12196
|
+
const addressFamily = isIP(value);
|
|
12197
|
+
if (typeof value !== "string" || value.includes("%") || addressFamily === 0 || !addressAllowed(value, TRUSTED_NETWORKS.filter((range) => range.bits === (addressFamily === 6 ? 128 : 32)))) throw new Error("origin_listen_host_invalid");
|
|
12198
|
+
return addressFamily === 6 ? new URL("http://[" + value + "]").hostname.slice(1, -1) : value;
|
|
12199
|
+
}
|
|
12200
|
+
/** Format a validated backend listener, including the brackets required by IPv6 URLs. */
|
|
12201
|
+
function formatOriginBackendOrigin(listenHost, listenPort) {
|
|
12202
|
+
return "http://" + (isIP(listenHost) === 6 ? "[" + listenHost + "]" : listenHost) + ":" + String(listenPort);
|
|
11788
12203
|
}
|
|
11789
12204
|
function validateOriginListenPort(value) {
|
|
11790
12205
|
if (!Number.isSafeInteger(value) || Number(value) < MIN_ORIGIN_LISTEN_PORT || Number(value) > 65535) throw new Error("origin_listen_port_invalid");
|
|
@@ -11793,14 +12208,15 @@ function validateOriginListenPort(value) {
|
|
|
11793
12208
|
}
|
|
11794
12209
|
/** Authorize direct proxy socket peers, not untrusted forwarded client headers. */
|
|
11795
12210
|
function validateOriginAllowedCidrs(value, listenHost) {
|
|
11796
|
-
const
|
|
12211
|
+
const addressFamily = isIP(listenHost);
|
|
12212
|
+
const input = value === void 0 && isLoopbackAddress(listenHost) ? [addressFamily === 6 ? "::1/128" : "127.0.0.0/8"] : value;
|
|
11797
12213
|
if (!Array.isArray(input) || input.length === 0 || input.length > 16) throw new Error("origin_allowed_cidrs_invalid");
|
|
11798
12214
|
const cidrs = [];
|
|
11799
12215
|
for (const entry of input) {
|
|
11800
|
-
if (typeof entry !== "string" || entry.length > 64 || isIP(entry.split("/")[0] ?? "") !==
|
|
12216
|
+
if (typeof entry !== "string" || entry.length > 64 || entry.includes("%") || isIP(entry.split("/")[0] ?? "") !== addressFamily) throw new Error("origin_allowed_cidrs_invalid");
|
|
11801
12217
|
try {
|
|
11802
12218
|
const cidr = parseCidr(entry);
|
|
11803
|
-
if (cidr.bits !== 32 || !TRUSTED_NETWORKS.some((range) => cidr.prefix >= range.prefix && addressAllowed(entry.split("/")[0], [range]))) throw new Error("untrusted CIDR");
|
|
12219
|
+
if (cidr.bits !== (addressFamily === 6 ? 128 : 32) || !TRUSTED_NETWORKS.some((range) => cidr.bits === range.bits && cidr.prefix >= range.prefix && addressAllowed(entry.split("/")[0], [range]))) throw new Error("untrusted CIDR");
|
|
11804
12220
|
cidrs.push(cidr.source);
|
|
11805
12221
|
} catch {
|
|
11806
12222
|
throw new Error("origin_allowed_cidrs_invalid");
|
|
@@ -11878,7 +12294,7 @@ var OriginConfigStore = class {
|
|
|
11878
12294
|
listenHost: settings.listenHost,
|
|
11879
12295
|
listenPort: settings.listenPort,
|
|
11880
12296
|
allowedCidrs: settings.allowedCidrs,
|
|
11881
|
-
backendOrigin:
|
|
12297
|
+
backendOrigin: formatOriginBackendOrigin(settings.listenHost, settings.listenPort)
|
|
11882
12298
|
},
|
|
11883
12299
|
storagePath: this.stateRoot,
|
|
11884
12300
|
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
@@ -12038,7 +12454,7 @@ var OriginController = class {
|
|
|
12038
12454
|
enabled: true,
|
|
12039
12455
|
state: "ready",
|
|
12040
12456
|
origin: settings.publicOrigin,
|
|
12041
|
-
backendOrigin:
|
|
12457
|
+
backendOrigin: formatOriginBackendOrigin(settings.listenHost, gateway.address().port)
|
|
12042
12458
|
});
|
|
12043
12459
|
}
|
|
12044
12460
|
async stop() {
|
|
@@ -17737,6 +18153,8 @@ async function apply(ctx, config) {
|
|
|
17737
18153
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17738
18154
|
}, (entry) => {
|
|
17739
18155
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18156
|
+
}, (entry) => {
|
|
18157
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17740
18158
|
});
|
|
17741
18159
|
await candidate.start();
|
|
17742
18160
|
lanGateway = candidate;
|
|
@@ -17797,6 +18215,8 @@ async function apply(ctx, config) {
|
|
|
17797
18215
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17798
18216
|
}, (entry) => {
|
|
17799
18217
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18218
|
+
}, (entry) => {
|
|
18219
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17800
18220
|
});
|
|
17801
18221
|
await candidate.start();
|
|
17802
18222
|
return candidate;
|
|
@@ -17817,6 +18237,8 @@ async function apply(ctx, config) {
|
|
|
17817
18237
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17818
18238
|
}, (entry) => {
|
|
17819
18239
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18240
|
+
}, (entry) => {
|
|
18241
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17820
18242
|
});
|
|
17821
18243
|
try {
|
|
17822
18244
|
await candidate.start();
|
|
@@ -17832,6 +18254,8 @@ async function apply(ctx, config) {
|
|
|
17832
18254
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17833
18255
|
}, (entry) => {
|
|
17834
18256
|
logger.warn("http failure %s", JSON.stringify(entry));
|
|
18257
|
+
}, (entry) => {
|
|
18258
|
+
logger.info("upstream auth %s", JSON.stringify(entry));
|
|
17835
18259
|
});
|
|
17836
18260
|
try {
|
|
17837
18261
|
await candidate.start();
|