dsh-mobile 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.en.md +8 -4
- package/README.md +14 -10
- package/assets/brand/app-icon-master.png +0 -0
- package/cordis.patch.yml +1 -1
- package/lib/client.js +436 -113
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -3
- package/lib/index.mjs +619 -32
- package/package.json +8 -5
package/lib/index.mjs
CHANGED
|
@@ -13,11 +13,12 @@ import { createServer as createServer$1, request } from "node:http";
|
|
|
13
13
|
import { createServer as createServer$2 } from "node:https";
|
|
14
14
|
import { Transform } from "node:stream";
|
|
15
15
|
import { pipeline } from "node:stream/promises";
|
|
16
|
-
import { createGzip } from "node:zlib";
|
|
16
|
+
import { createGzip, gzip } from "node:zlib";
|
|
17
17
|
import Bonjour from "bonjour-service";
|
|
18
18
|
import * as QRCode from "qrcode";
|
|
19
19
|
import { Service } from "@deepseek-ai/cordis";
|
|
20
20
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
21
|
+
import { lookup } from "node:dns/promises";
|
|
21
22
|
import { generate } from "selfsigned";
|
|
22
23
|
//#region src/access.ts
|
|
23
24
|
/** Stable error categories converted to deliberately terse HTTP responses. */
|
|
@@ -708,7 +709,8 @@ const SUPPORTED_DSH_VERSIONS = Object.freeze([
|
|
|
708
709
|
"0.1.0-rc.5",
|
|
709
710
|
"0.1.0-rc.6",
|
|
710
711
|
"0.1.0-rc.7",
|
|
711
|
-
"0.1.1-rc.2"
|
|
712
|
+
"0.1.1-rc.2",
|
|
713
|
+
"0.1.2-alpha.1"
|
|
712
714
|
]);
|
|
713
715
|
/**
|
|
714
716
|
* Reject an unverified DeepSeek Harness Host before opening the LAN listener.
|
|
@@ -720,10 +722,10 @@ function assertSupportedDshVersion(version) {
|
|
|
720
722
|
}
|
|
721
723
|
//#endregion
|
|
722
724
|
//#region src/private-file.ts
|
|
723
|
-
const execFile$
|
|
725
|
+
const execFile$3 = promisify(execFile);
|
|
724
726
|
let userSidTask;
|
|
725
727
|
async function currentWindowsUserSid() {
|
|
726
|
-
userSidTask ??= execFile$
|
|
728
|
+
userSidTask ??= execFile$3("whoami.exe", [
|
|
727
729
|
"/user",
|
|
728
730
|
"/fo",
|
|
729
731
|
"csv",
|
|
@@ -743,7 +745,7 @@ async function restrictPrivateFile(file, mode = 384) {
|
|
|
743
745
|
await chmod(file, mode);
|
|
744
746
|
if (process.platform !== "win32") return;
|
|
745
747
|
const userSid = await currentWindowsUserSid();
|
|
746
|
-
await execFile$
|
|
748
|
+
await execFile$3("icacls.exe", [
|
|
747
749
|
file,
|
|
748
750
|
"/inheritance:r",
|
|
749
751
|
"/grant:r",
|
|
@@ -985,7 +987,11 @@ const CSRF_COOKIE = "dsh_ma_csrf";
|
|
|
985
987
|
const CSRF_HEADER = "x-dsh-mobile-csrf";
|
|
986
988
|
const LOCAL_ADMIN_PREFIX = "/api/mobile-access";
|
|
987
989
|
const AUTH_PREFIX = "/mobile-access";
|
|
988
|
-
const WS_PATHS = /* @__PURE__ */ new Set([
|
|
990
|
+
const WS_PATHS = /* @__PURE__ */ new Set([
|
|
991
|
+
"/api/events.mux",
|
|
992
|
+
"/api/events.host",
|
|
993
|
+
"/api/remote.mux"
|
|
994
|
+
]);
|
|
989
995
|
/** Terse request failure safe to expose without internal diagnostics. */
|
|
990
996
|
var HttpError = class extends Error {
|
|
991
997
|
status;
|
|
@@ -1146,6 +1152,13 @@ function assertLocalAdminTrust(request, requireBrowserOrigin) {
|
|
|
1146
1152
|
if (requireBrowserOrigin && site !== void 0 && (origin === void 0 || site !== "same-origin")) throw new HttpError(403, "forbidden");
|
|
1147
1153
|
}
|
|
1148
1154
|
//#endregion
|
|
1155
|
+
//#region src/version.ts
|
|
1156
|
+
const manifest = createRequire(import.meta.url)("../package.json");
|
|
1157
|
+
/** Version of the installed DSH Mobile plugin package. */
|
|
1158
|
+
const DSH_MOBILE_VERSION = typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
1159
|
+
/** Oldest Android App release supported by this plugin generation. */
|
|
1160
|
+
const MINIMUM_ANDROID_APP_VERSION = "0.2.2";
|
|
1161
|
+
//#endregion
|
|
1149
1162
|
//#region src/computer-images.ts
|
|
1150
1163
|
const MAX_ENTRIES = 500;
|
|
1151
1164
|
const MAX_IMAGE_BYTES = 20971520;
|
|
@@ -1751,11 +1764,33 @@ const DISCOVERY_INTERVAL_MS = 3e3;
|
|
|
1751
1764
|
const MDNS_SERVICE_TYPE = "dsh-mobile";
|
|
1752
1765
|
const MOBILE_LAYOUT_MODULE = "@deepseek-ai/dsh-client-ui-layout";
|
|
1753
1766
|
const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
1767
|
+
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
1768
|
+
const MAX_MOBILE_BOOT_BATCH_BYTES = 33554432;
|
|
1769
|
+
const MAX_MOBILE_BOOT_ENTRY_BYTES = 8388608;
|
|
1770
|
+
const MAX_MOBILE_BOOT_BATCHES = 8;
|
|
1771
|
+
const UPSTREAM_AUTH_REFRESH_MARGIN_MS = 6e4;
|
|
1772
|
+
const UPSTREAM_COOKIE_PAIR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+=[\x21-\x3A\x3C-\x7E]*$/u;
|
|
1773
|
+
const CUSTOM_STYLE_FALLBACK = "/* Add mobile overrides in the DSH home mobile-access/mobile.css file. */\n";
|
|
1774
|
+
const CUSTOM_SCRIPT_FALLBACK = "window.dshMobile?.register(() => undefined)\n";
|
|
1754
1775
|
const MOBILE_CLIENT_MODULE = "dsh-mobile";
|
|
1755
1776
|
const CONNECTION_MODULE = "@deepseek-ai/dsh-client-connection";
|
|
1756
1777
|
const RUNTIME_MODULE = "@deepseek-ai/dsh-client-runtime";
|
|
1778
|
+
const RENDERER_MODULE = "@deepseek-ai/dsh-client-ui-renderer";
|
|
1779
|
+
const SIDEBAR_MODULE = "@deepseek-ai/dsh-client-ui-sidebar";
|
|
1757
1780
|
const SETTINGS_MODULE = "@deepseek-ai/dsh-client-ui-settings";
|
|
1758
|
-
const
|
|
1781
|
+
const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
1782
|
+
slots: RUNTIME_MODULE,
|
|
1783
|
+
dependencies: Object.freeze([RUNTIME_MODULE, "@deepseek-ai/dsh-client-ui-theme"])
|
|
1784
|
+
}), Object.freeze({
|
|
1785
|
+
slots: RENDERER_MODULE,
|
|
1786
|
+
dependencies: Object.freeze([
|
|
1787
|
+
"@deepseek-ai/dsh-client-locale",
|
|
1788
|
+
RENDERER_MODULE,
|
|
1789
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
1790
|
+
"@deepseek-ai/dsh-client-ui-theme"
|
|
1791
|
+
])
|
|
1792
|
+
})]);
|
|
1793
|
+
const MOBILE_CSRF_FETCH_BOOTSTRAP = `(()=>{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(`${CSRF_COOKIE}=`)};const token=document.cookie.split(';').map(value=>value.trim()).find(value=>value.startsWith(prefix))?.slice(prefix.length);if(token!==undefined)headers.set(${JSON.stringify(CSRF_HEADER)},token)}return nativeFetch(input,{...init,headers})};})();`;
|
|
1759
1794
|
const PAIR_PAGE = `<!doctype html>
|
|
1760
1795
|
<html lang="en">
|
|
1761
1796
|
<meta charset="utf-8">
|
|
@@ -1773,6 +1808,7 @@ const PAIR_PAGE = `<!doctype html>
|
|
|
1773
1808
|
<script src="/mobile-access/pair.js" defer><\/script>
|
|
1774
1809
|
</html>
|
|
1775
1810
|
`;
|
|
1811
|
+
const gzipBuffer = promisify(gzip);
|
|
1776
1812
|
function ensureMobileViewport(html) {
|
|
1777
1813
|
const match = /<meta\b(?=[^>]*\bname\s*=\s*["']viewport["'])[^>]*>/iu.exec(html);
|
|
1778
1814
|
if (match === null) {
|
|
@@ -1786,18 +1822,24 @@ function ensureMobileViewport(html) {
|
|
|
1786
1822
|
const next = content.test(match[0]) ? match[0].replace(content, (_whole, quote, value) => `content=${quote}${value},viewport-fit=cover${quote}`) : match[0].replace(/\s*\/?>$/u, " content=\"width=device-width,initial-scale=1,viewport-fit=cover\">");
|
|
1787
1823
|
return `${html.slice(0, match.index)}${next}${html.slice(match.index + match[0].length)}`;
|
|
1788
1824
|
}
|
|
1789
|
-
function orderAuthenticatedSettings(entries) {
|
|
1825
|
+
function orderAuthenticatedSettings(entries, slotsProvider) {
|
|
1790
1826
|
const mobile = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === MOBILE_CLIENT_MODULE);
|
|
1791
1827
|
const settings = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === SETTINGS_MODULE);
|
|
1792
1828
|
if (mobile.length === 0 || settings.length === 0) return;
|
|
1793
1829
|
if (mobile.length !== 1 || settings.length !== 1) throw new Error("upstream DSH mobile settings graph is ambiguous");
|
|
1794
|
-
if (!Array.isArray(mobile[0]?.inject) || !mobile[0].inject.includes(CONNECTION_MODULE) || !mobile[0].inject.includes(
|
|
1795
|
-
if (!Array.isArray(settings[0]?.inject) || !settings[0].inject.includes(CONNECTION_MODULE)
|
|
1796
|
-
mobile[0].inject = [CONNECTION_MODULE,
|
|
1830
|
+
if (!Array.isArray(mobile[0]?.inject) || !mobile[0].inject.includes(CONNECTION_MODULE) || !mobile[0].inject.includes(SIDEBAR_MODULE)) throw new Error("dsh-mobile client has unsupported dependencies");
|
|
1831
|
+
if (!Array.isArray(settings[0]?.inject) || !settings[0].inject.includes(CONNECTION_MODULE)) throw new Error("upstream DSH settings module has unsupported dependencies");
|
|
1832
|
+
mobile[0].inject = [CONNECTION_MODULE, slotsProvider];
|
|
1797
1833
|
if (!settings[0].inject.includes(MOBILE_CLIENT_MODULE)) settings[0].inject = [...settings[0].inject, MOBILE_CLIENT_MODULE];
|
|
1798
1834
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1835
|
+
function revisionedMobileBatchPath(entries) {
|
|
1836
|
+
const key = createHash("sha256").update(DSH_MOBILE_VERSION).update(JSON.stringify(entries)).digest("hex");
|
|
1837
|
+
return {
|
|
1838
|
+
key,
|
|
1839
|
+
path: `${MOBILE_BOOT_BATCH_PREFIX}${key}.js`
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function rewriteMobileIndexWithBatch(html) {
|
|
1801
1843
|
const assignment = /(?:window\.__DSH_BOOT__|globalThis\["__DSH_BOOT__"\])\s*=\s*/u.exec(html);
|
|
1802
1844
|
if (assignment?.index === void 0) throw new Error("upstream DSH index has no boot manifest");
|
|
1803
1845
|
const start = assignment.index;
|
|
@@ -1810,12 +1852,55 @@ function rewriteMobileIndex(html) {
|
|
|
1810
1852
|
const entries = parsed.entries;
|
|
1811
1853
|
const layout = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === MOBILE_LAYOUT_MODULE);
|
|
1812
1854
|
if (layout.length !== 1 || typeof layout[0]?.url !== "string" || typeof layout[0].rev !== "string") throw new Error("upstream DSH boot manifest has no unique layout module");
|
|
1813
|
-
if (!Array.isArray(layout[0].inject)
|
|
1855
|
+
if (!Array.isArray(layout[0].inject)) throw new Error("upstream DSH layout module has unsupported dependencies");
|
|
1856
|
+
const dependencyProfile = MOBILE_LAYOUT_DEPENDENCY_PROFILES.find((profile) => profile.dependencies.every((dependency) => layout[0]?.inject?.includes(dependency)));
|
|
1857
|
+
if (dependencyProfile === void 0) throw new Error("upstream DSH layout module has unsupported dependencies");
|
|
1814
1858
|
layout[0].url = MOBILE_LAYOUT_PATH;
|
|
1815
|
-
layout[0].rev =
|
|
1816
|
-
orderAuthenticatedSettings(entries);
|
|
1817
|
-
|
|
1818
|
-
|
|
1859
|
+
layout[0].rev = `dsh-mobile-layout-${DSH_MOBILE_VERSION}`;
|
|
1860
|
+
orderAuthenticatedSettings(entries, dependencyProfile.slots);
|
|
1861
|
+
let mobileBatch;
|
|
1862
|
+
if (parsed.batches !== void 0) {
|
|
1863
|
+
if (!Array.isArray(parsed.batches)) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
1864
|
+
const batches = parsed.batches;
|
|
1865
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1866
|
+
if (entryById.size !== entries.length) throw new Error("upstream DSH boot manifest has duplicate entries");
|
|
1867
|
+
const layoutBatches = [];
|
|
1868
|
+
for (const batch of batches) {
|
|
1869
|
+
if (batch === null || typeof batch !== "object" || batch.phase !== "bootstrap" && batch.phase !== "application" || typeof batch.url !== "string" || typeof batch.rev !== "string" || !Array.isArray(batch.entries) || batch.entries.length === 0 || batch.entries.some((id) => typeof id !== "string" || !entryById.has(id))) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
1870
|
+
if (batch.entries.includes(MOBILE_LAYOUT_MODULE)) layoutBatches.push(batch);
|
|
1871
|
+
}
|
|
1872
|
+
if (layoutBatches.length !== 1 || layoutBatches[0]?.phase !== "application") throw new Error("upstream DSH boot manifest has no unique application layout batch");
|
|
1873
|
+
const layoutBatch = layoutBatches[0];
|
|
1874
|
+
const planEntries = layoutBatch.entries.map((id) => {
|
|
1875
|
+
const entry = entryById.get(id);
|
|
1876
|
+
if (entry === void 0 || typeof entry.url !== "string" || typeof entry.rev !== "string") throw new Error("upstream DSH boot manifest batches are malformed");
|
|
1877
|
+
return Object.freeze({
|
|
1878
|
+
id,
|
|
1879
|
+
url: entry.url,
|
|
1880
|
+
rev: entry.rev
|
|
1881
|
+
});
|
|
1882
|
+
});
|
|
1883
|
+
const revision = revisionedMobileBatchPath(planEntries);
|
|
1884
|
+
layoutBatch.url = revision.path;
|
|
1885
|
+
layoutBatch.rev = revision.key;
|
|
1886
|
+
mobileBatch = Object.freeze({
|
|
1887
|
+
...revision,
|
|
1888
|
+
entries: Object.freeze(planEntries)
|
|
1889
|
+
});
|
|
1890
|
+
parsed.rev = createHash("sha256").update(JSON.stringify({
|
|
1891
|
+
entries,
|
|
1892
|
+
batches
|
|
1893
|
+
})).digest("hex").slice(0, 16);
|
|
1894
|
+
}
|
|
1895
|
+
const replacement = `${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${assignment[0]}${JSON.stringify(parsed)};`;
|
|
1896
|
+
return Object.freeze({
|
|
1897
|
+
html: ensureMobileViewport(`${html.slice(0, start)}${replacement}${html.slice(scriptEnd)}`),
|
|
1898
|
+
...mobileBatch === void 0 ? {} : { batch: mobileBatch }
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
/** Replace only DSH's layout client module while retaining its complete plugin graph. */
|
|
1902
|
+
function rewriteMobileIndex(html) {
|
|
1903
|
+
return rewriteMobileIndexWithBatch(html).html;
|
|
1819
1904
|
}
|
|
1820
1905
|
const PAIR_SCRIPT = `(() => {
|
|
1821
1906
|
const form = document.getElementById('pair-form')
|
|
@@ -2083,6 +2168,20 @@ function shouldCompressResponse(request, response) {
|
|
|
2083
2168
|
const pathname = request.url?.split("?", 1)[0] ?? "";
|
|
2084
2169
|
return (request.method === "GET" && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) || request.method === "POST" && pathname === SESSION_HISTORY_PATH) && 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"]);
|
|
2085
2170
|
}
|
|
2171
|
+
function revisionedStaticCacheControl(request) {
|
|
2172
|
+
if (request.method !== "GET" && request.method !== "HEAD") return void 0;
|
|
2173
|
+
let target;
|
|
2174
|
+
try {
|
|
2175
|
+
target = new URL(request.url ?? "/", "https://dsh-mobile.invalid");
|
|
2176
|
+
} catch {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const revision = target.searchParams.get("rev");
|
|
2180
|
+
const hasRevision = revision !== null && /^[a-z0-9_-]{4,128}$/iu.test(revision);
|
|
2181
|
+
const hashedAsset = /^\/assets\/.*-[a-z0-9_-]{8,}\.[a-z0-9]+$/iu.test(target.pathname);
|
|
2182
|
+
if (!(target.pathname.startsWith("/plugins/") && hasRevision) && !(target.pathname.startsWith("/assets/") && (hasRevision || hashedAsset))) return void 0;
|
|
2183
|
+
return "private, max-age=31536000, immutable";
|
|
2184
|
+
}
|
|
2086
2185
|
function isJsonRecord(value) {
|
|
2087
2186
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2088
2187
|
}
|
|
@@ -2177,6 +2276,9 @@ function extensionTarget(pathname) {
|
|
|
2177
2276
|
path: `/${parts.join("/")}`.replace(/\/{2,}/gu, "/")
|
|
2178
2277
|
};
|
|
2179
2278
|
}
|
|
2279
|
+
function mobileBootBatchKey(pathname) {
|
|
2280
|
+
return new RegExp(`^${MOBILE_BOOT_BATCH_PREFIX.replaceAll("/", "\\/")}([a-f\\d]{64})\\.js$`, "u").exec(pathname)?.[1];
|
|
2281
|
+
}
|
|
2180
2282
|
async function readBoundedBody(request, maximum) {
|
|
2181
2283
|
const declared = request.headers["content-length"];
|
|
2182
2284
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > maximum)) throw new HttpError(413, "payload_too_large");
|
|
@@ -2226,6 +2328,7 @@ function extensionContentType(path) {
|
|
|
2226
2328
|
var MobileAccessGateway = class {
|
|
2227
2329
|
config;
|
|
2228
2330
|
extensions;
|
|
2331
|
+
upstreamAuthenticatedUrl;
|
|
2229
2332
|
access;
|
|
2230
2333
|
listenerTlsEnabled;
|
|
2231
2334
|
tlsEnabled;
|
|
@@ -2239,15 +2342,21 @@ var MobileAccessGateway = class {
|
|
|
2239
2342
|
connectedSockets = /* @__PURE__ */ new Set();
|
|
2240
2343
|
activeRequests = /* @__PURE__ */ new Map();
|
|
2241
2344
|
activeWebSockets = /* @__PURE__ */ new Map();
|
|
2345
|
+
mobileBootBatches = /* @__PURE__ */ new Map();
|
|
2346
|
+
upstreamCookie;
|
|
2347
|
+
upstreamCookieExpiresAt = 0;
|
|
2348
|
+
upstreamCookieTask;
|
|
2349
|
+
upstreamAuthRequest;
|
|
2242
2350
|
nextOperationId = 1;
|
|
2243
2351
|
closing = false;
|
|
2244
2352
|
started = false;
|
|
2245
2353
|
closeTask;
|
|
2246
2354
|
removeSessionListener;
|
|
2247
2355
|
renewLimiter;
|
|
2248
|
-
constructor(config, store, extensions) {
|
|
2356
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl) {
|
|
2249
2357
|
this.config = config;
|
|
2250
2358
|
this.extensions = extensions;
|
|
2359
|
+
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
2251
2360
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
2252
2361
|
this.tlsEnabled = config.publicTls;
|
|
2253
2362
|
this.access = new AccessController(store, {
|
|
@@ -2543,13 +2652,24 @@ var MobileAccessGateway = class {
|
|
|
2543
2652
|
}
|
|
2544
2653
|
async handleExternalRequest(request, response) {
|
|
2545
2654
|
const target = parseRequestTarget(request.url);
|
|
2546
|
-
|
|
2655
|
+
const policy = this.requirePolicy();
|
|
2656
|
+
const isMutation = request.method !== "GET" && request.method !== "HEAD";
|
|
2657
|
+
assertExternalTrust(request, policy, isMutation);
|
|
2547
2658
|
if (target.decodedPathname === "/api/mobile-access" || target.decodedPathname.startsWith(`/api/mobile-access/`)) throw new HttpError(404, "not_found");
|
|
2548
2659
|
if (request.method === "TRACE" || request.method === "CONNECT") throw new HttpError(405, "method_not_allowed");
|
|
2549
2660
|
if (target.search === "" && request.method === "GET" && target.decodedPathname === `/mobile-access/health`) {
|
|
2550
2661
|
sendJson(response, 200, { ok: true }, this.tlsEnabled);
|
|
2551
2662
|
return;
|
|
2552
2663
|
}
|
|
2664
|
+
if (target.search === "" && request.method === "GET" && target.decodedPathname === `/mobile-access/metadata`) {
|
|
2665
|
+
sendJson(response, 200, {
|
|
2666
|
+
version: 1,
|
|
2667
|
+
pluginVersion: DSH_MOBILE_VERSION,
|
|
2668
|
+
minimumAndroidAppVersion: MINIMUM_ANDROID_APP_VERSION,
|
|
2669
|
+
discoveryProtocol: DISCOVERY_PROTOCOL
|
|
2670
|
+
}, this.tlsEnabled);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2553
2673
|
if (target.search === "" && request.method === "GET" && target.decodedPathname === `/mobile-access/discovery`) {
|
|
2554
2674
|
sendJson(response, 200, {
|
|
2555
2675
|
deviceName: discoveryDeviceName(),
|
|
@@ -2617,22 +2737,22 @@ var MobileAccessGateway = class {
|
|
|
2617
2737
|
const computerImages = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-images`;
|
|
2618
2738
|
const computerImage = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-image`;
|
|
2619
2739
|
const requestedExtension = extensionTarget(target.decodedPathname);
|
|
2740
|
+
const requestedMobileBootBatch = mobileBootBatchKey(target.decodedPathname);
|
|
2620
2741
|
const customAsset = request.method === "GET" ? target.decodedPathname === `/mobile-access/custom.css` ? {
|
|
2621
2742
|
file: this.config.customCssFile,
|
|
2622
2743
|
contentType: "text/css; charset=utf-8",
|
|
2623
|
-
fallback:
|
|
2744
|
+
fallback: CUSTOM_STYLE_FALLBACK
|
|
2624
2745
|
} : target.decodedPathname === `/mobile-access/custom.js` ? {
|
|
2625
2746
|
file: this.config.customScriptFile,
|
|
2626
2747
|
contentType: "text/javascript; charset=utf-8",
|
|
2627
|
-
fallback:
|
|
2748
|
+
fallback: CUSTOM_SCRIPT_FALLBACK
|
|
2628
2749
|
} : target.decodedPathname === MOBILE_LAYOUT_PATH ? {
|
|
2629
2750
|
file: this.config.mobileLayoutFile,
|
|
2630
2751
|
contentType: "text/javascript; charset=utf-8",
|
|
2631
2752
|
fallback: void 0
|
|
2632
2753
|
} : void 0 : void 0;
|
|
2633
|
-
if (customAsset === void 0 && !computerImages && !computerImage && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
2754
|
+
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");
|
|
2634
2755
|
if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "POST" && requestedExtension?.kind !== "route") throw new HttpError(405, "method_not_allowed");
|
|
2635
|
-
if (request.method === "POST" && target.decodedPathname !== "/api" && !target.decodedPathname.startsWith("/api/") && requestedExtension?.kind !== "action" && requestedExtension?.kind !== "route") throw new HttpError(405, "method_not_allowed");
|
|
2636
2756
|
let authorization;
|
|
2637
2757
|
try {
|
|
2638
2758
|
authorization = this.authorize(request);
|
|
@@ -2652,11 +2772,16 @@ var MobileAccessGateway = class {
|
|
|
2652
2772
|
}
|
|
2653
2773
|
throw error;
|
|
2654
2774
|
}
|
|
2775
|
+
if (isMutation) this.requireCsrf(request, authorization);
|
|
2655
2776
|
const extension = requestedExtension;
|
|
2656
2777
|
if (extension !== void 0) {
|
|
2657
2778
|
await this.handleExtensionRequest(extension, target, request, response, authorization);
|
|
2658
2779
|
return;
|
|
2659
2780
|
}
|
|
2781
|
+
if (requestedMobileBootBatch !== void 0) {
|
|
2782
|
+
await this.serveMobileBootBatch(requestedMobileBootBatch, request, response, authorization);
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2660
2785
|
if (customAsset !== void 0) {
|
|
2661
2786
|
const operation = this.allocateRequest(authorization, response, {});
|
|
2662
2787
|
try {
|
|
@@ -2734,15 +2859,30 @@ var MobileAccessGateway = class {
|
|
|
2734
2859
|
async handleExtensionRequest(targetInfo, target, request, response, authorization) {
|
|
2735
2860
|
const extensions = this.extensions;
|
|
2736
2861
|
if (extensions === void 0) throw new HttpError(404, "not_found");
|
|
2737
|
-
if (request.method !== "GET" && request.method !== "HEAD") this.requireCsrf(request, authorization);
|
|
2738
2862
|
if (targetInfo.kind === "manifest") {
|
|
2739
2863
|
if (request.method !== "GET" && request.method !== "HEAD") throw new HttpError(405, "method_not_allowed");
|
|
2740
2864
|
const operation = this.allocateRequest(authorization, response, {});
|
|
2741
2865
|
try {
|
|
2742
2866
|
operation.signal.throwIfAborted();
|
|
2867
|
+
const customRevision = async (file, fallback) => {
|
|
2868
|
+
let source;
|
|
2869
|
+
try {
|
|
2870
|
+
source = await readFile(file, { signal: operation.signal });
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
if (error.code !== "ENOENT") throw error;
|
|
2873
|
+
source = Buffer.from(fallback);
|
|
2874
|
+
}
|
|
2875
|
+
if (source.byteLength > 262144) throw new HttpError(413, "payload_too_large");
|
|
2876
|
+
return createHash("sha256").update(source).digest("hex");
|
|
2877
|
+
};
|
|
2878
|
+
const [scriptRevision, styleRevision] = await Promise.all([customRevision(this.config.customScriptFile, CUSTOM_SCRIPT_FALLBACK), customRevision(this.config.customCssFile, CUSTOM_STYLE_FALLBACK)]);
|
|
2743
2879
|
const body = Buffer.from(JSON.stringify({
|
|
2744
2880
|
protocol: 1,
|
|
2745
|
-
extensions: extensions.manifest()
|
|
2881
|
+
extensions: extensions.manifest(),
|
|
2882
|
+
legacy: {
|
|
2883
|
+
scriptRevision,
|
|
2884
|
+
styleRevision
|
|
2885
|
+
}
|
|
2746
2886
|
}));
|
|
2747
2887
|
const etag = createHash("sha256").update(body).update(extensions.contentDigest()).digest("hex");
|
|
2748
2888
|
if (headerValue(request.headers, "if-none-match") === etag) {
|
|
@@ -2897,11 +3037,81 @@ var MobileAccessGateway = class {
|
|
|
2897
3037
|
}
|
|
2898
3038
|
await pipeline(result.body, new ByteLimitTransform(4194304), response);
|
|
2899
3039
|
}
|
|
3040
|
+
/** Exchange DSH's process-local launch token for an authority-bound cookie kept inside this gateway. */
|
|
3041
|
+
async upstreamCookieHeader() {
|
|
3042
|
+
if (this.upstreamAuthenticatedUrl === void 0) return void 0;
|
|
3043
|
+
if (this.upstreamCookie !== void 0 && this.upstreamCookieExpiresAt > Date.now() + UPSTREAM_AUTH_REFRESH_MARGIN_MS) return this.upstreamCookie;
|
|
3044
|
+
if (this.upstreamCookieTask !== void 0) return this.upstreamCookieTask;
|
|
3045
|
+
const task = this.exchangeUpstreamCookie();
|
|
3046
|
+
this.upstreamCookieTask = task;
|
|
3047
|
+
try {
|
|
3048
|
+
return await task;
|
|
3049
|
+
} finally {
|
|
3050
|
+
if (this.upstreamCookieTask === task) this.upstreamCookieTask = void 0;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
async exchangeUpstreamCookie() {
|
|
3054
|
+
const authenticatedUrl = this.upstreamAuthenticatedUrl;
|
|
3055
|
+
if (authenticatedUrl === void 0) throw new HttpError(502, "upstream_unavailable");
|
|
3056
|
+
let target;
|
|
3057
|
+
try {
|
|
3058
|
+
target = new URL(authenticatedUrl);
|
|
3059
|
+
} catch {
|
|
3060
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
3061
|
+
}
|
|
3062
|
+
if (target.origin !== this.config.upstreamOrigin.origin || target.pathname !== "/" || target.hash !== "" || target.search === "") throw new HttpError(502, "upstream_unavailable");
|
|
3063
|
+
try {
|
|
3064
|
+
const proxied = await new Promise((resolve, reject) => {
|
|
3065
|
+
const upstreamRequest = request({
|
|
3066
|
+
protocol: "http:",
|
|
3067
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
3068
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
3069
|
+
method: "GET",
|
|
3070
|
+
path: `${target.pathname}${target.search}`,
|
|
3071
|
+
headers: {
|
|
3072
|
+
host: this.config.upstreamOrigin.host,
|
|
3073
|
+
accept: "text/html",
|
|
3074
|
+
"accept-encoding": "identity"
|
|
3075
|
+
},
|
|
3076
|
+
agent: false
|
|
3077
|
+
});
|
|
3078
|
+
this.upstreamAuthRequest = upstreamRequest;
|
|
3079
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
3080
|
+
upstreamRequest.destroy(/* @__PURE__ */ new Error("upstream timeout"));
|
|
3081
|
+
});
|
|
3082
|
+
upstreamRequest.once("response", resolve);
|
|
3083
|
+
upstreamRequest.once("error", reject);
|
|
3084
|
+
upstreamRequest.end();
|
|
3085
|
+
});
|
|
3086
|
+
await new Promise((resolve, reject) => {
|
|
3087
|
+
proxied.once("end", resolve);
|
|
3088
|
+
proxied.once("error", reject);
|
|
3089
|
+
proxied.resume();
|
|
3090
|
+
});
|
|
3091
|
+
const setCookie = proxied.headers["set-cookie"]?.[0];
|
|
3092
|
+
const pair = setCookie?.split(";", 1)[0];
|
|
3093
|
+
const maxAgeText = setCookie === void 0 ? void 0 : /(?:^|;\s*)Max-Age=(\d+)(?:;|$)/iu.exec(setCookie)?.[1];
|
|
3094
|
+
const maxAgeSeconds = maxAgeText === void 0 ? NaN : Number(maxAgeText);
|
|
3095
|
+
const expiresAt = Date.now() + maxAgeSeconds * 1e3;
|
|
3096
|
+
if (proxied.statusCode !== 303 || pair === void 0 || pair.length > 4096 || !UPSTREAM_COOKIE_PAIR.test(pair) || !Number.isSafeInteger(expiresAt) || maxAgeSeconds <= 0) throw new HttpError(502, "upstream_unavailable");
|
|
3097
|
+
this.upstreamCookie = pair;
|
|
3098
|
+
this.upstreamCookieExpiresAt = expiresAt;
|
|
3099
|
+
return pair;
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
if (error instanceof HttpError) throw error;
|
|
3102
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
3103
|
+
} finally {
|
|
3104
|
+
this.upstreamAuthRequest?.destroy();
|
|
3105
|
+
this.upstreamAuthRequest = void 0;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2900
3108
|
async proxyMobileIndex(request$1, response, authorization) {
|
|
2901
3109
|
const holder = {};
|
|
2902
3110
|
const operation = this.allocateRequest(authorization, response, holder);
|
|
2903
3111
|
try {
|
|
2904
3112
|
const upstreamHeaders = sanitizeRequestHeaders(request$1, this.config.upstreamOrigin);
|
|
3113
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
3114
|
+
if (upstreamCookie !== void 0) upstreamHeaders.cookie = upstreamCookie;
|
|
2905
3115
|
upstreamHeaders["accept-encoding"] = "identity";
|
|
2906
3116
|
const proxied = await new Promise((resolve, reject) => {
|
|
2907
3117
|
const upstreamRequest = request({
|
|
@@ -2932,7 +3142,9 @@ var MobileAccessGateway = class {
|
|
|
2932
3142
|
}
|
|
2933
3143
|
let body;
|
|
2934
3144
|
try {
|
|
2935
|
-
|
|
3145
|
+
const rewritten = rewriteMobileIndexWithBatch(Buffer.concat(chunks).toString("utf8"));
|
|
3146
|
+
if (rewritten.batch !== void 0) this.rememberMobileBootBatch(rewritten.batch);
|
|
3147
|
+
body = Buffer.from(rewritten.html);
|
|
2936
3148
|
} catch {
|
|
2937
3149
|
throw new HttpError(502, "upstream_unavailable");
|
|
2938
3150
|
}
|
|
@@ -2956,6 +3168,128 @@ var MobileAccessGateway = class {
|
|
|
2956
3168
|
operation.release();
|
|
2957
3169
|
}
|
|
2958
3170
|
}
|
|
3171
|
+
rememberMobileBootBatch(plan) {
|
|
3172
|
+
const existing = this.mobileBootBatches.get(plan.key);
|
|
3173
|
+
this.mobileBootBatches.delete(plan.key);
|
|
3174
|
+
this.mobileBootBatches.set(plan.key, existing ?? { plan });
|
|
3175
|
+
while (this.mobileBootBatches.size > MAX_MOBILE_BOOT_BATCHES) {
|
|
3176
|
+
const oldest = this.mobileBootBatches.keys().next().value;
|
|
3177
|
+
if (oldest === void 0) break;
|
|
3178
|
+
this.mobileBootBatches.delete(oldest);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
async serveMobileBootBatch(key, request, response, authorization) {
|
|
3182
|
+
if (request.method !== "GET" && request.method !== "HEAD") throw new HttpError(405, "method_not_allowed");
|
|
3183
|
+
const stored = this.mobileBootBatches.get(key);
|
|
3184
|
+
if (stored === void 0) throw new HttpError(404, "not_found");
|
|
3185
|
+
const operation = this.allocateRequest(authorization, response, {});
|
|
3186
|
+
try {
|
|
3187
|
+
const layoutStat = await stat(this.config.mobileLayoutFile);
|
|
3188
|
+
if (stored.body === void 0 || stored.etag === void 0 || stored.layoutMtimeMs !== layoutStat.mtimeMs) {
|
|
3189
|
+
const body = await this.assembleMobileBootBatch(stored.plan, operation.signal);
|
|
3190
|
+
stored.body = body;
|
|
3191
|
+
delete stored.gzipBody;
|
|
3192
|
+
stored.etag = createHash("sha256").update(body).digest("hex");
|
|
3193
|
+
stored.layoutMtimeMs = layoutStat.mtimeMs;
|
|
3194
|
+
}
|
|
3195
|
+
const compressed = acceptsGzip(request.headers["accept-encoding"]);
|
|
3196
|
+
const body = compressed ? stored.gzipBody ??= await gzipBuffer(stored.body) : stored.body;
|
|
3197
|
+
const etag = compressed ? `${stored.etag}-gzip` : stored.etag;
|
|
3198
|
+
const headers = {
|
|
3199
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
3200
|
+
"Content-Length": body.byteLength,
|
|
3201
|
+
"Cache-Control": "private, no-cache",
|
|
3202
|
+
ETag: etag
|
|
3203
|
+
};
|
|
3204
|
+
if (compressed) headers["Content-Encoding"] = "gzip";
|
|
3205
|
+
addVaryAcceptEncoding(headers);
|
|
3206
|
+
if (headerValue(request.headers, "if-none-match") === etag) {
|
|
3207
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
3208
|
+
response.writeHead(304, {
|
|
3209
|
+
ETag: etag,
|
|
3210
|
+
"Cache-Control": "private, no-cache",
|
|
3211
|
+
Vary: String(headers.vary)
|
|
3212
|
+
});
|
|
3213
|
+
response.end();
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
3217
|
+
response.writeHead(200, headers);
|
|
3218
|
+
if (request.method === "HEAD") response.end();
|
|
3219
|
+
else response.end(body);
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
if (error.code === "ENOENT") throw new HttpError(503, "mobile_frontend_unavailable");
|
|
3222
|
+
throw error;
|
|
3223
|
+
} finally {
|
|
3224
|
+
operation.release();
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
async assembleMobileBootBatch(plan, signal) {
|
|
3228
|
+
const bodies = new Array(plan.entries.length);
|
|
3229
|
+
let cursor = 0;
|
|
3230
|
+
const worker = async () => {
|
|
3231
|
+
while (cursor < plan.entries.length) {
|
|
3232
|
+
const index = cursor++;
|
|
3233
|
+
const entry = plan.entries[index];
|
|
3234
|
+
bodies[index] = entry.id === MOBILE_LAYOUT_MODULE ? await readFile(this.config.mobileLayoutFile, { signal }) : await this.readUpstreamClientBundle(entry.url, signal);
|
|
3235
|
+
if (bodies[index].byteLength > MAX_MOBILE_BOOT_ENTRY_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
await Promise.all(Array.from({ length: Math.min(8, plan.entries.length) }, worker));
|
|
3239
|
+
if (bodies.reduce((bytes, body) => bytes + body.byteLength + 2, 0) > MAX_MOBILE_BOOT_BATCH_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
3240
|
+
return Buffer.concat(bodies.flatMap((body) => [body, Buffer.from("\n;\n")]));
|
|
3241
|
+
}
|
|
3242
|
+
async readUpstreamClientBundle(source, signal) {
|
|
3243
|
+
if (!source.startsWith("/plugins/") || source.includes("#")) throw new HttpError(502, "upstream_unavailable");
|
|
3244
|
+
const target = new URL(source, this.config.upstreamOrigin);
|
|
3245
|
+
if (target.origin !== this.config.upstreamOrigin.origin) throw new HttpError(502, "upstream_unavailable");
|
|
3246
|
+
let upstreamRequest;
|
|
3247
|
+
const aborted = () => {
|
|
3248
|
+
upstreamRequest?.destroy(/* @__PURE__ */ new Error("request aborted"));
|
|
3249
|
+
};
|
|
3250
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
3251
|
+
try {
|
|
3252
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
3253
|
+
const proxied = await new Promise((resolve, reject) => {
|
|
3254
|
+
upstreamRequest = request({
|
|
3255
|
+
protocol: "http:",
|
|
3256
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
3257
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
3258
|
+
method: "GET",
|
|
3259
|
+
path: `${target.pathname}${target.search}`,
|
|
3260
|
+
headers: {
|
|
3261
|
+
host: this.config.upstreamOrigin.host,
|
|
3262
|
+
accept: "text/javascript",
|
|
3263
|
+
"accept-encoding": "identity",
|
|
3264
|
+
...upstreamCookie === void 0 ? {} : { cookie: upstreamCookie }
|
|
3265
|
+
},
|
|
3266
|
+
agent: false
|
|
3267
|
+
});
|
|
3268
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
3269
|
+
upstreamRequest?.destroy(/* @__PURE__ */ new Error("upstream timeout"));
|
|
3270
|
+
});
|
|
3271
|
+
upstreamRequest.once("response", resolve);
|
|
3272
|
+
upstreamRequest.once("error", reject);
|
|
3273
|
+
upstreamRequest.end();
|
|
3274
|
+
});
|
|
3275
|
+
if ((proxied.statusCode ?? 502) !== 200) throw new HttpError(502, "upstream_unavailable");
|
|
3276
|
+
const chunks = [];
|
|
3277
|
+
let bytes = 0;
|
|
3278
|
+
for await (const chunk of proxied) {
|
|
3279
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3280
|
+
bytes += buffer.byteLength;
|
|
3281
|
+
if (bytes > MAX_MOBILE_BOOT_ENTRY_BYTES) throw new HttpError(502, "upstream_unavailable");
|
|
3282
|
+
chunks.push(buffer);
|
|
3283
|
+
}
|
|
3284
|
+
return Buffer.concat(chunks);
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
if (error instanceof HttpError) throw error;
|
|
3287
|
+
throw new HttpError(502, "upstream_unavailable");
|
|
3288
|
+
} finally {
|
|
3289
|
+
signal.removeEventListener("abort", aborted);
|
|
3290
|
+
upstreamRequest?.destroy();
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
2959
3293
|
allocateRequest(authorization, response, upstream) {
|
|
2960
3294
|
if (this.activeRequests.size >= this.config.maxActiveRequests) throw new HttpError(429, "busy");
|
|
2961
3295
|
const id = this.nextOperationId++;
|
|
@@ -2991,6 +3325,8 @@ var MobileAccessGateway = class {
|
|
|
2991
3325
|
try {
|
|
2992
3326
|
const bufferedBody = request$2.method === "POST" && request$2.url?.split("?", 1)[0] === SESSION_HISTORY_PATH ? mobileHistoryRequestBody(request$2, await readBoundedBody(request$2, this.config.maxBodyBytes)) : void 0;
|
|
2993
3327
|
const upstreamHeaders = sanitizeRequestHeaders(request$2, this.config.upstreamOrigin);
|
|
3328
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
3329
|
+
if (upstreamCookie !== void 0) upstreamHeaders.cookie = upstreamCookie;
|
|
2994
3330
|
if (bufferedBody !== void 0) upstreamHeaders["content-length"] = String(bufferedBody.byteLength);
|
|
2995
3331
|
const proxied = await new Promise((resolve, reject) => {
|
|
2996
3332
|
const upstreamRequest = request({
|
|
@@ -3017,6 +3353,8 @@ var MobileAccessGateway = class {
|
|
|
3017
3353
|
});
|
|
3018
3354
|
setSecurityHeaders(response, this.tlsEnabled);
|
|
3019
3355
|
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
3356
|
+
const cacheControl = revisionedStaticCacheControl(request$2);
|
|
3357
|
+
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
3020
3358
|
const compressed = shouldCompressResponse(request$2, proxied);
|
|
3021
3359
|
if (compressed) {
|
|
3022
3360
|
delete headers["accept-ranges"];
|
|
@@ -3132,6 +3470,7 @@ var MobileAccessGateway = class {
|
|
|
3132
3470
|
if (decodedKey.length !== 16 || decodedKey.toString("base64") !== key) throw new HttpError(400, "bad_request");
|
|
3133
3471
|
const authorization = this.authorize(request);
|
|
3134
3472
|
if (this.activeWebSockets.size >= this.config.maxWebSockets) throw new HttpError(429, "busy");
|
|
3473
|
+
const upstreamCookie = await this.upstreamCookieHeader();
|
|
3135
3474
|
const upstream = connect({
|
|
3136
3475
|
host: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
3137
3476
|
port: Number(this.config.upstreamOrigin.port)
|
|
@@ -3190,6 +3529,7 @@ var MobileAccessGateway = class {
|
|
|
3190
3529
|
`Sec-WebSocket-Key: ${key}`,
|
|
3191
3530
|
"Sec-WebSocket-Version: 13"
|
|
3192
3531
|
];
|
|
3532
|
+
if (upstreamCookie !== void 0) requestLines.push(`Cookie: ${upstreamCookie}`);
|
|
3193
3533
|
const protocol = headerValue(request.headers, "sec-websocket-protocol");
|
|
3194
3534
|
const extensions = headerValue(request.headers, "sec-websocket-extensions");
|
|
3195
3535
|
if (protocol !== void 0) requestLines.push(`Sec-WebSocket-Protocol: ${protocol}`);
|
|
@@ -3289,6 +3629,8 @@ var MobileAccessGateway = class {
|
|
|
3289
3629
|
}
|
|
3290
3630
|
async performClose() {
|
|
3291
3631
|
this.closing = true;
|
|
3632
|
+
this.upstreamAuthRequest?.destroy();
|
|
3633
|
+
this.upstreamAuthRequest = void 0;
|
|
3292
3634
|
this.removeSessionListener();
|
|
3293
3635
|
const accessClose = this.access.close();
|
|
3294
3636
|
for (const request of this.activeRequests.values()) request.abort();
|
|
@@ -3452,6 +3794,190 @@ var MemoryDeviceStore = class {
|
|
|
3452
3794
|
}
|
|
3453
3795
|
};
|
|
3454
3796
|
//#endregion
|
|
3797
|
+
//#region src/diagnostics.ts
|
|
3798
|
+
const execFile$2 = promisify(execFile);
|
|
3799
|
+
const REMOTE_ERROR_GUIDANCE = Object.freeze({
|
|
3800
|
+
component_missing: "重新安装完整插件包。",
|
|
3801
|
+
funnel_permission_required: "继续完成 Tailscale Funnel 授权。",
|
|
3802
|
+
funnel_https_required: "继续完成 Tailscale HTTPS 授权。",
|
|
3803
|
+
funnel_start_failed: "重新打开授权页并允许 Funnel。",
|
|
3804
|
+
funnel_start_timeout: "检查网络后点击“重新连接”。",
|
|
3805
|
+
tailscale_dns_missing: "确认 Tailscale 登录仍有效后重新连接。",
|
|
3806
|
+
sidecar_launch_failed: "重新安装完整插件包后重试。",
|
|
3807
|
+
sidecar_stopped: "点击“重新连接”。",
|
|
3808
|
+
sidecar_exited: "点击“重新连接”;仍失败时复制诊断报告。",
|
|
3809
|
+
control_channel_failed: "点击“重新连接”。",
|
|
3810
|
+
cpolar_component_missing: "先安装 cpolar 官方组件。",
|
|
3811
|
+
cpolar_component_invalid: "彻底移除 cpolar 组件后重新安装。",
|
|
3812
|
+
cpolar_config_missing: "保存 cpolar Authtoken 后重试。",
|
|
3813
|
+
cpolar_config_invalid: "重新保存 cpolar Authtoken。",
|
|
3814
|
+
cpolar_start_timeout: "检查网络后点击“重新连接”。",
|
|
3815
|
+
cpolar_stopped: "点击“重新连接”。",
|
|
3816
|
+
cpolar_exited: "点击“重新连接”;仍失败时复制诊断报告。",
|
|
3817
|
+
gateway_start_failed: "确认 DSH 正在运行后重新连接。"
|
|
3818
|
+
});
|
|
3819
|
+
function check(id, status, label, detail, action) {
|
|
3820
|
+
return Object.freeze({
|
|
3821
|
+
id,
|
|
3822
|
+
status,
|
|
3823
|
+
label,
|
|
3824
|
+
detail,
|
|
3825
|
+
...action === void 0 ? {} : { action }
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
function maskLanOrigin(origin) {
|
|
3829
|
+
if (origin === void 0) return "未分配";
|
|
3830
|
+
try {
|
|
3831
|
+
const url = new URL(origin);
|
|
3832
|
+
const octets = url.hostname.split(".");
|
|
3833
|
+
const host = octets.length === 4 ? `${octets[0]}.${octets[1]}.${octets[2]}.x` : "局域网地址";
|
|
3834
|
+
return `${url.protocol}//${host}${url.port === "" ? "" : `:${url.port}`}`;
|
|
3835
|
+
} catch {
|
|
3836
|
+
return "地址格式无效";
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
function remoteSuffix(origin) {
|
|
3840
|
+
if (origin === void 0) return "未分配";
|
|
3841
|
+
try {
|
|
3842
|
+
const hostname = new URL(origin).hostname;
|
|
3843
|
+
if (hostname.endsWith(".ts.net")) return "*.ts.net";
|
|
3844
|
+
for (const suffix of [
|
|
3845
|
+
".cpolar.cn",
|
|
3846
|
+
".cpolar.io",
|
|
3847
|
+
".cpolar.top",
|
|
3848
|
+
".cpolar.com"
|
|
3849
|
+
]) if (hostname.endsWith(suffix)) return `*${suffix}`;
|
|
3850
|
+
return "公共 HTTPS 地址";
|
|
3851
|
+
} catch {
|
|
3852
|
+
return "地址格式无效";
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
function defaultFirewallProbe(platform = process.platform) {
|
|
3856
|
+
return async (port) => {
|
|
3857
|
+
if (platform !== "win32") return { state: "not-applicable" };
|
|
3858
|
+
if (port === void 0) return { state: "unknown" };
|
|
3859
|
+
const script = [
|
|
3860
|
+
"$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
|
|
3861
|
+
"$ready = $true",
|
|
3862
|
+
"$specs | ForEach-Object {",
|
|
3863
|
+
" $spec = $_",
|
|
3864
|
+
" $rule = Get-NetFirewallRule -DisplayName $spec.Name -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' } | Select-Object -First 1",
|
|
3865
|
+
" if ($null -eq $rule) { $ready = $false; return }",
|
|
3866
|
+
" $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
|
|
3867
|
+
` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
|
|
3868
|
+
" if ($matching.Count -eq 0) { $ready = $false }",
|
|
3869
|
+
"}",
|
|
3870
|
+
"if ($ready) { 'ready' } else { 'missing' }"
|
|
3871
|
+
].join("; ");
|
|
3872
|
+
try {
|
|
3873
|
+
return { state: (await execFile$2("powershell.exe", [
|
|
3874
|
+
"-NoProfile",
|
|
3875
|
+
"-NonInteractive",
|
|
3876
|
+
"-Command",
|
|
3877
|
+
script
|
|
3878
|
+
], {
|
|
3879
|
+
encoding: "utf8",
|
|
3880
|
+
timeout: 3e3,
|
|
3881
|
+
windowsHide: true
|
|
3882
|
+
})).stdout.trim() === "ready" ? "ready" : "missing" };
|
|
3883
|
+
} catch {
|
|
3884
|
+
return { state: "unknown" };
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
/** Allow remote relays enough time to answer without making diagnostics unbounded. */
|
|
3889
|
+
function remoteDiagnosticTimeoutMs(origin) {
|
|
3890
|
+
const hostname = new URL(origin).hostname.toLowerCase();
|
|
3891
|
+
if (hostname.endsWith(".ts.net") || hostname.includes(".cpolar.")) return 1e4;
|
|
3892
|
+
return 5e3;
|
|
3893
|
+
}
|
|
3894
|
+
async function defaultRemoteProbe(origin) {
|
|
3895
|
+
if (origin === void 0) return { state: "not-applicable" };
|
|
3896
|
+
const hostname = new URL(origin).hostname;
|
|
3897
|
+
const started = performance.now();
|
|
3898
|
+
try {
|
|
3899
|
+
const response = await fetch(new URL("/mobile-access/health", origin), {
|
|
3900
|
+
cache: "no-store",
|
|
3901
|
+
redirect: "error",
|
|
3902
|
+
signal: AbortSignal.timeout(remoteDiagnosticTimeoutMs(origin))
|
|
3903
|
+
});
|
|
3904
|
+
const latencyMs = Math.max(0, Math.round(performance.now() - started));
|
|
3905
|
+
if (response.status === 429) return {
|
|
3906
|
+
state: "rate-limited",
|
|
3907
|
+
latencyMs
|
|
3908
|
+
};
|
|
3909
|
+
return response.ok ? {
|
|
3910
|
+
state: "ready",
|
|
3911
|
+
latencyMs
|
|
3912
|
+
} : {
|
|
3913
|
+
state: "unreachable",
|
|
3914
|
+
latencyMs
|
|
3915
|
+
};
|
|
3916
|
+
} catch {
|
|
3917
|
+
let fakeIp = false;
|
|
3918
|
+
try {
|
|
3919
|
+
fakeIp = (await lookup(hostname, { all: true })).some(({ address }) => {
|
|
3920
|
+
const [first, second] = address.split(".").map(Number);
|
|
3921
|
+
return first === 198 && (second === 18 || second === 19);
|
|
3922
|
+
});
|
|
3923
|
+
} catch {}
|
|
3924
|
+
return {
|
|
3925
|
+
state: "unreachable",
|
|
3926
|
+
...fakeIp ? { fakeIp: true } : {}
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
function reportLine(entry) {
|
|
3931
|
+
return `[${entry.status.toUpperCase()}] ${entry.label}: ${entry.detail}${entry.action === void 0 ? "" : ` ${entry.action}`}`;
|
|
3932
|
+
}
|
|
3933
|
+
/** Run bounded read-only checks and return a report safe to paste into an issue. */
|
|
3934
|
+
async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
3935
|
+
const checks = [];
|
|
3936
|
+
const remoteProbe = snapshot.remote.running && snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0 ? (probes.remote ?? defaultRemoteProbe)(snapshot.remote.origin) : Promise.resolve({ state: "not-applicable" });
|
|
3937
|
+
const [firewall, remoteObservation] = await Promise.all([(probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port), remoteProbe]);
|
|
3938
|
+
checks.push(check("versions", "ok", "版本兼容", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
|
|
3939
|
+
if (snapshot.lan.networkError !== void 0) checks.push(check("network", "error", "局域网网卡", "已保存的网卡当前不可用。", "重新运行 dsh-mobile setup。"));
|
|
3940
|
+
else if (snapshot.lan.configuredInterface !== void 0) checks.push(check("network", "ok", "局域网网卡", `正在跟随 ${snapshot.lan.interfaceName ?? snapshot.lan.configuredInterface}。`));
|
|
3941
|
+
else checks.push(check("network", "info", "局域网网卡", "当前使用固定网络配置。"));
|
|
3942
|
+
if (snapshot.lan.running && snapshot.lan.origin !== void 0) checks.push(check("lan", "ok", "局域网网关", `已监听 ${maskLanOrigin(snapshot.lan.origin)},配对入口可用。`));
|
|
3943
|
+
else checks.push(check("lan", "info", "局域网网关", "当前未开启。", "需要手机直连时开启局域网访问。"));
|
|
3944
|
+
if (firewall.state === "ready") checks.push(check("firewall", "ok", "Windows 防火墙", "局域网 TCP 与发现规则已启用。"));
|
|
3945
|
+
else if (firewall.state === "missing") checks.push(check("firewall", "warning", "Windows 防火墙", "未找到完整的局域网放行规则。", "以管理员身份重新运行 dsh-mobile setup。"));
|
|
3946
|
+
else if (firewall.state === "unknown") checks.push(check("firewall", "info", "Windows 防火墙", "系统未允许插件读取防火墙状态。", "若手机找不到电脑,以管理员身份重新运行 setup。"));
|
|
3947
|
+
if (!snapshot.remote.running || snapshot.remote.state === "off") checks.push(check("remote", "info", "远程通道", "当前未启用。"));
|
|
3948
|
+
else if (snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0) {
|
|
3949
|
+
if (remoteObservation.state === "ready") checks.push(check("remote", "ok", "远程通道", `${snapshot.remote.provider} 公共地址 ${remoteSuffix(snapshot.remote.origin)} 可达,往返约 ${String(remoteObservation.latencyMs ?? 0)} ms。`));
|
|
3950
|
+
else if (remoteObservation.state === "rate-limited") checks.push(check("remote", "warning", "远程通道", "公共地址可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。"));
|
|
3951
|
+
else if (snapshot.remote.provider === "tailscale" && remoteObservation.fakeIp === true) checks.push(check("remote", "error", "远程通道", "Tailscale 地址被当前 VPN 或 DNS 代理接管,但 TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。"));
|
|
3952
|
+
else checks.push(check("remote", "error", "远程通道", "提供方显示已就绪,但公共地址暂不可达。", "点击“重新连接”;仍失败时检查提供方状态。"));
|
|
3953
|
+
} else if (snapshot.remote.state === "starting" || snapshot.remote.state === "connecting" || snapshot.remote.state === "needs-login") checks.push(check("remote", "warning", "远程通道", snapshot.remote.state === "needs-login" ? "等待完成 Tailscale 登录。" : "仍在建立连接。", snapshot.remote.state === "needs-login" ? "返回远程页继续登录。" : "等待片刻后重新检查。"));
|
|
3954
|
+
else checks.push(check("remote", "error", "远程通道", `连接未建立(${snapshot.remote.errorCode ?? snapshot.remote.state})。`, REMOTE_ERROR_GUIDANCE[snapshot.remote.errorCode ?? ""] ?? "返回远程页点击“重新连接”。"));
|
|
3955
|
+
checks.push(check("phone-network", "info", "手机网络", "电脑无法判断路由器是否隔离了手机。", "局域网仍失败时,确认手机与电脑在同一网络,并关闭访客网络或 AP 隔离。"));
|
|
3956
|
+
const overall = checks.some((entry) => entry.status === "error") ? "error" : checks.some((entry) => entry.status === "warning") ? "attention" : "ok";
|
|
3957
|
+
const summary = overall === "ok" ? "连接基础检查正常。" : overall === "attention" ? "发现需要留意的项目。" : "发现会影响连接的问题。";
|
|
3958
|
+
const report = [
|
|
3959
|
+
"DSH Mobile 诊断报告",
|
|
3960
|
+
`生成时间: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
3961
|
+
`版本: plugin=${DSH_MOBILE_VERSION}; dsh=${snapshot.dshVersion}; min-app=${MINIMUM_ANDROID_APP_VERSION}`,
|
|
3962
|
+
`LAN: ${snapshot.lan.running ? "on" : "off"}; endpoint=${maskLanOrigin(snapshot.lan.origin)}`,
|
|
3963
|
+
`Remote: provider=${snapshot.remote.provider}; state=${snapshot.remote.state}; endpoint=${remoteSuffix(snapshot.remote.origin)}`,
|
|
3964
|
+
...checks.map(reportLine)
|
|
3965
|
+
].join("\n");
|
|
3966
|
+
return Object.freeze({
|
|
3967
|
+
version: 1,
|
|
3968
|
+
generatedAt: Date.now(),
|
|
3969
|
+
overall,
|
|
3970
|
+
versions: Object.freeze({
|
|
3971
|
+
plugin: DSH_MOBILE_VERSION,
|
|
3972
|
+
dsh: snapshot.dshVersion,
|
|
3973
|
+
minimumAndroidApp: MINIMUM_ANDROID_APP_VERSION
|
|
3974
|
+
}),
|
|
3975
|
+
summary,
|
|
3976
|
+
checks: Object.freeze(checks),
|
|
3977
|
+
report
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
//#endregion
|
|
3455
3981
|
//#region src/mobile-guide.ts
|
|
3456
3982
|
/**
|
|
3457
3983
|
* Instructions handed to the DSH agent when the user runs `/mobile <task>`.
|
|
@@ -3487,6 +4013,7 @@ const MOBILE_CUSTOMIZATION_GUIDE = `你在为用户定制 DSH Mobile 的手机
|
|
|
3487
4013
|
//#endregion
|
|
3488
4014
|
//#region src/funnel.ts
|
|
3489
4015
|
const MAX_PROTOCOL_LINE_BYTES = 16384;
|
|
4016
|
+
const FUNNEL_START_TIMEOUT_MS = 45e3;
|
|
3490
4017
|
function publicStatus$1(status) {
|
|
3491
4018
|
return Object.freeze({
|
|
3492
4019
|
enabled: status.enabled,
|
|
@@ -3580,6 +4107,7 @@ var FunnelController = class {
|
|
|
3580
4107
|
state: "off"
|
|
3581
4108
|
});
|
|
3582
4109
|
queue = Promise.resolve();
|
|
4110
|
+
startTimer;
|
|
3583
4111
|
constructor(options) {
|
|
3584
4112
|
this.options = options;
|
|
3585
4113
|
if (!isAbsolute(options.executable) || !isAbsolute(options.stateDirectory)) throw new Error("Funnel paths must be absolute");
|
|
@@ -3718,6 +4246,11 @@ var FunnelController = class {
|
|
|
3718
4246
|
windowsHide: true
|
|
3719
4247
|
});
|
|
3720
4248
|
this.child = child;
|
|
4249
|
+
this.clearStartTimer();
|
|
4250
|
+
this.startTimer = setTimeout(() => {
|
|
4251
|
+
this.enqueue(() => this.failGeneration(generation, "funnel_start_timeout"));
|
|
4252
|
+
}, FUNNEL_START_TIMEOUT_MS);
|
|
4253
|
+
this.startTimer.unref();
|
|
3721
4254
|
child.stderr.resume();
|
|
3722
4255
|
child.stdout.setEncoding("utf8");
|
|
3723
4256
|
child.stdout.on("data", (chunk) => {
|
|
@@ -3756,6 +4289,7 @@ var FunnelController = class {
|
|
|
3756
4289
|
}
|
|
3757
4290
|
async handleEvent(generation, event) {
|
|
3758
4291
|
if (generation !== this.generation || !this.enabled) return;
|
|
4292
|
+
this.clearStartTimer();
|
|
3759
4293
|
if (event.type === "login") {
|
|
3760
4294
|
this.publish({
|
|
3761
4295
|
enabled: true,
|
|
@@ -3825,6 +4359,7 @@ var FunnelController = class {
|
|
|
3825
4359
|
await this.stopProcessAndGateway();
|
|
3826
4360
|
}
|
|
3827
4361
|
async stopProcessAndGateway() {
|
|
4362
|
+
this.clearStartTimer();
|
|
3828
4363
|
const child = this.child;
|
|
3829
4364
|
this.child = void 0;
|
|
3830
4365
|
child?.stdin.end();
|
|
@@ -3850,6 +4385,11 @@ var FunnelController = class {
|
|
|
3850
4385
|
this.gatewayValue = void 0;
|
|
3851
4386
|
await gateway?.close();
|
|
3852
4387
|
}
|
|
4388
|
+
clearStartTimer() {
|
|
4389
|
+
if (this.startTimer === void 0) return;
|
|
4390
|
+
clearTimeout(this.startTimer);
|
|
4391
|
+
this.startTimer = void 0;
|
|
4392
|
+
}
|
|
3853
4393
|
};
|
|
3854
4394
|
/** Locate the current platform's bundled Funnel executable, with one local development override. */
|
|
3855
4395
|
function funnelExecutable(importMetaUrl, environment = process.env) {
|
|
@@ -4789,8 +5329,16 @@ async function materializeManagedSetup(setup, table) {
|
|
|
4789
5329
|
//#region src/plugin.ts
|
|
4790
5330
|
/** Stable Cordis plugin name. */
|
|
4791
5331
|
const name = "dsh-mobile";
|
|
4792
|
-
/** The stock WebServer serves the control card;
|
|
4793
|
-
const inject = [
|
|
5332
|
+
/** The stock WebServer serves the control card; Connection authenticates the loopback DSH origin. */
|
|
5333
|
+
const inject = [
|
|
5334
|
+
"webServer",
|
|
5335
|
+
"commands",
|
|
5336
|
+
"connection"
|
|
5337
|
+
];
|
|
5338
|
+
function upstreamAuthenticatedUrl(ctx, upstreamOrigin) {
|
|
5339
|
+
const connection = ctx.connection;
|
|
5340
|
+
return typeof connection?.authenticatedUrl === "function" ? connection.authenticatedUrl(upstreamOrigin.origin) : void 0;
|
|
5341
|
+
}
|
|
4794
5342
|
function installedDshVersion() {
|
|
4795
5343
|
const manifest = createRequire(import.meta.url)("@deepseek-ai/dsh-host-webserver/package.json");
|
|
4796
5344
|
if (manifest === null || typeof manifest !== "object") return void 0;
|
|
@@ -4922,10 +5470,12 @@ function remoteControlPayload(provider, status, gateway, providerStatuses, cpola
|
|
|
4922
5470
|
}
|
|
4923
5471
|
/** Mount the resident control route and its optional authenticated LAN gateway. */
|
|
4924
5472
|
async function apply(ctx, config) {
|
|
4925
|
-
|
|
5473
|
+
const dshVersion = installedDshVersion();
|
|
5474
|
+
assertSupportedDshVersion(dshVersion);
|
|
4926
5475
|
const loaded = await loadSetup(config);
|
|
4927
5476
|
const mobileAccess = createMobileAccessService(ctx);
|
|
4928
5477
|
const template = loopbackTemplate(loaded);
|
|
5478
|
+
const upstreamLoginUrl = upstreamAuthenticatedUrl(ctx, template.upstreamOrigin);
|
|
4929
5479
|
const instanceId = await stableInstanceId(loaded, template);
|
|
4930
5480
|
const stateDirectory = dirname(template.stateFile);
|
|
4931
5481
|
const remoteDirectory = join(stateDirectory, "remote");
|
|
@@ -4966,7 +5516,7 @@ async function apply(ctx, config) {
|
|
|
4966
5516
|
let lanGateway;
|
|
4967
5517
|
const startGateway = async (candidateConfig) => {
|
|
4968
5518
|
const resolved = parseGatewayConfig(candidateConfig);
|
|
4969
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess);
|
|
5519
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl);
|
|
4970
5520
|
await candidate.start();
|
|
4971
5521
|
lanGateway = candidate;
|
|
4972
5522
|
return { close: async () => {
|
|
@@ -5006,7 +5556,7 @@ async function apply(ctx, config) {
|
|
|
5006
5556
|
}
|
|
5007
5557
|
const createRemoteGateway = async (publicOrigin, listenPort = 0) => {
|
|
5008
5558
|
const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, instanceId, listenPort);
|
|
5009
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess);
|
|
5559
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl);
|
|
5010
5560
|
await candidate.start();
|
|
5011
5561
|
return candidate;
|
|
5012
5562
|
};
|
|
@@ -5054,6 +5604,39 @@ async function apply(ctx, config) {
|
|
|
5054
5604
|
origin: lanGateway?.address().origin,
|
|
5055
5605
|
...lanGateway === void 0 ? {} : { extensions: lanGateway.extensionStatus() }
|
|
5056
5606
|
});
|
|
5607
|
+
const diagnosticsPayload = async () => {
|
|
5608
|
+
let interfaceName;
|
|
5609
|
+
let networkError;
|
|
5610
|
+
if (loaded.kind === "managed") try {
|
|
5611
|
+
interfaceName = selectLanNetwork(void 0, loaded.setup.networkInterface).name;
|
|
5612
|
+
} catch {
|
|
5613
|
+
networkError = "network_interface_unavailable";
|
|
5614
|
+
}
|
|
5615
|
+
const remote = remoteController().status();
|
|
5616
|
+
return collectConnectionDiagnostics({
|
|
5617
|
+
dshVersion,
|
|
5618
|
+
lan: {
|
|
5619
|
+
running: lanController.isRunning(),
|
|
5620
|
+
...lanGateway === void 0 ? {} : {
|
|
5621
|
+
origin: lanGateway.address().origin,
|
|
5622
|
+
port: lanGateway.address().port
|
|
5623
|
+
},
|
|
5624
|
+
...loaded.kind === "managed" ? {
|
|
5625
|
+
configuredInterface: loaded.setup.networkInterface,
|
|
5626
|
+
port: loaded.setup.listenPort
|
|
5627
|
+
} : {},
|
|
5628
|
+
...interfaceName === void 0 ? {} : { interfaceName },
|
|
5629
|
+
...networkError === void 0 ? {} : { networkError }
|
|
5630
|
+
},
|
|
5631
|
+
remote: {
|
|
5632
|
+
provider: remoteProvider,
|
|
5633
|
+
running: remote.enabled,
|
|
5634
|
+
state: remote.state,
|
|
5635
|
+
...remote.origin === void 0 ? {} : { origin: remote.origin },
|
|
5636
|
+
...remote.errorCode === void 0 ? {} : { errorCode: remote.errorCode }
|
|
5637
|
+
}
|
|
5638
|
+
});
|
|
5639
|
+
};
|
|
5057
5640
|
const adminRoute = {
|
|
5058
5641
|
kind: "prefix",
|
|
5059
5642
|
path: LOCAL_ADMIN_PREFIX,
|
|
@@ -5067,6 +5650,10 @@ async function apply(ctx, config) {
|
|
|
5067
5650
|
sendJson(response, 200, lanPayload(), false);
|
|
5068
5651
|
return;
|
|
5069
5652
|
}
|
|
5653
|
+
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/diagnostics`) {
|
|
5654
|
+
sendJson(response, 200, await diagnosticsPayload(), false);
|
|
5655
|
+
return;
|
|
5656
|
+
}
|
|
5070
5657
|
if (request.method === "POST" && lanControl) {
|
|
5071
5658
|
const body = await readJsonObject(request, 4096);
|
|
5072
5659
|
if (typeof body.running !== "boolean") throw new HttpError(400, "bad_request");
|