dsh-mobile 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTORS.md +10 -5
- package/README.en.md +23 -9
- package/README.md +24 -10
- package/SECURITY.md +1 -1
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/docs/CLOUDFLARE_TUNNEL.en.md +3 -1
- package/docs/CLOUDFLARE_TUNNEL.md +3 -1
- package/docs/README.md +1 -0
- package/docs/SLOW_CONNECTIONS.en.md +77 -0
- package/docs/SLOW_CONNECTIONS.md +77 -0
- package/lib/client.js +52 -10
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +46 -5
- package/lib/index.mjs +662 -238
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +135 -2
- package/lib/mobile-layout.js.map +1 -1
- package/node_modules/dsh-mobile-question-fixes/package.json +1 -1
- package/package.json +11 -2
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import { createGzip, gzip } from "node:zlib";
|
|
17
17
|
import Bonjour from "bonjour-service";
|
|
18
18
|
import * as QRCode from "qrcode";
|
|
19
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
19
20
|
import { Logger, Service } from "@deepseek-ai/cordis";
|
|
20
21
|
import { connect as connect$1 } from "node:tls";
|
|
21
22
|
import { generate } from "selfsigned";
|
|
@@ -241,15 +242,10 @@ var AccessController = class {
|
|
|
241
242
|
/** Exchange a valid persistent device credential for a new short Session. */
|
|
242
243
|
async renew(deviceToken) {
|
|
243
244
|
this.requireInitialized();
|
|
244
|
-
if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
|
|
245
245
|
return this.exclusive(async () => {
|
|
246
246
|
const now = this.now();
|
|
247
|
-
const
|
|
248
|
-
const index = this.devices.
|
|
249
|
-
const device = this.devices[index];
|
|
250
|
-
if (device === void 0) throw new AccessError(401, "authentication_failed");
|
|
251
|
-
if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
|
|
252
|
-
if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
|
|
247
|
+
const device = this.requireDevice(deviceToken, now);
|
|
248
|
+
const index = this.devices.indexOf(device);
|
|
253
249
|
const updated = Object.freeze({
|
|
254
250
|
...device,
|
|
255
251
|
lastSeenAt: now
|
|
@@ -264,15 +260,10 @@ var AccessController = class {
|
|
|
264
260
|
/** Validate a persistent device credential without consuming a Session slot. */
|
|
265
261
|
async probe(deviceToken) {
|
|
266
262
|
this.requireInitialized();
|
|
267
|
-
if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
|
|
268
263
|
return this.exclusive(async () => {
|
|
269
264
|
const now = this.now();
|
|
270
|
-
const
|
|
271
|
-
const index = this.devices.
|
|
272
|
-
const device = this.devices[index];
|
|
273
|
-
if (device === void 0) throw new AccessError(401, "authentication_failed");
|
|
274
|
-
if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
|
|
275
|
-
if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
|
|
265
|
+
const device = this.requireDevice(deviceToken, now);
|
|
266
|
+
const index = this.devices.indexOf(device);
|
|
276
267
|
const updated = Object.freeze({
|
|
277
268
|
...device,
|
|
278
269
|
lastSeenAt: now
|
|
@@ -287,6 +278,24 @@ var AccessController = class {
|
|
|
287
278
|
});
|
|
288
279
|
});
|
|
289
280
|
}
|
|
281
|
+
/** Validate a device credential without writing state or opening a Session. */
|
|
282
|
+
authorizeDevice(deviceToken) {
|
|
283
|
+
this.requireInitialized();
|
|
284
|
+
const device = this.requireDevice(deviceToken, this.now());
|
|
285
|
+
return Object.freeze({
|
|
286
|
+
deviceId: device.id,
|
|
287
|
+
deviceExpiresAt: device.expiresAt
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
requireDevice(deviceToken, now) {
|
|
291
|
+
if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
|
|
292
|
+
const tokenDigest = digest(deviceToken);
|
|
293
|
+
const device = this.devices.find((candidate) => timingSafeEqual(Buffer.from(candidate.tokenDigest, "hex"), tokenDigest));
|
|
294
|
+
if (device === void 0) throw new AccessError(401, "authentication_failed");
|
|
295
|
+
if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
|
|
296
|
+
if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
|
|
297
|
+
return device;
|
|
298
|
+
}
|
|
290
299
|
/** Resolve a short Session Cookie without revealing whether device or Session failed. */
|
|
291
300
|
authorizeSession(sessionToken) {
|
|
292
301
|
this.requireInitialized();
|
|
@@ -745,6 +754,112 @@ async function probeTcpReachable(host, port, timeoutMs = 1500) {
|
|
|
745
754
|
});
|
|
746
755
|
}
|
|
747
756
|
//#endregion
|
|
757
|
+
//#region src/websocket-paths.ts
|
|
758
|
+
/** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
|
|
759
|
+
const MAX_EXTRA_WEBSOCKET_PATHS = 16;
|
|
760
|
+
const MAX_WEBSOCKET_PATH_LENGTH = 256;
|
|
761
|
+
function fail(code) {
|
|
762
|
+
throw new Error(code);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Validate one exact pathname for proxying. Query strings are matched at
|
|
766
|
+
* upgrade time, so only the pathname is stored. Rejects anything the
|
|
767
|
+
* gateway cannot match exactly.
|
|
768
|
+
*/
|
|
769
|
+
function validateWebSocketPath(value) {
|
|
770
|
+
if (typeof value !== "string") fail("websocket_path_invalid");
|
|
771
|
+
const path = value;
|
|
772
|
+
if (path.length === 0 || path.length > 256) fail("websocket_path_invalid");
|
|
773
|
+
if (!path.startsWith("/")) fail("websocket_path_invalid");
|
|
774
|
+
if (/[\s\u0000-\u001f\u007f#?]/u.test(path)) fail("websocket_path_invalid");
|
|
775
|
+
if (path.includes("..")) fail("websocket_path_invalid");
|
|
776
|
+
return path;
|
|
777
|
+
}
|
|
778
|
+
/** Validate a whole replacement list all-or-nothing; duplicates collapse. */
|
|
779
|
+
function normalizeWebSocketPaths(value) {
|
|
780
|
+
if (!Array.isArray(value)) fail("websocket_paths_invalid");
|
|
781
|
+
if (value.length > 16) fail("websocket_paths_invalid");
|
|
782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const entry of value) {
|
|
784
|
+
const path = validateWebSocketPath(entry);
|
|
785
|
+
seen.add(path);
|
|
786
|
+
}
|
|
787
|
+
return [...seen];
|
|
788
|
+
}
|
|
789
|
+
/** Cap remembered rejected upgrade paths offered for one-click approval. */
|
|
790
|
+
const MAX_BLOCKED_UPGRADE_PATHS = 32;
|
|
791
|
+
/**
|
|
792
|
+
* In-memory log of rejected third-party upgrade paths, shared by every
|
|
793
|
+
* gateway instance so the approval UI sees attempts on any listener.
|
|
794
|
+
* Bounded and newest-first; a restart clears it (attempts reappear on
|
|
795
|
+
* the next blocked handshake).
|
|
796
|
+
*/
|
|
797
|
+
var BlockedUpgradePathLog = class {
|
|
798
|
+
entries = /* @__PURE__ */ new Map();
|
|
799
|
+
record(pathname) {
|
|
800
|
+
const now = Date.now();
|
|
801
|
+
const stats = this.entries.get(pathname);
|
|
802
|
+
if (stats === void 0) {
|
|
803
|
+
if (this.entries.size >= 32) {
|
|
804
|
+
let oldest;
|
|
805
|
+
for (const [path, entry] of this.entries) if (oldest === void 0 || entry.lastSeen < (this.entries.get(oldest)?.lastSeen ?? 0)) oldest = path;
|
|
806
|
+
if (oldest !== void 0) this.entries.delete(oldest);
|
|
807
|
+
}
|
|
808
|
+
this.entries.set(pathname, {
|
|
809
|
+
attempts: 1,
|
|
810
|
+
firstSeen: now,
|
|
811
|
+
lastSeen: now
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
stats.attempts += 1;
|
|
816
|
+
stats.lastSeen = now;
|
|
817
|
+
}
|
|
818
|
+
report() {
|
|
819
|
+
return [...this.entries].map(([path, stats]) => ({
|
|
820
|
+
path,
|
|
821
|
+
...stats
|
|
822
|
+
})).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
/** File-backed store shared by every gateway instance (LAN and remote). */
|
|
826
|
+
var WebSocketPathStore = class {
|
|
827
|
+
file;
|
|
828
|
+
paths = /* @__PURE__ */ new Set();
|
|
829
|
+
loaded = false;
|
|
830
|
+
constructor(file) {
|
|
831
|
+
this.file = file;
|
|
832
|
+
}
|
|
833
|
+
/** Snapshot for the upgrade check. */
|
|
834
|
+
has(pathname) {
|
|
835
|
+
return this.paths.has(pathname);
|
|
836
|
+
}
|
|
837
|
+
list() {
|
|
838
|
+
return [...this.paths];
|
|
839
|
+
}
|
|
840
|
+
async load() {
|
|
841
|
+
if (!this.loaded) {
|
|
842
|
+
try {
|
|
843
|
+
const raw = JSON.parse(await readFile(this.file, "utf8"));
|
|
844
|
+
this.paths = new Set(normalizeWebSocketPaths(raw.paths ?? []));
|
|
845
|
+
} catch {
|
|
846
|
+
this.paths = /* @__PURE__ */ new Set();
|
|
847
|
+
}
|
|
848
|
+
this.loaded = true;
|
|
849
|
+
}
|
|
850
|
+
return this.list();
|
|
851
|
+
}
|
|
852
|
+
/** Replace the whole list after validating; persists atomically. */
|
|
853
|
+
async replace(paths) {
|
|
854
|
+
const next = normalizeWebSocketPaths([...paths]);
|
|
855
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
856
|
+
await writeFile(this.file, `${JSON.stringify({ paths: next })}\n`, "utf8");
|
|
857
|
+
this.paths = new Set(next);
|
|
858
|
+
this.loaded = true;
|
|
859
|
+
return this.list();
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
//#endregion
|
|
748
863
|
//#region src/config.ts
|
|
749
864
|
/** Loader-facing defaults; {@link parseGatewayConfig} enforces cross-field security rules. */
|
|
750
865
|
const Config = z.object({
|
|
@@ -762,6 +877,14 @@ const Config = z.object({
|
|
|
762
877
|
mobileLayoutFile: z.string().hidden(),
|
|
763
878
|
mobileCompatibilityFile: z.string().hidden(),
|
|
764
879
|
excludedClientModules: z.array(String).default([]),
|
|
880
|
+
websocketCompression: z.object({
|
|
881
|
+
paths: z.array(String).default([]),
|
|
882
|
+
maxMessageBytes: z.natural(),
|
|
883
|
+
maxQueuedBytes: z.natural(),
|
|
884
|
+
thresholdBytes: z.natural(),
|
|
885
|
+
concurrencyLimit: z.natural(),
|
|
886
|
+
level: z.natural().max(9)
|
|
887
|
+
}),
|
|
765
888
|
instanceId: z.string().hidden(),
|
|
766
889
|
pairingCaFile: z.string().hidden(),
|
|
767
890
|
initiallyEnabled: z.boolean().hidden().required(),
|
|
@@ -790,6 +913,21 @@ function integer(value, name, fallback, minimum, maximum) {
|
|
|
790
913
|
if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`${name} must be an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
791
914
|
return resolved;
|
|
792
915
|
}
|
|
916
|
+
function parseWebSocketCompression(raw) {
|
|
917
|
+
if (raw !== void 0 && (typeof raw !== "object" || raw === null || Array.isArray(raw))) throw new Error("websocketCompression must be an object");
|
|
918
|
+
const value = raw ?? {};
|
|
919
|
+
const maxMessageBytes = integer(value.maxMessageBytes, "websocketCompression.maxMessageBytes", 33554432, 1024, 268435456);
|
|
920
|
+
const maxQueuedBytes = integer(value.maxQueuedBytes, "websocketCompression.maxQueuedBytes", 67108864, 1024, 268435456);
|
|
921
|
+
if (maxQueuedBytes < maxMessageBytes + 14) throw new Error("websocketCompression.maxQueuedBytes must cover maxMessageBytes and the frame header");
|
|
922
|
+
return Object.freeze({
|
|
923
|
+
paths: Object.freeze(normalizeWebSocketPaths(value.paths ?? [])),
|
|
924
|
+
maxMessageBytes,
|
|
925
|
+
maxQueuedBytes,
|
|
926
|
+
thresholdBytes: integer(value.thresholdBytes, "websocketCompression.thresholdBytes", 1024, 0, maxMessageBytes),
|
|
927
|
+
concurrencyLimit: integer(value.concurrencyLimit, "websocketCompression.concurrencyLimit", 4, 1, 32),
|
|
928
|
+
level: integer(value.level, "websocketCompression.level", 3, 0, 9)
|
|
929
|
+
});
|
|
930
|
+
}
|
|
793
931
|
function stringArray(value, name) {
|
|
794
932
|
if (!Array.isArray(value) || value.length === 0 || value.some((entry) => typeof entry !== "string")) throw new Error(`${name} must be a non-empty string array`);
|
|
795
933
|
return value;
|
|
@@ -896,6 +1034,7 @@ function parseGatewayConfig(raw) {
|
|
|
896
1034
|
mobileLayoutFile: value.mobileLayoutFile === void 0 ? fileURLToPath(new URL("./mobile-layout.js", import.meta.url)) : absoluteFile(value.mobileLayoutFile, "mobileLayoutFile"),
|
|
897
1035
|
mobileCompatibilityFile: value.mobileCompatibilityFile === void 0 ? fileURLToPath(new URL("./mobile-compat.js", import.meta.url)) : absoluteFile(value.mobileCompatibilityFile, "mobileCompatibilityFile"),
|
|
898
1036
|
excludedClientModules: excludedClientModules(value.excludedClientModules),
|
|
1037
|
+
websocketCompression: parseWebSocketCompression(value.websocketCompression),
|
|
899
1038
|
instanceId: value.instanceId === void 0 ? createHash("sha256").update(absoluteFile(value.stateFile, "stateFile")).digest("hex") : /^[a-f\d]{64}$/u.test(value.instanceId) ? value.instanceId : (() => {
|
|
900
1039
|
throw new Error("instanceId must be a lowercase SHA-256 value");
|
|
901
1040
|
})(),
|
|
@@ -1402,21 +1541,6 @@ async function readJsonObject(request, maximumBytes) {
|
|
|
1402
1541
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new HttpError(400, "bad_request");
|
|
1403
1542
|
return parsed;
|
|
1404
1543
|
}
|
|
1405
|
-
/** Strict cookie parser: malformed or duplicate names invalidate the whole header. */
|
|
1406
|
-
function parseCookies(header) {
|
|
1407
|
-
if (header === void 0) return /* @__PURE__ */ new Map();
|
|
1408
|
-
if (header.length > 8192) return void 0;
|
|
1409
|
-
const cookies = /* @__PURE__ */ new Map();
|
|
1410
|
-
for (const part of header.split(";")) {
|
|
1411
|
-
const equals = part.indexOf("=");
|
|
1412
|
-
if (equals <= 0) return void 0;
|
|
1413
|
-
const name = part.slice(0, equals).trim();
|
|
1414
|
-
const value = part.slice(equals + 1).trim();
|
|
1415
|
-
if (!/^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/u.test(name) || !/^[\w\-.~+/=]*$/u.test(value) || cookies.has(name)) return;
|
|
1416
|
-
cookies.set(name, value);
|
|
1417
|
-
}
|
|
1418
|
-
return cookies;
|
|
1419
|
-
}
|
|
1420
1544
|
/** Serialize a host-only Cookie with no Domain attribute. */
|
|
1421
1545
|
function cookie(name, value, options) {
|
|
1422
1546
|
const parts = [
|
|
@@ -1516,6 +1640,280 @@ function ensureMobileCompatibility(html) {
|
|
|
1516
1640
|
return `${html.slice(0, first.index)}${script}${html.slice(first.index)}`;
|
|
1517
1641
|
}
|
|
1518
1642
|
//#endregion
|
|
1643
|
+
//#region src/browser-auth-cookies.ts
|
|
1644
|
+
const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
|
|
1645
|
+
const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
|
|
1646
|
+
/** Non-secret browser identity; the persistent device credential keeps its narrow renewal path. */
|
|
1647
|
+
const HOST_DEVICE_COOKIE = "__Host-dsh_ma_device_id";
|
|
1648
|
+
const AUTH_COOKIE_NAMES = /* @__PURE__ */ new Set([
|
|
1649
|
+
SESSION_COOKIE,
|
|
1650
|
+
DEVICE_COOKIE,
|
|
1651
|
+
CSRF_COOKIE,
|
|
1652
|
+
HOST_SESSION_COOKIE,
|
|
1653
|
+
HOST_CSRF_COOKIE,
|
|
1654
|
+
HOST_DEVICE_COOKIE
|
|
1655
|
+
]);
|
|
1656
|
+
/** Read only gateway authentication cookies, retaining distinct values from different scopes. */
|
|
1657
|
+
function browserAuthCookies(header) {
|
|
1658
|
+
if (header === void 0) return /* @__PURE__ */ new Map();
|
|
1659
|
+
if (header.length > 8192) throw new HttpError(401, "authentication_failed");
|
|
1660
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
1661
|
+
for (const part of header.split(";")) {
|
|
1662
|
+
const equals = part.indexOf("=");
|
|
1663
|
+
if (equals <= 0) continue;
|
|
1664
|
+
const name = part.slice(0, equals).trim();
|
|
1665
|
+
if (!AUTH_COOKIE_NAMES.has(name)) continue;
|
|
1666
|
+
const values = cookies.get(name) ?? [];
|
|
1667
|
+
cookies.set(name, values);
|
|
1668
|
+
const value = part.slice(equals + 1).trim();
|
|
1669
|
+
if (!/^[\w\-.~+/=]{1,512}$/u.test(value) || values.includes(value)) continue;
|
|
1670
|
+
values.push(value);
|
|
1671
|
+
}
|
|
1672
|
+
return cookies;
|
|
1673
|
+
}
|
|
1674
|
+
/** Select exactly one authenticated value; never let cookie order choose between valid identities. */
|
|
1675
|
+
function authenticatedCookie(values, authenticate, accepts = () => true) {
|
|
1676
|
+
let selected;
|
|
1677
|
+
for (const value of values) {
|
|
1678
|
+
let result;
|
|
1679
|
+
try {
|
|
1680
|
+
result = authenticate(value);
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
if (error instanceof AccessError && error.status === 401) continue;
|
|
1683
|
+
throw error;
|
|
1684
|
+
}
|
|
1685
|
+
if (!accepts(result)) continue;
|
|
1686
|
+
if (selected !== void 0) throw new HttpError(401, "authentication_failed");
|
|
1687
|
+
selected = {
|
|
1688
|
+
value,
|
|
1689
|
+
result
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
if (selected === void 0) throw new HttpError(401, "authentication_failed");
|
|
1693
|
+
return selected;
|
|
1694
|
+
}
|
|
1695
|
+
//#endregion
|
|
1696
|
+
//#region src/websocket-compression.ts
|
|
1697
|
+
function payloadBytes(data) {
|
|
1698
|
+
if (Array.isArray(data)) return data.reduce((bytes, part) => bytes + part.byteLength, 0);
|
|
1699
|
+
return data.byteLength;
|
|
1700
|
+
}
|
|
1701
|
+
function forwardableClose(code) {
|
|
1702
|
+
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Bridge an authorized HTTP upgrade, negotiating compression only with the client.
|
|
1706
|
+
* Errors after acceptance close both legs. Socket destruction by the gateway also
|
|
1707
|
+
* releases the ws wrappers, pending compression, and handshake/close timers.
|
|
1708
|
+
* @param request - validated browser upgrade request.
|
|
1709
|
+
* @param client - gateway-owned client socket, paused during authorization.
|
|
1710
|
+
* @param upstream - gateway-owned loopback socket, connecting or connected.
|
|
1711
|
+
* @param head - bytes the HTTP parser read after the client's upgrade headers.
|
|
1712
|
+
* @param target - validated upstream URL, including the original query.
|
|
1713
|
+
* @param upstreamCookie - gateway's signed upstream cookie; browser cookies are discarded.
|
|
1714
|
+
* @param options - resolved message, queue, and compression limits.
|
|
1715
|
+
* @param handshakeTimeoutMs - maximum handshake and close-handshake duration.
|
|
1716
|
+
* @returns fulfillment after both HTTP upgrades are accepted, or rejection before acceptance.
|
|
1717
|
+
*/
|
|
1718
|
+
function bridgeCompressedWebSocket(request, client, upstream, head, target, upstreamCookie, options, handshakeTimeoutMs) {
|
|
1719
|
+
return new Promise((resolve, reject) => {
|
|
1720
|
+
let incoming;
|
|
1721
|
+
let outgoing;
|
|
1722
|
+
let server;
|
|
1723
|
+
let accepted = false;
|
|
1724
|
+
let stopped = false;
|
|
1725
|
+
let closing = false;
|
|
1726
|
+
let closeTimer;
|
|
1727
|
+
const handshakeTimer = setTimeout(() => {
|
|
1728
|
+
stop(/* @__PURE__ */ new Error("WebSocket bridge handshake timed out"));
|
|
1729
|
+
}, handshakeTimeoutMs);
|
|
1730
|
+
handshakeTimer.unref();
|
|
1731
|
+
const stop = (error) => {
|
|
1732
|
+
if (stopped) return;
|
|
1733
|
+
stopped = true;
|
|
1734
|
+
clearTimeout(handshakeTimer);
|
|
1735
|
+
clearTimeout(closeTimer);
|
|
1736
|
+
if (!accepted) reject(error);
|
|
1737
|
+
incoming?.terminate();
|
|
1738
|
+
outgoing?.terminate();
|
|
1739
|
+
client.destroy();
|
|
1740
|
+
upstream.destroy();
|
|
1741
|
+
server?.close();
|
|
1742
|
+
};
|
|
1743
|
+
const finish = () => {
|
|
1744
|
+
if (incoming?.readyState === WebSocket.CLOSED && outgoing?.readyState === WebSocket.CLOSED) stop(/* @__PURE__ */ new Error("WebSocket bridge closed"));
|
|
1745
|
+
};
|
|
1746
|
+
const beginClose = (code, reason) => {
|
|
1747
|
+
if (!accepted) {
|
|
1748
|
+
stop(new Error(reason));
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (stopped || closing) return;
|
|
1752
|
+
closing = true;
|
|
1753
|
+
incoming?.resume();
|
|
1754
|
+
outgoing?.resume();
|
|
1755
|
+
for (const socket of [incoming, outgoing]) if (socket?.readyState === WebSocket.OPEN) socket.close(code, reason);
|
|
1756
|
+
closeTimer = setTimeout(() => {
|
|
1757
|
+
stop(/* @__PURE__ */ new Error("WebSocket bridge close timed out"));
|
|
1758
|
+
}, handshakeTimeoutMs);
|
|
1759
|
+
closeTimer.unref();
|
|
1760
|
+
};
|
|
1761
|
+
const socketError = (error) => {
|
|
1762
|
+
stop(error);
|
|
1763
|
+
};
|
|
1764
|
+
const socketClosed = () => {
|
|
1765
|
+
if (!accepted) stop(/* @__PURE__ */ new Error("WebSocket socket closed before acceptance"));
|
|
1766
|
+
};
|
|
1767
|
+
client.on("error", socketError);
|
|
1768
|
+
upstream.on("error", socketError);
|
|
1769
|
+
client.once("close", socketClosed);
|
|
1770
|
+
upstream.once("close", socketClosed);
|
|
1771
|
+
const wsError = (error) => {
|
|
1772
|
+
if (!accepted) {
|
|
1773
|
+
stop(error);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
beginClose(error.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? 1009 : 1011, error.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? "Message too large" : "WebSocket bridge failed");
|
|
1777
|
+
};
|
|
1778
|
+
const attachRelay = (source, destination) => {
|
|
1779
|
+
let queuedBytes = 0;
|
|
1780
|
+
const send = (bytes, operation) => {
|
|
1781
|
+
if (stopped || closing) return;
|
|
1782
|
+
if (destination.readyState !== WebSocket.OPEN) {
|
|
1783
|
+
beginClose(1011, "WebSocket peer unavailable");
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const queued = bytes + 14;
|
|
1787
|
+
if (queued > options.maxQueuedBytes - queuedBytes) {
|
|
1788
|
+
beginClose(1009, "WebSocket queue limit");
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
queuedBytes += queued;
|
|
1792
|
+
source.pause();
|
|
1793
|
+
const complete = (error) => {
|
|
1794
|
+
queuedBytes -= queued;
|
|
1795
|
+
if (error != null) {
|
|
1796
|
+
wsError(error);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (!stopped && !closing && queuedBytes === 0 && source.readyState === WebSocket.OPEN) source.resume();
|
|
1800
|
+
};
|
|
1801
|
+
try {
|
|
1802
|
+
operation(complete);
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
complete(error instanceof Error ? error : /* @__PURE__ */ new Error("WebSocket forwarding failed"));
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
source.on("message", (data, binary) => {
|
|
1808
|
+
const bytes = payloadBytes(data);
|
|
1809
|
+
if (bytes > options.maxMessageBytes) {
|
|
1810
|
+
beginClose(1009, "Message too large");
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
send(bytes, (callback) => {
|
|
1814
|
+
destination.send(data, {
|
|
1815
|
+
binary,
|
|
1816
|
+
compress: destination === incoming
|
|
1817
|
+
}, callback);
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
source.on("ping", (data) => {
|
|
1821
|
+
send(data.byteLength, (callback) => {
|
|
1822
|
+
destination.ping(data, void 0, callback);
|
|
1823
|
+
});
|
|
1824
|
+
});
|
|
1825
|
+
source.on("pong", (data) => {
|
|
1826
|
+
send(data.byteLength, (callback) => {
|
|
1827
|
+
destination.pong(data, void 0, callback);
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
source.on("close", (code, reason) => {
|
|
1831
|
+
if (stopped) return;
|
|
1832
|
+
if (code === 1006 || !forwardableClose(code) && code !== 1005) {
|
|
1833
|
+
stop(/* @__PURE__ */ new Error("WebSocket peer closed without a valid close frame"));
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
closing = true;
|
|
1837
|
+
destination.resume();
|
|
1838
|
+
if (destination.readyState === WebSocket.OPEN) {
|
|
1839
|
+
if (code === 1005) destination.close();
|
|
1840
|
+
else destination.close(code, reason);
|
|
1841
|
+
}
|
|
1842
|
+
closeTimer ??= setTimeout(() => {
|
|
1843
|
+
stop(/* @__PURE__ */ new Error("WebSocket bridge close timed out"));
|
|
1844
|
+
}, handshakeTimeoutMs);
|
|
1845
|
+
closeTimer.unref();
|
|
1846
|
+
finish();
|
|
1847
|
+
});
|
|
1848
|
+
};
|
|
1849
|
+
try {
|
|
1850
|
+
if (client.destroyed || upstream.destroyed) throw new Error("WebSocket socket is already closed");
|
|
1851
|
+
const protocolHeader = request.headers["sec-websocket-protocol"];
|
|
1852
|
+
const protocols = typeof protocolHeader === "string" ? protocolHeader.split(",").map((protocol) => protocol.trim()) : [];
|
|
1853
|
+
const url = new URL(target);
|
|
1854
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1855
|
+
outgoing = new WebSocket(url, protocols, {
|
|
1856
|
+
createConnection: () => upstream,
|
|
1857
|
+
handshakeTimeout: handshakeTimeoutMs,
|
|
1858
|
+
perMessageDeflate: false,
|
|
1859
|
+
autoPong: false,
|
|
1860
|
+
followRedirects: false,
|
|
1861
|
+
maxPayload: options.maxMessageBytes,
|
|
1862
|
+
headers: {
|
|
1863
|
+
host: target.host,
|
|
1864
|
+
origin: target.origin,
|
|
1865
|
+
"sec-fetch-site": "same-origin",
|
|
1866
|
+
...upstreamCookie === void 0 ? {} : { cookie: upstreamCookie }
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
outgoing.on("error", wsError);
|
|
1870
|
+
outgoing.once("close", () => {
|
|
1871
|
+
if (!accepted) stop(/* @__PURE__ */ new Error("Upstream WebSocket closed before acceptance"));
|
|
1872
|
+
else finish();
|
|
1873
|
+
});
|
|
1874
|
+
outgoing.once("open", () => {
|
|
1875
|
+
if (stopped || client.destroyed) {
|
|
1876
|
+
stop(/* @__PURE__ */ new Error("Client closed before WebSocket acceptance"));
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
upstream.setTimeout(0);
|
|
1880
|
+
server = new WebSocketServer({
|
|
1881
|
+
noServer: true,
|
|
1882
|
+
clientTracking: false,
|
|
1883
|
+
autoPong: false,
|
|
1884
|
+
maxPayload: options.maxMessageBytes,
|
|
1885
|
+
handleProtocols: () => outgoing?.protocol || false,
|
|
1886
|
+
perMessageDeflate: {
|
|
1887
|
+
clientNoContextTakeover: true,
|
|
1888
|
+
serverNoContextTakeover: true,
|
|
1889
|
+
threshold: options.thresholdBytes,
|
|
1890
|
+
concurrencyLimit: options.concurrencyLimit,
|
|
1891
|
+
zlibDeflateOptions: { level: options.level }
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
server.on("error", wsError);
|
|
1895
|
+
try {
|
|
1896
|
+
server.handleUpgrade(request, client, head, (socket) => {
|
|
1897
|
+
incoming = socket;
|
|
1898
|
+
incoming.on("error", wsError);
|
|
1899
|
+
incoming.once("close", finish);
|
|
1900
|
+
attachRelay(incoming, outgoing);
|
|
1901
|
+
attachRelay(outgoing, incoming);
|
|
1902
|
+
accepted = true;
|
|
1903
|
+
clearTimeout(handshakeTimer);
|
|
1904
|
+
incoming.resume();
|
|
1905
|
+
resolve();
|
|
1906
|
+
});
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
stop(error instanceof Error ? error : /* @__PURE__ */ new Error("Client WebSocket upgrade failed"));
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
stop(error instanceof Error ? error : /* @__PURE__ */ new Error("WebSocket bridge setup failed"));
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
//#endregion
|
|
1519
1917
|
//#region src/version.ts
|
|
1520
1918
|
const manifest = createRequire(import.meta.url)("../package.json");
|
|
1521
1919
|
/** Version of the installed DSH Mobile plugin package. */
|
|
@@ -2469,6 +2867,7 @@ function renderPairPage(locale) {
|
|
|
2469
2867
|
<html lang="${copy.lang}">
|
|
2470
2868
|
<meta charset="utf-8">
|
|
2471
2869
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
2870
|
+
<style>input,textarea{font-size:16px}</style>
|
|
2472
2871
|
<title>${copy.pairTitle}</title>
|
|
2473
2872
|
<main>
|
|
2474
2873
|
<h1>${copy.pairHeading}</h1>
|
|
@@ -2577,8 +2976,6 @@ function renderLoginScript(locale) {
|
|
|
2577
2976
|
//#region src/gateway.ts
|
|
2578
2977
|
const MAX_CONTROL_BODY_BYTES = 16384;
|
|
2579
2978
|
const MAX_HEADER_BYTES = 16384;
|
|
2580
|
-
const MOBILE_HISTORY_PAGE_MESSAGES = 10;
|
|
2581
|
-
const SESSION_HISTORY_PATH = "/api/session.history";
|
|
2582
2979
|
const DISCOVERY_QUERY = Buffer.from("DSH_MOBILE_DISCOVER_V1", "ascii");
|
|
2583
2980
|
const DISCOVERY_PROTOCOL = 1;
|
|
2584
2981
|
const DISCOVERY_INTERVAL_MS = 3e3;
|
|
@@ -2664,7 +3061,9 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
|
2664
3061
|
"@deepseek-ai/dsh-client-ui-theme"
|
|
2665
3062
|
])
|
|
2666
3063
|
})]);
|
|
2667
|
-
|
|
3064
|
+
function mobileCsrfFetchBootstrap(cookieName) {
|
|
3065
|
+
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})};})();`;
|
|
3066
|
+
}
|
|
2668
3067
|
const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true},window.__DSH_FILE_UPLOAD__={fetch:(input,init)=>window.fetch(input,init)}})();`;
|
|
2669
3068
|
/** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
|
|
2670
3069
|
const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
|
|
@@ -2976,7 +3375,7 @@ function rewriteMobileIndexWithBatch(html, options = {}) {
|
|
|
2976
3375
|
batches: plan.batches
|
|
2977
3376
|
})).digest("hex").slice(0, 16);
|
|
2978
3377
|
}
|
|
2979
|
-
const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${
|
|
3378
|
+
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)};`;
|
|
2980
3379
|
const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
|
|
2981
3380
|
return Object.freeze({
|
|
2982
3381
|
html: ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, preloadReplacements))),
|
|
@@ -3177,7 +3576,7 @@ function isCompressibleContentType(value) {
|
|
|
3177
3576
|
}
|
|
3178
3577
|
function shouldCompressResponse(request, response) {
|
|
3179
3578
|
const pathname = request.url?.split("?", 1)[0] ?? "";
|
|
3180
|
-
return
|
|
3579
|
+
return request.method === "GET" && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) && response.statusCode === 200 && request.headers.range === void 0 && response.headers["content-range"] === void 0 && response.headers["content-encoding"] === void 0 && acceptsGzip(request.headers["accept-encoding"]) && isCompressibleContentType(response.headers["content-type"]);
|
|
3181
3580
|
}
|
|
3182
3581
|
function revisionedStaticCacheControl(request, statusCode) {
|
|
3183
3582
|
if (request.method !== "GET" && request.method !== "HEAD") return void 0;
|
|
@@ -3194,39 +3593,12 @@ function revisionedStaticCacheControl(request, statusCode) {
|
|
|
3194
3593
|
if (!(target.pathname.startsWith("/plugins/") && hasRevision) && !(target.pathname.startsWith("/assets/") && (hasRevision || hashedAsset))) return void 0;
|
|
3195
3594
|
return "private, max-age=31536000, immutable";
|
|
3196
3595
|
}
|
|
3197
|
-
function isJsonRecord(value) {
|
|
3198
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3199
|
-
}
|
|
3200
|
-
function mobileHistoryRequestBody(request, body) {
|
|
3201
|
-
if (request.method !== "POST" || request.url?.split("?", 1)[0] !== SESSION_HISTORY_PATH) return body;
|
|
3202
|
-
let parsed;
|
|
3203
|
-
try {
|
|
3204
|
-
parsed = JSON.parse(body.toString("utf8"));
|
|
3205
|
-
} catch {
|
|
3206
|
-
return body;
|
|
3207
|
-
}
|
|
3208
|
-
if (!isJsonRecord(parsed) || parsed.method !== "session.history" || !isJsonRecord(parsed.payload)) return body;
|
|
3209
|
-
const requested = parsed.payload.maxMessages;
|
|
3210
|
-
if (typeof requested === "number" && Number.isInteger(requested) && requested > 0 && requested <= MOBILE_HISTORY_PAGE_MESSAGES) return body;
|
|
3211
|
-
return Buffer.from(JSON.stringify({
|
|
3212
|
-
...parsed,
|
|
3213
|
-
payload: {
|
|
3214
|
-
...parsed.payload,
|
|
3215
|
-
maxMessages: MOBILE_HISTORY_PAGE_MESSAGES
|
|
3216
|
-
}
|
|
3217
|
-
}));
|
|
3218
|
-
}
|
|
3219
3596
|
function addVaryAcceptEncoding(headers) {
|
|
3220
3597
|
const existing = headers.vary;
|
|
3221
3598
|
const values = (Array.isArray(existing) ? existing.map((value) => String(value)) : existing === void 0 ? [] : [String(existing)]).flatMap((value) => value.split(",").map((part) => part.trim()).filter(Boolean));
|
|
3222
3599
|
if (!values.some((value) => value.toLowerCase() === "accept-encoding")) values.push("Accept-Encoding");
|
|
3223
3600
|
headers.vary = values.join(", ");
|
|
3224
3601
|
}
|
|
3225
|
-
function requestCookies(request) {
|
|
3226
|
-
const cookies = parseCookies(request.headers.cookie);
|
|
3227
|
-
if (cookies === void 0) throw new HttpError(401, "authentication_failed");
|
|
3228
|
-
return cookies;
|
|
3229
|
-
}
|
|
3230
3602
|
function mapError(error) {
|
|
3231
3603
|
if (error instanceof HttpError) return error;
|
|
3232
3604
|
if (error instanceof AccessError) return new HttpError(error.status, error.code);
|
|
@@ -3755,9 +4127,9 @@ var MobileAccessGateway = class {
|
|
|
3755
4127
|
return this.policy;
|
|
3756
4128
|
}
|
|
3757
4129
|
authorize(request) {
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
return this.access.authorizeSession(
|
|
4130
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
4131
|
+
const name = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_SESSION_COOKIE : SESSION_COOKIE;
|
|
4132
|
+
return authenticatedCookie(cookies.get(name) ?? [], (token) => this.access.authorizeSession(token)).result;
|
|
3761
4133
|
}
|
|
3762
4134
|
requireCsrf(request, authorization) {
|
|
3763
4135
|
const value = headerValue(request.headers, CSRF_HEADER);
|
|
@@ -3765,16 +4137,40 @@ var MobileAccessGateway = class {
|
|
|
3765
4137
|
}
|
|
3766
4138
|
setSessionCookies(response, result, now) {
|
|
3767
4139
|
const maxAge = (result.sessionExpiresAt - now) / 1e3;
|
|
3768
|
-
response.setHeader("Set-Cookie", [
|
|
3769
|
-
|
|
4140
|
+
response.setHeader("Set-Cookie", [
|
|
4141
|
+
cookie(SESSION_COOKIE, result.sessionToken, {
|
|
4142
|
+
tls: this.tlsEnabled,
|
|
4143
|
+
httpOnly: true,
|
|
4144
|
+
path: "/",
|
|
4145
|
+
maxAgeSeconds: maxAge
|
|
4146
|
+
}),
|
|
4147
|
+
cookie(CSRF_COOKIE, result.csrfToken, {
|
|
4148
|
+
tls: this.tlsEnabled,
|
|
4149
|
+
httpOnly: false,
|
|
4150
|
+
path: "/",
|
|
4151
|
+
maxAgeSeconds: maxAge
|
|
4152
|
+
}),
|
|
4153
|
+
...this.tlsEnabled ? [cookie(HOST_SESSION_COOKIE, result.sessionToken, {
|
|
4154
|
+
tls: true,
|
|
4155
|
+
httpOnly: true,
|
|
4156
|
+
path: "/",
|
|
4157
|
+
maxAgeSeconds: maxAge
|
|
4158
|
+
}), cookie(HOST_CSRF_COOKIE, result.csrfToken, {
|
|
4159
|
+
tls: true,
|
|
4160
|
+
httpOnly: false,
|
|
4161
|
+
path: "/",
|
|
4162
|
+
maxAgeSeconds: maxAge
|
|
4163
|
+
})] : []
|
|
4164
|
+
]);
|
|
4165
|
+
}
|
|
4166
|
+
setBrowserDeviceBinding(response, deviceId, deviceExpiresAt) {
|
|
4167
|
+
if (!this.tlsEnabled) return;
|
|
4168
|
+
const existing = response.getHeader("Set-Cookie");
|
|
4169
|
+
response.setHeader("Set-Cookie", [...existing, cookie(HOST_DEVICE_COOKIE, deviceId, {
|
|
4170
|
+
tls: true,
|
|
3770
4171
|
httpOnly: true,
|
|
3771
4172
|
path: "/",
|
|
3772
|
-
maxAgeSeconds:
|
|
3773
|
-
}), cookie(CSRF_COOKIE, result.csrfToken, {
|
|
3774
|
-
tls: this.tlsEnabled,
|
|
3775
|
-
httpOnly: false,
|
|
3776
|
-
path: "/",
|
|
3777
|
-
maxAgeSeconds: maxAge
|
|
4173
|
+
maxAgeSeconds: (deviceExpiresAt - Date.now()) / 1e3
|
|
3778
4174
|
})]);
|
|
3779
4175
|
}
|
|
3780
4176
|
async handlePair(request, response) {
|
|
@@ -3790,6 +4186,7 @@ var MobileAccessGateway = class {
|
|
|
3790
4186
|
path: "/mobile-access/auth/renew",
|
|
3791
4187
|
maxAgeSeconds: (result.deviceExpiresAt - now) / 1e3
|
|
3792
4188
|
})]);
|
|
4189
|
+
this.setBrowserDeviceBinding(response, result.deviceId, result.deviceExpiresAt);
|
|
3793
4190
|
sendJson(response, 201, {
|
|
3794
4191
|
paired: true,
|
|
3795
4192
|
deviceId: result.deviceId,
|
|
@@ -3800,21 +4197,13 @@ var MobileAccessGateway = class {
|
|
|
3800
4197
|
async handleRenew(request, response) {
|
|
3801
4198
|
if (!this.renewLimiter.take(request.socket.remoteAddress ?? "unknown", Date.now())) throw new HttpError(429, "rate_limited");
|
|
3802
4199
|
await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
3803
|
-
const
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
} catch (error) {
|
|
3809
|
-
if (error instanceof AccessError && error.status === 401) response.setHeader("Set-Cookie", cookie(DEVICE_COOKIE, "", {
|
|
3810
|
-
tls: this.tlsEnabled,
|
|
3811
|
-
httpOnly: true,
|
|
3812
|
-
path: "/mobile-access/auth/renew",
|
|
3813
|
-
maxAgeSeconds: 0
|
|
3814
|
-
}));
|
|
3815
|
-
throw error;
|
|
3816
|
-
}
|
|
4200
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
4201
|
+
const bindings = this.tlsEnabled ? cookies.get(HOST_DEVICE_COOKIE) : void 0;
|
|
4202
|
+
if (bindings !== void 0 && bindings.length !== 1) throw new HttpError(401, "authentication_failed");
|
|
4203
|
+
const device = authenticatedCookie(cookies.get("dsh_ma_device") ?? [], (token) => this.access.authorizeDevice(token), (candidate) => bindings === void 0 || candidate.deviceId === bindings[0]);
|
|
4204
|
+
const result = await this.access.renew(device.value);
|
|
3817
4205
|
this.setSessionCookies(response, result, Date.now());
|
|
4206
|
+
this.setBrowserDeviceBinding(response, result.deviceId, device.result.deviceExpiresAt);
|
|
3818
4207
|
sendJson(response, 200, {
|
|
3819
4208
|
renewed: true,
|
|
3820
4209
|
deviceId: result.deviceId,
|
|
@@ -3865,17 +4254,31 @@ var MobileAccessGateway = class {
|
|
|
3865
4254
|
const authorization = this.authorize(request);
|
|
3866
4255
|
this.requireCsrf(request, authorization);
|
|
3867
4256
|
this.access.logout(authorization);
|
|
3868
|
-
response.setHeader("Set-Cookie", [
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
4257
|
+
response.setHeader("Set-Cookie", [
|
|
4258
|
+
cookie(SESSION_COOKIE, "", {
|
|
4259
|
+
tls: this.tlsEnabled,
|
|
4260
|
+
httpOnly: true,
|
|
4261
|
+
path: "/",
|
|
4262
|
+
maxAgeSeconds: 0
|
|
4263
|
+
}),
|
|
4264
|
+
cookie(CSRF_COOKIE, "", {
|
|
4265
|
+
tls: this.tlsEnabled,
|
|
4266
|
+
httpOnly: false,
|
|
4267
|
+
path: "/",
|
|
4268
|
+
maxAgeSeconds: 0
|
|
4269
|
+
}),
|
|
4270
|
+
...this.tlsEnabled ? [cookie(HOST_SESSION_COOKIE, "", {
|
|
4271
|
+
tls: true,
|
|
4272
|
+
httpOnly: true,
|
|
4273
|
+
path: "/",
|
|
4274
|
+
maxAgeSeconds: 0
|
|
4275
|
+
}), cookie(HOST_CSRF_COOKIE, "", {
|
|
4276
|
+
tls: true,
|
|
4277
|
+
httpOnly: false,
|
|
4278
|
+
path: "/",
|
|
4279
|
+
maxAgeSeconds: 0
|
|
4280
|
+
})] : []
|
|
4281
|
+
]);
|
|
3879
4282
|
sendJson(response, 200, { loggedOut: true }, this.tlsEnabled);
|
|
3880
4283
|
}
|
|
3881
4284
|
async handleExternalRequest(request, response) {
|
|
@@ -3994,7 +4397,14 @@ var MobileAccessGateway = class {
|
|
|
3994
4397
|
fallback: void 0
|
|
3995
4398
|
} : void 0 : void 0;
|
|
3996
4399
|
if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
3997
|
-
if (
|
|
4400
|
+
if (![
|
|
4401
|
+
"GET",
|
|
4402
|
+
"HEAD",
|
|
4403
|
+
"POST",
|
|
4404
|
+
"PUT",
|
|
4405
|
+
"PATCH",
|
|
4406
|
+
"DELETE"
|
|
4407
|
+
].includes(request.method ?? "") && requestedExtension?.kind !== "route") throw new HttpError(405, "method_not_allowed");
|
|
3998
4408
|
let authorization;
|
|
3999
4409
|
try {
|
|
4000
4410
|
authorization = this.authorize(request);
|
|
@@ -4400,9 +4810,15 @@ var MobileAccessGateway = class {
|
|
|
4400
4810
|
try {
|
|
4401
4811
|
const html = Buffer.concat(chunks).toString("utf8");
|
|
4402
4812
|
const plan = parseMobileBootPlan(html, this.config.excludedClientModules);
|
|
4403
|
-
const
|
|
4813
|
+
const options = plan.rewrittenBatchEntries === void 0 ? { excludedClientModules: this.config.excludedClientModules } : {
|
|
4404
4814
|
...await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries)),
|
|
4405
4815
|
excludedClientModules: this.config.excludedClientModules
|
|
4816
|
+
};
|
|
4817
|
+
const cookies = browserAuthCookies(request$2.headers.cookie);
|
|
4818
|
+
const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
|
|
4819
|
+
const rewritten = rewriteMobileIndexWithBatch(html, {
|
|
4820
|
+
...options,
|
|
4821
|
+
csrfCookie
|
|
4406
4822
|
});
|
|
4407
4823
|
for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
|
|
4408
4824
|
body = Buffer.from(rewritten.html);
|
|
@@ -4771,11 +5187,9 @@ var MobileAccessGateway = class {
|
|
|
4771
5187
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
4772
5188
|
let bodyDone;
|
|
4773
5189
|
try {
|
|
4774
|
-
const bufferedBody = request$3.method === "POST" && request$3.url?.split("?", 1)[0] === SESSION_HISTORY_PATH ? mobileHistoryRequestBody(request$3, await readBoundedBody(request$3, this.config.maxBodyBytes)) : void 0;
|
|
4775
5190
|
const upstreamHeaders = sanitizeRequestHeaders(request$3, this.config.upstreamOrigin);
|
|
4776
5191
|
const upstreamCookie = await this.upstreamCookieHeader();
|
|
4777
5192
|
if (upstreamCookie !== void 0) upstreamHeaders.cookie = upstreamCookie;
|
|
4778
|
-
if (bufferedBody !== void 0) upstreamHeaders["content-length"] = String(bufferedBody.byteLength);
|
|
4779
5193
|
const proxied = await new Promise((resolve, reject) => {
|
|
4780
5194
|
const upstreamRequest = request({
|
|
4781
5195
|
protocol: "http:",
|
|
@@ -4792,11 +5206,7 @@ var MobileAccessGateway = class {
|
|
|
4792
5206
|
});
|
|
4793
5207
|
upstreamRequest.once("response", resolve);
|
|
4794
5208
|
upstreamRequest.once("error", reject);
|
|
4795
|
-
|
|
4796
|
-
else {
|
|
4797
|
-
upstreamRequest.end(bufferedBody);
|
|
4798
|
-
bodyDone = Promise.resolve();
|
|
4799
|
-
}
|
|
5209
|
+
bodyDone = pipeline(request$3, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
|
|
4800
5210
|
bodyDone.catch(reject);
|
|
4801
5211
|
});
|
|
4802
5212
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
@@ -5019,10 +5429,13 @@ var MobileAccessGateway = class {
|
|
|
5019
5429
|
throw new HttpError(400, "bad_request");
|
|
5020
5430
|
}
|
|
5021
5431
|
if (decodedKey.length !== 16 || decodedKey.toString("base64") !== key) throw new HttpError(400, "bad_request");
|
|
5022
|
-
|
|
5432
|
+
let authorization = this.authorize(request);
|
|
5023
5433
|
if (this.activeWebSockets.size >= this.config.maxWebSockets) throw new HttpError(429, "busy");
|
|
5024
5434
|
const upstreamCookie = await this.upstreamCookieHeader();
|
|
5025
5435
|
if (client.destroyed) return;
|
|
5436
|
+
authorization = this.authorize(request);
|
|
5437
|
+
if (this.activeWebSockets.size >= this.config.maxWebSockets) throw new HttpError(429, "busy");
|
|
5438
|
+
const compressed = this.config.websocketCompression.paths.includes(target.decodedPathname);
|
|
5026
5439
|
const upstream = connect({
|
|
5027
5440
|
host: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
5028
5441
|
port: Number(this.config.upstreamOrigin.port)
|
|
@@ -5045,20 +5458,28 @@ var MobileAccessGateway = class {
|
|
|
5045
5458
|
});
|
|
5046
5459
|
this.activeWebSockets.set(id, record);
|
|
5047
5460
|
const cleanup = () => {
|
|
5461
|
+
if (compressed && (!client.destroyed || !upstream.destroyed)) return;
|
|
5048
5462
|
const active = this.activeWebSockets.get(id);
|
|
5049
5463
|
if (active !== void 0) clearTimeout(active.timer);
|
|
5050
5464
|
this.activeWebSockets.delete(id);
|
|
5051
5465
|
};
|
|
5052
5466
|
client.once("close", () => {
|
|
5053
|
-
upstream.destroy();
|
|
5467
|
+
if (!compressed) upstream.destroy();
|
|
5054
5468
|
cleanup();
|
|
5055
5469
|
});
|
|
5056
5470
|
upstream.once("close", () => {
|
|
5057
|
-
client.destroy();
|
|
5471
|
+
if (!compressed) client.destroy();
|
|
5058
5472
|
cleanup();
|
|
5059
5473
|
});
|
|
5060
5474
|
upstream.setTimeout(this.config.upstreamTimeoutMs, closeBoth);
|
|
5061
5475
|
try {
|
|
5476
|
+
if (compressed) {
|
|
5477
|
+
const compressionTarget = new URL(this.config.upstreamOrigin.href);
|
|
5478
|
+
compressionTarget.pathname = target.pathname;
|
|
5479
|
+
compressionTarget.search = target.search;
|
|
5480
|
+
await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
|
|
5481
|
+
return;
|
|
5482
|
+
}
|
|
5062
5483
|
await new Promise((resolve, reject) => {
|
|
5063
5484
|
const connected = () => {
|
|
5064
5485
|
upstream.off("error", failed);
|
|
@@ -5357,8 +5778,8 @@ var MemoryDeviceStore = class {
|
|
|
5357
5778
|
//#endregion
|
|
5358
5779
|
//#region src/frp-component.ts
|
|
5359
5780
|
const FRP_VERSION$1 = "0.70.1";
|
|
5360
|
-
const MAX_ARCHIVE_ENTRIES = 128;
|
|
5361
|
-
const MAX_ARCHIVE_LIST_BYTES = 262144;
|
|
5781
|
+
const MAX_ARCHIVE_ENTRIES$1 = 128;
|
|
5782
|
+
const MAX_ARCHIVE_LIST_BYTES$1 = 262144;
|
|
5362
5783
|
/** Pinned official FRP release metadata for supported desktop targets. */
|
|
5363
5784
|
const FRP_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
5364
5785
|
{
|
|
@@ -5463,12 +5884,12 @@ async function replaceDirectory(target, candidate) {
|
|
|
5463
5884
|
function sha256$2(bytes) {
|
|
5464
5885
|
return createHash("sha256").update(bytes).digest("hex");
|
|
5465
5886
|
}
|
|
5466
|
-
async function runCapture(file, args) {
|
|
5887
|
+
async function runCapture$1(file, args) {
|
|
5467
5888
|
return new Promise((resolveRun, reject) => {
|
|
5468
5889
|
execFile(file, [...args], {
|
|
5469
5890
|
windowsHide: true,
|
|
5470
5891
|
timeout: 12e4,
|
|
5471
|
-
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
5892
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES$1,
|
|
5472
5893
|
encoding: "utf8"
|
|
5473
5894
|
}, (error, stdout) => {
|
|
5474
5895
|
if (error === null) resolveRun(stdout);
|
|
@@ -5476,7 +5897,7 @@ async function runCapture(file, args) {
|
|
|
5476
5897
|
});
|
|
5477
5898
|
});
|
|
5478
5899
|
}
|
|
5479
|
-
function validatedArchiveEntry(rawEntry) {
|
|
5900
|
+
function validatedArchiveEntry$1(rawEntry) {
|
|
5480
5901
|
if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("frp_archive_path_invalid");
|
|
5481
5902
|
const segments = rawEntry.replace(/\/$/u, "").split("/");
|
|
5482
5903
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("frp_archive_path_invalid");
|
|
@@ -5484,10 +5905,10 @@ function validatedArchiveEntry(rawEntry) {
|
|
|
5484
5905
|
}
|
|
5485
5906
|
/** Select exactly one nested frpc executable from a safe archive listing. */
|
|
5486
5907
|
function selectFrpExecutableEntry(entries, executableName) {
|
|
5487
|
-
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("frp_archive_entries_invalid");
|
|
5908
|
+
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES$1) throw new Error("frp_archive_entries_invalid");
|
|
5488
5909
|
let executableEntry;
|
|
5489
5910
|
for (const entry of entries) {
|
|
5490
|
-
const segments = validatedArchiveEntry(entry);
|
|
5911
|
+
const segments = validatedArchiveEntry$1(entry);
|
|
5491
5912
|
if (segments.length >= 2 && segments.at(-1) === executableName) {
|
|
5492
5913
|
if (executableEntry !== void 0) throw new Error("frp_archive_executable_ambiguous");
|
|
5493
5914
|
executableEntry = entry.replace(/\/$/u, "");
|
|
@@ -5496,22 +5917,22 @@ function selectFrpExecutableEntry(entries, executableName) {
|
|
|
5496
5917
|
if (executableEntry === void 0) throw new Error("frp_archive_executable_missing");
|
|
5497
5918
|
return executableEntry;
|
|
5498
5919
|
}
|
|
5499
|
-
async function defaultExtractArtifact$
|
|
5920
|
+
async function defaultExtractArtifact$2(archive, destination, executableName) {
|
|
5500
5921
|
const tar = process.platform === "win32" ? "tar.exe" : "tar";
|
|
5501
|
-
const executableEntry = selectFrpExecutableEntry((await runCapture(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
|
|
5922
|
+
const executableEntry = selectFrpExecutableEntry((await runCapture$1(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
|
|
5502
5923
|
const unpacked = join(destination, "archive");
|
|
5503
5924
|
await mkdir(unpacked, {
|
|
5504
5925
|
recursive: true,
|
|
5505
5926
|
mode: 448
|
|
5506
5927
|
});
|
|
5507
|
-
await runCapture(tar, [
|
|
5928
|
+
await runCapture$1(tar, [
|
|
5508
5929
|
"-xf",
|
|
5509
5930
|
archive,
|
|
5510
5931
|
"-C",
|
|
5511
5932
|
unpacked,
|
|
5512
5933
|
executableEntry
|
|
5513
5934
|
]);
|
|
5514
|
-
const extracted = join(unpacked, ...validatedArchiveEntry(executableEntry));
|
|
5935
|
+
const extracted = join(unpacked, ...validatedArchiveEntry$1(executableEntry));
|
|
5515
5936
|
if (!await regularFile$2(extracted)) throw new Error("frp_archive_executable_invalid");
|
|
5516
5937
|
await copyFile(extracted, join(destination, executableName));
|
|
5517
5938
|
}
|
|
@@ -5551,7 +5972,7 @@ async function defaultFetchArtifact$2(artifact, signal) {
|
|
|
5551
5972
|
return bytes;
|
|
5552
5973
|
}
|
|
5553
5974
|
async function defaultInspectExecutable(executable) {
|
|
5554
|
-
return (await runCapture(executable, ["--version"])).trim();
|
|
5975
|
+
return (await runCapture$1(executable, ["--version"])).trim();
|
|
5555
5976
|
}
|
|
5556
5977
|
/** Owns the optional official frpc binary inside the DSH Mobile state directory. */
|
|
5557
5978
|
var FrpComponentManager = class {
|
|
@@ -5586,7 +6007,7 @@ var FrpComponentManager = class {
|
|
|
5586
6007
|
this.stagingRoot
|
|
5587
6008
|
]) if (!inside$2(stateDirectory, child)) throw new Error("frp component path escaped its state directory");
|
|
5588
6009
|
this.fetchArtifact = options.fetchArtifact ?? defaultFetchArtifact$2;
|
|
5589
|
-
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$
|
|
6010
|
+
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$2;
|
|
5590
6011
|
this.inspectExecutable = options.inspectExecutable ?? defaultInspectExecutable;
|
|
5591
6012
|
}
|
|
5592
6013
|
/** Inspect the managed executable without relying on global FRP installations. */
|
|
@@ -5698,112 +6119,6 @@ var FrpComponentManager = class {
|
|
|
5698
6119
|
}
|
|
5699
6120
|
};
|
|
5700
6121
|
//#endregion
|
|
5701
|
-
//#region src/websocket-paths.ts
|
|
5702
|
-
/** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
|
|
5703
|
-
const MAX_EXTRA_WEBSOCKET_PATHS = 16;
|
|
5704
|
-
const MAX_WEBSOCKET_PATH_LENGTH = 256;
|
|
5705
|
-
function fail(code) {
|
|
5706
|
-
throw new Error(code);
|
|
5707
|
-
}
|
|
5708
|
-
/**
|
|
5709
|
-
* Validate one exact pathname for proxying. Query strings are matched at
|
|
5710
|
-
* upgrade time, so only the pathname is stored. Rejects anything the
|
|
5711
|
-
* gateway cannot match exactly.
|
|
5712
|
-
*/
|
|
5713
|
-
function validateWebSocketPath(value) {
|
|
5714
|
-
if (typeof value !== "string") fail("websocket_path_invalid");
|
|
5715
|
-
const path = value;
|
|
5716
|
-
if (path.length === 0 || path.length > 256) fail("websocket_path_invalid");
|
|
5717
|
-
if (!path.startsWith("/")) fail("websocket_path_invalid");
|
|
5718
|
-
if (/[\s\u0000-\u001f\u007f#?]/u.test(path)) fail("websocket_path_invalid");
|
|
5719
|
-
if (path.includes("..")) fail("websocket_path_invalid");
|
|
5720
|
-
return path;
|
|
5721
|
-
}
|
|
5722
|
-
/** Validate a whole replacement list all-or-nothing; duplicates collapse. */
|
|
5723
|
-
function normalizeWebSocketPaths(value) {
|
|
5724
|
-
if (!Array.isArray(value)) fail("websocket_paths_invalid");
|
|
5725
|
-
if (value.length > 16) fail("websocket_paths_invalid");
|
|
5726
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5727
|
-
for (const entry of value) {
|
|
5728
|
-
const path = validateWebSocketPath(entry);
|
|
5729
|
-
seen.add(path);
|
|
5730
|
-
}
|
|
5731
|
-
return [...seen];
|
|
5732
|
-
}
|
|
5733
|
-
/** Cap remembered rejected upgrade paths offered for one-click approval. */
|
|
5734
|
-
const MAX_BLOCKED_UPGRADE_PATHS = 32;
|
|
5735
|
-
/**
|
|
5736
|
-
* In-memory log of rejected third-party upgrade paths, shared by every
|
|
5737
|
-
* gateway instance so the approval UI sees attempts on any listener.
|
|
5738
|
-
* Bounded and newest-first; a restart clears it (attempts reappear on
|
|
5739
|
-
* the next blocked handshake).
|
|
5740
|
-
*/
|
|
5741
|
-
var BlockedUpgradePathLog = class {
|
|
5742
|
-
entries = /* @__PURE__ */ new Map();
|
|
5743
|
-
record(pathname) {
|
|
5744
|
-
const now = Date.now();
|
|
5745
|
-
const stats = this.entries.get(pathname);
|
|
5746
|
-
if (stats === void 0) {
|
|
5747
|
-
if (this.entries.size >= 32) {
|
|
5748
|
-
let oldest;
|
|
5749
|
-
for (const [path, entry] of this.entries) if (oldest === void 0 || entry.lastSeen < (this.entries.get(oldest)?.lastSeen ?? 0)) oldest = path;
|
|
5750
|
-
if (oldest !== void 0) this.entries.delete(oldest);
|
|
5751
|
-
}
|
|
5752
|
-
this.entries.set(pathname, {
|
|
5753
|
-
attempts: 1,
|
|
5754
|
-
firstSeen: now,
|
|
5755
|
-
lastSeen: now
|
|
5756
|
-
});
|
|
5757
|
-
return;
|
|
5758
|
-
}
|
|
5759
|
-
stats.attempts += 1;
|
|
5760
|
-
stats.lastSeen = now;
|
|
5761
|
-
}
|
|
5762
|
-
report() {
|
|
5763
|
-
return [...this.entries].map(([path, stats]) => ({
|
|
5764
|
-
path,
|
|
5765
|
-
...stats
|
|
5766
|
-
})).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
5767
|
-
}
|
|
5768
|
-
};
|
|
5769
|
-
/** File-backed store shared by every gateway instance (LAN and remote). */
|
|
5770
|
-
var WebSocketPathStore = class {
|
|
5771
|
-
file;
|
|
5772
|
-
paths = /* @__PURE__ */ new Set();
|
|
5773
|
-
loaded = false;
|
|
5774
|
-
constructor(file) {
|
|
5775
|
-
this.file = file;
|
|
5776
|
-
}
|
|
5777
|
-
/** Snapshot for the upgrade check. */
|
|
5778
|
-
has(pathname) {
|
|
5779
|
-
return this.paths.has(pathname);
|
|
5780
|
-
}
|
|
5781
|
-
list() {
|
|
5782
|
-
return [...this.paths];
|
|
5783
|
-
}
|
|
5784
|
-
async load() {
|
|
5785
|
-
if (!this.loaded) {
|
|
5786
|
-
try {
|
|
5787
|
-
const raw = JSON.parse(await readFile(this.file, "utf8"));
|
|
5788
|
-
this.paths = new Set(normalizeWebSocketPaths(raw.paths ?? []));
|
|
5789
|
-
} catch {
|
|
5790
|
-
this.paths = /* @__PURE__ */ new Set();
|
|
5791
|
-
}
|
|
5792
|
-
this.loaded = true;
|
|
5793
|
-
}
|
|
5794
|
-
return this.list();
|
|
5795
|
-
}
|
|
5796
|
-
/** Replace the whole list after validating; persists atomically. */
|
|
5797
|
-
async replace(paths) {
|
|
5798
|
-
const next = normalizeWebSocketPaths([...paths]);
|
|
5799
|
-
await mkdir(dirname(this.file), { recursive: true });
|
|
5800
|
-
await writeFile(this.file, `${JSON.stringify({ paths: next })}\n`, "utf8");
|
|
5801
|
-
this.paths = new Set(next);
|
|
5802
|
-
this.loaded = true;
|
|
5803
|
-
return this.list();
|
|
5804
|
-
}
|
|
5805
|
-
};
|
|
5806
|
-
//#endregion
|
|
5807
6122
|
//#region src/frp-template.ts
|
|
5808
6123
|
/** Loopback-only HTTP vhost port used between Caddy and frps. */
|
|
5809
6124
|
const FRP_VHOST_HTTP_PORT = 7080;
|
|
@@ -8595,6 +8910,10 @@ async function downloadPinnedArtifact(request) {
|
|
|
8595
8910
|
}
|
|
8596
8911
|
//#endregion
|
|
8597
8912
|
//#region src/cloudflared-component.ts
|
|
8913
|
+
/** Upper bound for `tar -t` output; a pinned release listing is a few lines. */
|
|
8914
|
+
const MAX_ARCHIVE_LIST_BYTES = 1048576;
|
|
8915
|
+
const MAX_ARCHIVE_ENTRIES = 256;
|
|
8916
|
+
const DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
8598
8917
|
const CLOUDFLARED_VERSION = "2026.9.1";
|
|
8599
8918
|
/** Pinned official cloudflared release metadata for supported desktop targets. */
|
|
8600
8919
|
const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
@@ -8605,7 +8924,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8605
8924
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
8606
8925
|
downloadBytes: 54976432,
|
|
8607
8926
|
downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
|
|
8608
|
-
executableName: "cloudflared.exe"
|
|
8927
|
+
executableName: "cloudflared.exe",
|
|
8928
|
+
executableBytes: 54976432,
|
|
8929
|
+
executableSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712"
|
|
8609
8930
|
},
|
|
8610
8931
|
{
|
|
8611
8932
|
version: CLOUDFLARED_VERSION,
|
|
@@ -8614,7 +8935,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8614
8935
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
8615
8936
|
downloadBytes: 39838488,
|
|
8616
8937
|
downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
|
|
8617
|
-
executableName: "cloudflared"
|
|
8938
|
+
executableName: "cloudflared",
|
|
8939
|
+
executableBytes: 39838488,
|
|
8940
|
+
executableSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc"
|
|
8618
8941
|
},
|
|
8619
8942
|
{
|
|
8620
8943
|
version: CLOUDFLARED_VERSION,
|
|
@@ -8623,7 +8946,39 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
|
8623
8946
|
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
|
|
8624
8947
|
downloadBytes: 37466252,
|
|
8625
8948
|
downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
|
|
8626
|
-
executableName: "cloudflared"
|
|
8949
|
+
executableName: "cloudflared",
|
|
8950
|
+
executableBytes: 37466252,
|
|
8951
|
+
executableSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3"
|
|
8952
|
+
},
|
|
8953
|
+
{
|
|
8954
|
+
version: CLOUDFLARED_VERSION,
|
|
8955
|
+
platform: "darwin",
|
|
8956
|
+
arch: "x64",
|
|
8957
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64.tgz`,
|
|
8958
|
+
downloadBytes: 21118723,
|
|
8959
|
+
downloadSha256: "ff0d3b51d5ff70eceef89d6b32145fee985018a2174596a5dbe405e2766e2ac4",
|
|
8960
|
+
executableName: "cloudflared",
|
|
8961
|
+
executableBytes: 41731488,
|
|
8962
|
+
executableSha256: "1ea07ae775b03236bd6be18ca1848d6bdc4af2f4f3bce398823b5a36e5761b75",
|
|
8963
|
+
archive: {
|
|
8964
|
+
format: "tgz",
|
|
8965
|
+
member: "cloudflared"
|
|
8966
|
+
}
|
|
8967
|
+
},
|
|
8968
|
+
{
|
|
8969
|
+
version: CLOUDFLARED_VERSION,
|
|
8970
|
+
platform: "darwin",
|
|
8971
|
+
arch: "arm64",
|
|
8972
|
+
downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-arm64.tgz`,
|
|
8973
|
+
downloadBytes: 19217478,
|
|
8974
|
+
downloadSha256: "c27ab8fd0aa489449e3d201eb02f957ef460a13b613662928b1b23394bf1bcfe",
|
|
8975
|
+
executableName: "cloudflared",
|
|
8976
|
+
executableBytes: 38893072,
|
|
8977
|
+
executableSha256: "9a0b19f67dc7a3011bc6b972c7ce06a5fcea8784ac6bd599ffa382ea4aeb5a6e",
|
|
8978
|
+
archive: {
|
|
8979
|
+
format: "tgz",
|
|
8980
|
+
member: "cloudflared"
|
|
8981
|
+
}
|
|
8627
8982
|
}
|
|
8628
8983
|
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
8629
8984
|
/**
|
|
@@ -8650,6 +9005,60 @@ async function regularFile$1(file, expectedBytes) {
|
|
|
8650
9005
|
throw error;
|
|
8651
9006
|
}
|
|
8652
9007
|
}
|
|
9008
|
+
async function runCapture(file, args) {
|
|
9009
|
+
return new Promise((resolveRun, reject) => {
|
|
9010
|
+
execFile(file, [...args], {
|
|
9011
|
+
windowsHide: true,
|
|
9012
|
+
timeout: 12e4,
|
|
9013
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
9014
|
+
encoding: "utf8"
|
|
9015
|
+
}, (error, stdout) => {
|
|
9016
|
+
if (error === null) resolveRun(stdout);
|
|
9017
|
+
else reject(error);
|
|
9018
|
+
});
|
|
9019
|
+
});
|
|
9020
|
+
}
|
|
9021
|
+
/** Reject absolute, traversing, or Windows-shaped archive members. */
|
|
9022
|
+
function validatedArchiveEntry(rawEntry) {
|
|
9023
|
+
if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("cloudflared_archive_path_invalid");
|
|
9024
|
+
const segments = rawEntry.replace(/\/$/u, "").split("/");
|
|
9025
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("cloudflared_archive_path_invalid");
|
|
9026
|
+
return segments;
|
|
9027
|
+
}
|
|
9028
|
+
/** Select exactly one archive member named `member`, wherever it sits in the tree. */
|
|
9029
|
+
function selectCloudflaredArchiveEntry(entries, member) {
|
|
9030
|
+
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("cloudflared_archive_entries_invalid");
|
|
9031
|
+
let selected;
|
|
9032
|
+
for (const entry of entries) if (validatedArchiveEntry(entry).at(-1) === member) {
|
|
9033
|
+
if (selected !== void 0) throw new Error("cloudflared_archive_executable_ambiguous");
|
|
9034
|
+
selected = entry.replace(/\/$/u, "");
|
|
9035
|
+
}
|
|
9036
|
+
if (selected === void 0) throw new Error("cloudflared_archive_executable_missing");
|
|
9037
|
+
return selected;
|
|
9038
|
+
}
|
|
9039
|
+
/**
|
|
9040
|
+
* Unpack the pinned member out of a `.tgz` using the system `tar`, which ships
|
|
9041
|
+
* with Windows 10+, macOS, and every supported Linux base.
|
|
9042
|
+
*/
|
|
9043
|
+
async function defaultExtractArtifact$1(archive, destination, member) {
|
|
9044
|
+
const tar = process.platform === "win32" ? "tar.exe" : "tar";
|
|
9045
|
+
const selected = selectCloudflaredArchiveEntry((await runCapture(tar, ["-tzf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), member);
|
|
9046
|
+
const unpacked = join(destination, "archive");
|
|
9047
|
+
await mkdir(unpacked, {
|
|
9048
|
+
recursive: true,
|
|
9049
|
+
mode: 448
|
|
9050
|
+
});
|
|
9051
|
+
await runCapture(tar, [
|
|
9052
|
+
"-xzf",
|
|
9053
|
+
archive,
|
|
9054
|
+
"-C",
|
|
9055
|
+
unpacked,
|
|
9056
|
+
selected
|
|
9057
|
+
]);
|
|
9058
|
+
const extracted = join(unpacked, ...validatedArchiveEntry(selected));
|
|
9059
|
+
if (!await regularFile$1(extracted)) throw new Error("cloudflared_archive_executable_invalid");
|
|
9060
|
+
await copyFile(extracted, join(destination, member));
|
|
9061
|
+
}
|
|
8653
9062
|
async function defaultFetchArtifact$1(url, signal, expectedBytes) {
|
|
8654
9063
|
return downloadPinnedArtifact({
|
|
8655
9064
|
url,
|
|
@@ -8674,6 +9083,7 @@ var CloudflaredComponentManager = class {
|
|
|
8674
9083
|
arch;
|
|
8675
9084
|
release;
|
|
8676
9085
|
fetchArtifact;
|
|
9086
|
+
extractArtifact;
|
|
8677
9087
|
installed = false;
|
|
8678
9088
|
errorCode;
|
|
8679
9089
|
queue = Promise.resolve();
|
|
@@ -8698,12 +9108,14 @@ var CloudflaredComponentManager = class {
|
|
|
8698
9108
|
]) if (!inside$1(stateDirectory, child)) throw new Error("cloudflared component path escaped its state directory");
|
|
8699
9109
|
const release = this.release;
|
|
8700
9110
|
this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact$1(url, signal, release?.downloadBytes ?? 0));
|
|
9111
|
+
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$1;
|
|
8701
9112
|
}
|
|
8702
9113
|
/** Inspect the managed binary without using any global cloudflared state. */
|
|
8703
9114
|
async initialize() {
|
|
8704
9115
|
const release = this.release;
|
|
8705
|
-
this.
|
|
8706
|
-
|
|
9116
|
+
this.errorCode = void 0;
|
|
9117
|
+
this.installed = release !== void 0 && await regularFile$1(this.executable, release.executableBytes);
|
|
9118
|
+
if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.executableSha256) {
|
|
8707
9119
|
this.installed = false;
|
|
8708
9120
|
this.errorCode = "cloudflared_component_invalid";
|
|
8709
9121
|
}
|
|
@@ -8716,7 +9128,7 @@ var CloudflaredComponentManager = class {
|
|
|
8716
9128
|
installed: this.installed,
|
|
8717
9129
|
version: release.version,
|
|
8718
9130
|
downloadBytes: release.downloadBytes,
|
|
8719
|
-
installedBytes: release.
|
|
9131
|
+
installedBytes: release.executableBytes,
|
|
8720
9132
|
sourceUrl: release.downloadUrl,
|
|
8721
9133
|
downloadPage: DOWNLOAD_PAGE$1,
|
|
8722
9134
|
termsUrl: TERMS_URL$1,
|
|
@@ -8738,7 +9150,7 @@ var CloudflaredComponentManager = class {
|
|
|
8738
9150
|
const controller = new AbortController();
|
|
8739
9151
|
const timeout = setTimeout(() => {
|
|
8740
9152
|
controller.abort();
|
|
8741
|
-
},
|
|
9153
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
8742
9154
|
timeout.unref();
|
|
8743
9155
|
let bytes;
|
|
8744
9156
|
try {
|
|
@@ -8749,12 +9161,24 @@ var CloudflaredComponentManager = class {
|
|
|
8749
9161
|
if (bytes.byteLength !== release.downloadBytes) throw new Error("cloudflared_download_size_mismatch");
|
|
8750
9162
|
if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cloudflared_download_hash_mismatch");
|
|
8751
9163
|
const staged = join(staging, release.executableName);
|
|
8752
|
-
await writeFile(staged, bytes, {
|
|
9164
|
+
if (release.archive === void 0) await writeFile(staged, bytes, {
|
|
8753
9165
|
flag: "wx",
|
|
8754
9166
|
mode: 384
|
|
8755
9167
|
});
|
|
9168
|
+
else {
|
|
9169
|
+
const downloaded = join(staging, `${release.executableName}.download`);
|
|
9170
|
+
await writeFile(downloaded, bytes, {
|
|
9171
|
+
flag: "wx",
|
|
9172
|
+
mode: 384
|
|
9173
|
+
});
|
|
9174
|
+
try {
|
|
9175
|
+
await this.extractArtifact(downloaded, staging, release.archive.member);
|
|
9176
|
+
} finally {
|
|
9177
|
+
await rm(downloaded, { force: true });
|
|
9178
|
+
}
|
|
9179
|
+
}
|
|
8756
9180
|
await chmod(staged, 448);
|
|
8757
|
-
if (!await regularFile$1(staged, release.
|
|
9181
|
+
if (!await regularFile$1(staged, release.executableBytes) || await sha256$1(staged) !== release.executableSha256) throw new Error("cloudflared_executable_hash_mismatch");
|
|
8758
9182
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
8759
9183
|
await mkdir(candidate, {
|
|
8760
9184
|
recursive: true,
|
|
@@ -13946,6 +14370,6 @@ async function apply(ctx, config) {
|
|
|
13946
14370
|
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
13947
14371
|
}
|
|
13948
14372
|
//#endregion
|
|
13949
|
-
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
14373
|
+
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
13950
14374
|
|
|
13951
14375
|
//# sourceMappingURL=index.mjs.map
|