dsh-mobile 0.6.10 → 0.6.12
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 +25 -0
- package/CONTRIBUTORS.md +24 -6
- package/README.en.md +22 -19
- package/README.md +22 -19
- package/SECURITY.md +9 -0
- package/apps/mobile/README.md +71 -90
- package/apps/mobile/README.zh-CN.md +131 -0
- package/apps/mobile/release.json +3 -3
- package/docs/ATTACH_EXISTING_FRPS.en.md +1 -1
- package/docs/ATTACH_EXISTING_FRPS.md +1 -1
- package/docs/CADDY_MANAGED.en.md +7 -7
- package/docs/CADDY_MANAGED.md +7 -7
- package/docs/CLIENT_MODULES.en.md +2 -0
- package/docs/CLIENT_MODULES.md +2 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +1 -1
- package/docs/CLOUDFLARE_TUNNEL.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.en.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.md +1 -1
- package/docs/DSH_0.1.5_LAN.md +1 -1
- package/docs/EXTENSION_WORKERS.en.md +2 -2
- package/docs/EXTENSION_WORKERS.md +2 -2
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +10 -4
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +10 -4
- package/docs/README.md +4 -2
- package/docs/SELF_HOSTED_FRP.en.md +2 -2
- package/docs/SELF_HOSTED_FRP.md +2 -2
- package/docs/SELF_HOSTED_ORIGIN.en.md +3 -1
- package/docs/SELF_HOSTED_ORIGIN.md +3 -1
- package/docs/SKIN_COMPATIBILITY.en.md +1 -0
- package/docs/SKIN_COMPATIBILITY.md +1 -0
- package/docs/SLOW_CONNECTIONS.en.md +12 -0
- package/docs/SLOW_CONNECTIONS.md +12 -0
- package/lib/client.js +234 -37
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +34 -3
- package/lib/index.mjs +402 -209
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +5 -1
- package/lib/mobile-compat.js.map +1 -1
- package/package.json +6 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -64,6 +64,10 @@ var BoundedRateLimiter = class {
|
|
|
64
64
|
current.count += 1;
|
|
65
65
|
return true;
|
|
66
66
|
}
|
|
67
|
+
/** Discard entries that no longer identify an eligible key in a trusted, finite key set. */
|
|
68
|
+
retainKeys(eligible) {
|
|
69
|
+
for (const key of this.buckets.keys()) if (!eligible.has(key)) this.buckets.delete(key);
|
|
70
|
+
}
|
|
67
71
|
/** Current table size, exposed for bounded-state assertions. */
|
|
68
72
|
get size() {
|
|
69
73
|
return this.buckets.size;
|
|
@@ -104,6 +108,7 @@ var AccessController = class {
|
|
|
104
108
|
pairLimiter;
|
|
105
109
|
devices = [];
|
|
106
110
|
pairingWindow;
|
|
111
|
+
pairPending = false;
|
|
107
112
|
sessions = /* @__PURE__ */ new Map();
|
|
108
113
|
sessionEndedListeners = /* @__PURE__ */ new Set();
|
|
109
114
|
mutation = Promise.resolve();
|
|
@@ -212,40 +217,52 @@ var AccessController = class {
|
|
|
212
217
|
});
|
|
213
218
|
});
|
|
214
219
|
}
|
|
220
|
+
/** Limit invalid guesses by peer without letting them consume a valid one-time secret's admission. */
|
|
221
|
+
rejectPairingAttempt(sourceKey, now) {
|
|
222
|
+
if (!this.pairLimiter.take(sourceKey, now)) throw new AccessError(429, "rate_limited");
|
|
223
|
+
throw new AccessError(401, "authentication_failed");
|
|
224
|
+
}
|
|
215
225
|
/** Consume the pairing window exactly once and persist only the device-token digest. */
|
|
216
226
|
async pair(sourceKey, token, label) {
|
|
217
227
|
this.requireInitialized();
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (token.length > 512
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
228
|
+
const initialNow = this.now();
|
|
229
|
+
const initialWindow = this.pairingWindow;
|
|
230
|
+
if (token.length > 512 || initialWindow === void 0 || initialWindow.expiresAt <= initialNow || !matchesDigest(token, initialWindow.digest)) this.rejectPairingAttempt(sourceKey, initialNow);
|
|
231
|
+
if (this.pairPending) throw new AccessError(429, "rate_limited");
|
|
232
|
+
this.pairPending = true;
|
|
233
|
+
try {
|
|
234
|
+
return await this.exclusive(async () => {
|
|
235
|
+
const now = this.now();
|
|
236
|
+
const window = this.pairingWindow;
|
|
237
|
+
if (window === void 0 || window.expiresAt <= now || !matchesDigest(token, window.digest)) {
|
|
238
|
+
if (window !== void 0 && window.expiresAt <= now) this.pairingWindow = void 0;
|
|
239
|
+
this.rejectPairingAttempt(sourceKey, now);
|
|
240
|
+
}
|
|
241
|
+
this.pairingWindow = void 0;
|
|
242
|
+
if (this.devices.filter((device) => device.revokedAt === void 0 && device.expiresAt > now).length >= this.options.maxDevices) throw new AccessError(409, "device_limit");
|
|
243
|
+
const deviceToken = opaqueToken();
|
|
244
|
+
const device = Object.freeze({
|
|
245
|
+
id: randomBytes(16).toString("hex"),
|
|
246
|
+
label: normalizeLabel(label),
|
|
247
|
+
tokenDigest: digestHex(deviceToken),
|
|
248
|
+
createdAt: now,
|
|
249
|
+
expiresAt: now + this.options.deviceTtlMs,
|
|
250
|
+
lastSeenAt: now,
|
|
251
|
+
...this.options.pairingInstanceId === void 0 ? {} : { nativeInstanceId: this.options.pairingInstanceId }
|
|
252
|
+
});
|
|
253
|
+
const next = [...this.devices.filter((candidate) => candidate.revokedAt === void 0 && candidate.expiresAt > now), device];
|
|
254
|
+
await this.store.save(this.snapshot(next));
|
|
255
|
+
this.devices = next;
|
|
256
|
+
const session = this.createSession(device, now);
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
...session,
|
|
259
|
+
deviceToken,
|
|
260
|
+
deviceExpiresAt: device.expiresAt
|
|
261
|
+
});
|
|
247
262
|
});
|
|
248
|
-
}
|
|
263
|
+
} finally {
|
|
264
|
+
this.pairPending = false;
|
|
265
|
+
}
|
|
249
266
|
}
|
|
250
267
|
/** Exchange a valid persistent device credential for a new short Session. */
|
|
251
268
|
async renew(deviceToken) {
|
|
@@ -1681,88 +1698,6 @@ var MobileAccessGatewayController = class {
|
|
|
1681
1698
|
return run;
|
|
1682
1699
|
}
|
|
1683
1700
|
};
|
|
1684
|
-
//#endregion
|
|
1685
|
-
//#region src/client-module-recovery.ts
|
|
1686
|
-
const CLIENT_MODULE_RECOVERY_COPY = {
|
|
1687
|
-
zh: {
|
|
1688
|
-
title: "移动页面模块需要调整",
|
|
1689
|
-
explanation: "电脑上的插件或依赖关系已变化,之前保存的模块选择无法加载当前页面。",
|
|
1690
|
-
scope: "可为本设备加载全部模块并重新打开 DSH。不会修改电脑默认设置、其他设备或已安装的插件。",
|
|
1691
|
-
action: "为本设备加载全部模块并重试",
|
|
1692
|
-
working: "正在更新本设备的模块选择…",
|
|
1693
|
-
failed: "未能恢复。请确认电脑和远程通道仍在线,然后重试。"
|
|
1694
|
-
},
|
|
1695
|
-
en: {
|
|
1696
|
-
title: "Mobile page modules need attention",
|
|
1697
|
-
explanation: "The computer’s plugins or dependencies have changed. The saved module selection can no longer load this page.",
|
|
1698
|
-
scope: "Load all modules for this device and reopen DSH. Computer defaults, other devices, and installed plugins are unchanged.",
|
|
1699
|
-
action: "Load all modules for this device and retry",
|
|
1700
|
-
working: "Updating this device’s module selection…",
|
|
1701
|
-
failed: "Could not restore the page. Check that the computer and remote connection are online, then retry."
|
|
1702
|
-
},
|
|
1703
|
-
it: {
|
|
1704
|
-
title: "I moduli della pagina mobile richiedono attenzione",
|
|
1705
|
-
explanation: "I plugin o le dipendenze del computer sono cambiati. La selezione salvata non può più caricare questa pagina.",
|
|
1706
|
-
scope: "Carica tutti i moduli per questo dispositivo e riapri DSH. I predefiniti del computer, gli altri dispositivi e i plugin installati non cambiano.",
|
|
1707
|
-
action: "Carica tutti i moduli per questo dispositivo e riprova",
|
|
1708
|
-
working: "Aggiornamento dei moduli per questo dispositivo…",
|
|
1709
|
-
failed: "Ripristino non riuscito. Verifica che il computer e la connessione remota siano online, poi riprova."
|
|
1710
|
-
}
|
|
1711
|
-
};
|
|
1712
|
-
/** Render an explicit device-only recovery action without interpolating graph diagnostics or credentials.
|
|
1713
|
-
* @param locale - Negotiated browser display language.
|
|
1714
|
-
* @returns An owned HTML page which changes no selection until the user activates its button.
|
|
1715
|
-
*/
|
|
1716
|
-
function renderClientModuleRecoveryPage(locale) {
|
|
1717
|
-
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
1718
|
-
return `<!doctype html>
|
|
1719
|
-
<html lang="${locale === "zh" ? "zh-CN" : locale}">
|
|
1720
|
-
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
1721
|
-
<meta name="color-scheme" content="light dark"><title>${copy.title}</title>
|
|
1722
|
-
<style>
|
|
1723
|
-
body{margin:0;background:Canvas;color:CanvasText;font:16px/1.5 system-ui,sans-serif}
|
|
1724
|
-
main{box-sizing:border-box;max-width:640px;margin:32px auto;padding:24px max(24px,env(safe-area-inset-left)) 24px max(24px,env(safe-area-inset-right));overflow-wrap:anywhere}
|
|
1725
|
-
h1{font-size:24px;line-height:1.3}button{min-height:48px;max-width:100%;padding:12px 20px;border:1px solid ButtonText;border-radius:12px;background:ButtonFace;color:ButtonText;font:inherit;cursor:pointer}
|
|
1726
|
-
button:focus-visible{outline:2px solid Highlight;outline-offset:3px}button:disabled{opacity:.6;cursor:wait}#recovery-status{min-height:1.5em}
|
|
1727
|
-
</style></head>
|
|
1728
|
-
<body><main><h1>${copy.title}</h1><p>${copy.explanation}</p><p>${copy.scope}</p>
|
|
1729
|
-
<button id="load-all-modules" type="button">${copy.action}</button><p id="recovery-status" role="status" aria-live="polite"></p>
|
|
1730
|
-
</main><script>${renderClientModuleRecoveryScript(locale)}<\/script></body></html>
|
|
1731
|
-
`;
|
|
1732
|
-
}
|
|
1733
|
-
/** Render the fixed same-origin action; CSRF values are read from the active browser cookie only at user activation.
|
|
1734
|
-
* @param locale - Negotiated browser display language.
|
|
1735
|
-
* @returns JavaScript that preserves selections on failure and reloads only after an authorized save succeeds.
|
|
1736
|
-
*/
|
|
1737
|
-
function renderClientModuleRecoveryScript(locale) {
|
|
1738
|
-
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
1739
|
-
return `(() => {
|
|
1740
|
-
const button = document.getElementById('load-all-modules')
|
|
1741
|
-
const status = document.getElementById('recovery-status')
|
|
1742
|
-
button.addEventListener('click', async () => {
|
|
1743
|
-
if (button.disabled) return
|
|
1744
|
-
button.disabled = true
|
|
1745
|
-
status.textContent = ${JSON.stringify(copy.working)}
|
|
1746
|
-
try {
|
|
1747
|
-
const values = document.cookie.split(';').map(value => value.trim())
|
|
1748
|
-
const host = values.filter(value => value.startsWith('__Host-dsh_ma_csrf='))
|
|
1749
|
-
const candidates = host.length === 0 ? values.filter(value => value.startsWith('dsh_ma_csrf=')) : host
|
|
1750
|
-
if (candidates.length !== 1) throw new Error('csrf_unavailable')
|
|
1751
|
-
const csrf = candidates[0].slice(candidates[0].indexOf('=') + 1)
|
|
1752
|
-
const response = await fetch('/mobile-access/client-modules', {
|
|
1753
|
-
method: 'POST', credentials: 'same-origin', redirect: 'error', cache: 'no-store',
|
|
1754
|
-
headers: { 'content-type': 'application/json', 'x-dsh-mobile-csrf': csrf },
|
|
1755
|
-
body: JSON.stringify({ excludedClientModules: [] }),
|
|
1756
|
-
})
|
|
1757
|
-
if (!response.ok) throw new Error('recovery_failed')
|
|
1758
|
-
location.reload()
|
|
1759
|
-
} catch {
|
|
1760
|
-
status.textContent = ${JSON.stringify(copy.failed)}
|
|
1761
|
-
button.disabled = false
|
|
1762
|
-
}
|
|
1763
|
-
})
|
|
1764
|
-
})()`;
|
|
1765
|
-
}
|
|
1766
1701
|
function unwrapBrackets(hostname) {
|
|
1767
1702
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1768
1703
|
}
|
|
@@ -1979,6 +1914,256 @@ function assertLocalAdminTrust(request, requireBrowserOrigin, authenticateDeskto
|
|
|
1979
1914
|
if (requireBrowserOrigin && site !== void 0 && site !== "same-origin") throw new HttpError(403, "forbidden");
|
|
1980
1915
|
}
|
|
1981
1916
|
//#endregion
|
|
1917
|
+
//#region src/browser-auth-cookies.ts
|
|
1918
|
+
const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
|
|
1919
|
+
const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
|
|
1920
|
+
/** Non-secret browser identity; the persistent device credential keeps its narrow renewal path. */
|
|
1921
|
+
const HOST_DEVICE_COOKIE = "__Host-dsh_ma_device_id";
|
|
1922
|
+
const AUTH_COOKIE_NAMES = /* @__PURE__ */ new Set([
|
|
1923
|
+
SESSION_COOKIE,
|
|
1924
|
+
DEVICE_COOKIE,
|
|
1925
|
+
CSRF_COOKIE,
|
|
1926
|
+
HOST_SESSION_COOKIE,
|
|
1927
|
+
HOST_CSRF_COOKIE,
|
|
1928
|
+
HOST_DEVICE_COOKIE
|
|
1929
|
+
]);
|
|
1930
|
+
/** Read only gateway authentication cookies, retaining distinct values from different scopes. */
|
|
1931
|
+
function browserAuthCookies(header) {
|
|
1932
|
+
if (header === void 0) return /* @__PURE__ */ new Map();
|
|
1933
|
+
if (header.length > 8192) throw new HttpError(401, "authentication_failed");
|
|
1934
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
1935
|
+
for (const part of header.split(";")) {
|
|
1936
|
+
const equals = part.indexOf("=");
|
|
1937
|
+
if (equals <= 0) continue;
|
|
1938
|
+
const name = part.slice(0, equals).trim();
|
|
1939
|
+
if (!AUTH_COOKIE_NAMES.has(name)) continue;
|
|
1940
|
+
const values = cookies.get(name) ?? [];
|
|
1941
|
+
cookies.set(name, values);
|
|
1942
|
+
const value = part.slice(equals + 1).trim();
|
|
1943
|
+
if (!/^[\w\-.~+/=]{1,512}$/u.test(value) || values.includes(value)) continue;
|
|
1944
|
+
values.push(value);
|
|
1945
|
+
}
|
|
1946
|
+
return cookies;
|
|
1947
|
+
}
|
|
1948
|
+
/** Select exactly one authenticated value; never let cookie order choose between valid identities. */
|
|
1949
|
+
function authenticatedCookie(values, authenticate, accepts = () => true) {
|
|
1950
|
+
let selected;
|
|
1951
|
+
for (const value of values) {
|
|
1952
|
+
let result;
|
|
1953
|
+
try {
|
|
1954
|
+
result = authenticate(value);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
if (error instanceof AccessError && error.status === 401) continue;
|
|
1957
|
+
throw error;
|
|
1958
|
+
}
|
|
1959
|
+
if (!accepts(result)) continue;
|
|
1960
|
+
if (selected !== void 0) throw new HttpError(401, "authentication_failed");
|
|
1961
|
+
selected = {
|
|
1962
|
+
value,
|
|
1963
|
+
result
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
if (selected === void 0) throw new HttpError(401, "authentication_failed");
|
|
1967
|
+
return selected;
|
|
1968
|
+
}
|
|
1969
|
+
//#endregion
|
|
1970
|
+
//#region src/auth-failure-log.ts
|
|
1971
|
+
const LOG_INTERVAL_MS = 3e4;
|
|
1972
|
+
const MAX_LOGGED_ORIGIN_LENGTH = 512;
|
|
1973
|
+
function requestPathCategory(path) {
|
|
1974
|
+
switch (path) {
|
|
1975
|
+
case "/":
|
|
1976
|
+
case "/index.html":
|
|
1977
|
+
case "/api/session/prompt":
|
|
1978
|
+
case "/api/events.mux":
|
|
1979
|
+
case "/api/events.host":
|
|
1980
|
+
case "/api/remote.mux":
|
|
1981
|
+
case "/mobile-access/compat.js":
|
|
1982
|
+
case "/mobile-access/mobile-layout.js":
|
|
1983
|
+
case "/mobile-access/custom.js":
|
|
1984
|
+
case "/mobile-access/custom.css":
|
|
1985
|
+
case "/mobile-access/client-modules":
|
|
1986
|
+
case "/mobile-access/events": return path;
|
|
1987
|
+
default:
|
|
1988
|
+
if (path.startsWith("/api/")) return "/api/*";
|
|
1989
|
+
if (path.startsWith("/mobile-access/extensions/")) return "/mobile-access/extensions/*";
|
|
1990
|
+
if (path.startsWith("/mobile-access/")) return "/mobile-access/*";
|
|
1991
|
+
return "/*";
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
function requestMethod(method) {
|
|
1995
|
+
switch (method) {
|
|
1996
|
+
case "GET":
|
|
1997
|
+
case "HEAD":
|
|
1998
|
+
case "POST":
|
|
1999
|
+
case "PUT":
|
|
2000
|
+
case "PATCH":
|
|
2001
|
+
case "DELETE":
|
|
2002
|
+
case "OPTIONS": return method;
|
|
2003
|
+
default: return "OTHER";
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function requestEntry(kind, status, request, path, canonicalOrigin, authorization) {
|
|
2007
|
+
let sessionCookie = false;
|
|
2008
|
+
let csrfCookies = 0;
|
|
2009
|
+
for (const cookie of (request.headers.cookie ?? "").split(";")) {
|
|
2010
|
+
const equals = cookie.indexOf("=");
|
|
2011
|
+
if (equals < 0) continue;
|
|
2012
|
+
const name = cookie.slice(0, equals).trim();
|
|
2013
|
+
if (name === "dsh_ma_session" || name === "__Host-dsh_ma_session") sessionCookie = true;
|
|
2014
|
+
if (name === "dsh_ma_csrf" || name === "__Host-dsh_ma_csrf") csrfCookies += 1;
|
|
2015
|
+
}
|
|
2016
|
+
const site = request.headers["sec-fetch-site"];
|
|
2017
|
+
return {
|
|
2018
|
+
kind,
|
|
2019
|
+
status,
|
|
2020
|
+
method: requestMethod(request.method),
|
|
2021
|
+
path: requestPathCategory(path),
|
|
2022
|
+
sessionCookie,
|
|
2023
|
+
csrfHeader: request.headers[CSRF_HEADER] !== void 0,
|
|
2024
|
+
csrfCookies,
|
|
2025
|
+
...canonicalOrigin === void 0 || canonicalOrigin.length > MAX_LOGGED_ORIGIN_LENGTH ? {} : { origin: canonicalOrigin },
|
|
2026
|
+
...site === "same-origin" || site === "same-site" || site === "cross-site" || site === "none" ? { site } : {},
|
|
2027
|
+
...authorization === void 0 ? {} : { device: authorization.deviceId.slice(0, 8) }
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Gateway-owned diagnostic sampling with two fixed slots and no timer or attacker-selected key.
|
|
2032
|
+
* Each kind records immediately, then at most once per 30 seconds. Suppressed counts accompany
|
|
2033
|
+
* its next recorded request; no credential, request body, arbitrary path or query is retained.
|
|
2034
|
+
* The caller supplies the Origin already normalized by its successful request-trust check.
|
|
2035
|
+
*/
|
|
2036
|
+
var AuthFailureRecorder = class {
|
|
2037
|
+
observe;
|
|
2038
|
+
now;
|
|
2039
|
+
reportObserverFailure;
|
|
2040
|
+
windows = {
|
|
2041
|
+
authorize: {
|
|
2042
|
+
nextLogAt: 0,
|
|
2043
|
+
suppressed: 0
|
|
2044
|
+
},
|
|
2045
|
+
csrf: {
|
|
2046
|
+
nextLogAt: 0,
|
|
2047
|
+
suppressed: 0
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
observerFailureReported = false;
|
|
2051
|
+
constructor(observe, now = () => performance.now(), reportObserverFailure = () => {
|
|
2052
|
+
process.emitWarning("Mobile access authentication diagnostics could not be recorded", { code: "DSH_MOBILE_AUTH_LOG_FAILED" });
|
|
2053
|
+
}) {
|
|
2054
|
+
this.observe = observe;
|
|
2055
|
+
this.now = now;
|
|
2056
|
+
this.reportObserverFailure = reportObserverFailure;
|
|
2057
|
+
}
|
|
2058
|
+
/** Record a trusted-request rejection without changing its authentication result or retrying it. */
|
|
2059
|
+
record(kind, status, request, path, canonicalOrigin, authorization) {
|
|
2060
|
+
if (this.observe === void 0) return;
|
|
2061
|
+
const now = this.now();
|
|
2062
|
+
const window = this.windows[kind];
|
|
2063
|
+
if (now < window.nextLogAt) {
|
|
2064
|
+
window.suppressed = Math.min(Number.MAX_SAFE_INTEGER, window.suppressed + 1);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
const suppressed = window.suppressed;
|
|
2068
|
+
window.nextLogAt = now + LOG_INTERVAL_MS;
|
|
2069
|
+
window.suppressed = 0;
|
|
2070
|
+
try {
|
|
2071
|
+
this.observe({
|
|
2072
|
+
...requestEntry(kind, status, request, path, canonicalOrigin, authorization),
|
|
2073
|
+
...suppressed === 0 ? {} : { suppressed }
|
|
2074
|
+
});
|
|
2075
|
+
} catch (observerError) {
|
|
2076
|
+
if (this.observerFailureReported) return;
|
|
2077
|
+
this.observerFailureReported = true;
|
|
2078
|
+
try {
|
|
2079
|
+
this.reportObserverFailure();
|
|
2080
|
+
} catch (reporterError) {}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
//#endregion
|
|
2085
|
+
//#region src/client-module-recovery.ts
|
|
2086
|
+
const CLIENT_MODULE_RECOVERY_COPY = {
|
|
2087
|
+
zh: {
|
|
2088
|
+
title: "移动页面模块需要调整",
|
|
2089
|
+
explanation: "电脑上的插件或依赖关系已变化,之前保存的模块选择无法加载当前页面。",
|
|
2090
|
+
scope: "可为本设备加载全部模块并重新打开 DSH。不会修改电脑默认设置、其他设备或已安装的插件。",
|
|
2091
|
+
action: "为本设备加载全部模块并重试",
|
|
2092
|
+
working: "正在更新本设备的模块选择…",
|
|
2093
|
+
failed: "未能恢复。请确认电脑和远程通道仍在线,然后重试。"
|
|
2094
|
+
},
|
|
2095
|
+
en: {
|
|
2096
|
+
title: "Mobile page modules need attention",
|
|
2097
|
+
explanation: "The computer’s plugins or dependencies have changed. The saved module selection can no longer load this page.",
|
|
2098
|
+
scope: "Load all modules for this device and reopen DSH. Computer defaults, other devices, and installed plugins are unchanged.",
|
|
2099
|
+
action: "Load all modules for this device and retry",
|
|
2100
|
+
working: "Updating this device’s module selection…",
|
|
2101
|
+
failed: "Could not restore the page. Check that the computer and remote connection are online, then retry."
|
|
2102
|
+
},
|
|
2103
|
+
it: {
|
|
2104
|
+
title: "I moduli della pagina mobile richiedono attenzione",
|
|
2105
|
+
explanation: "I plugin o le dipendenze del computer sono cambiati. La selezione salvata non può più caricare questa pagina.",
|
|
2106
|
+
scope: "Carica tutti i moduli per questo dispositivo e riapri DSH. I predefiniti del computer, gli altri dispositivi e i plugin installati non cambiano.",
|
|
2107
|
+
action: "Carica tutti i moduli per questo dispositivo e riprova",
|
|
2108
|
+
working: "Aggiornamento dei moduli per questo dispositivo…",
|
|
2109
|
+
failed: "Ripristino non riuscito. Verifica che il computer e la connessione remota siano online, poi riprova."
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
/** Render an explicit device-only recovery action without interpolating graph diagnostics or credentials.
|
|
2113
|
+
* @param locale - Negotiated browser display language.
|
|
2114
|
+
* @returns An owned HTML page which changes no selection until the user activates its button.
|
|
2115
|
+
*/
|
|
2116
|
+
function renderClientModuleRecoveryPage(locale) {
|
|
2117
|
+
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
2118
|
+
return `<!doctype html>
|
|
2119
|
+
<html lang="${locale === "zh" ? "zh-CN" : locale}">
|
|
2120
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
2121
|
+
<meta name="color-scheme" content="light dark"><title>${copy.title}</title>
|
|
2122
|
+
<style>
|
|
2123
|
+
body{margin:0;background:Canvas;color:CanvasText;font:16px/1.5 system-ui,sans-serif}
|
|
2124
|
+
main{box-sizing:border-box;max-width:640px;margin:32px auto;padding:24px max(24px,env(safe-area-inset-left)) 24px max(24px,env(safe-area-inset-right));overflow-wrap:anywhere}
|
|
2125
|
+
h1{font-size:24px;line-height:1.3}button{min-height:48px;max-width:100%;padding:12px 20px;border:1px solid ButtonText;border-radius:12px;background:ButtonFace;color:ButtonText;font:inherit;cursor:pointer}
|
|
2126
|
+
button:focus-visible{outline:2px solid Highlight;outline-offset:3px}button:disabled{opacity:.6;cursor:wait}#recovery-status{min-height:1.5em}
|
|
2127
|
+
</style></head>
|
|
2128
|
+
<body><main><h1>${copy.title}</h1><p>${copy.explanation}</p><p>${copy.scope}</p>
|
|
2129
|
+
<button id="load-all-modules" type="button">${copy.action}</button><p id="recovery-status" role="status" aria-live="polite"></p>
|
|
2130
|
+
</main><script>${renderClientModuleRecoveryScript(locale)}<\/script></body></html>
|
|
2131
|
+
`;
|
|
2132
|
+
}
|
|
2133
|
+
/** Render the fixed same-origin action; CSRF values are read from the active browser cookie only at user activation.
|
|
2134
|
+
* @param locale - Negotiated browser display language.
|
|
2135
|
+
* @returns JavaScript that preserves selections on failure and reloads only after an authorized save succeeds.
|
|
2136
|
+
*/
|
|
2137
|
+
function renderClientModuleRecoveryScript(locale) {
|
|
2138
|
+
const copy = CLIENT_MODULE_RECOVERY_COPY[locale];
|
|
2139
|
+
return `(() => {
|
|
2140
|
+
const button = document.getElementById('load-all-modules')
|
|
2141
|
+
const status = document.getElementById('recovery-status')
|
|
2142
|
+
button.addEventListener('click', async () => {
|
|
2143
|
+
if (button.disabled) return
|
|
2144
|
+
button.disabled = true
|
|
2145
|
+
status.textContent = ${JSON.stringify(copy.working)}
|
|
2146
|
+
try {
|
|
2147
|
+
const values = document.cookie.split(';').map(value => value.trim())
|
|
2148
|
+
const host = values.filter(value => value.startsWith('__Host-dsh_ma_csrf='))
|
|
2149
|
+
const candidates = host.length === 0 ? values.filter(value => value.startsWith('dsh_ma_csrf=')) : host
|
|
2150
|
+
if (candidates.length !== 1) throw new Error('csrf_unavailable')
|
|
2151
|
+
const csrf = candidates[0].slice(candidates[0].indexOf('=') + 1)
|
|
2152
|
+
const response = await fetch('/mobile-access/client-modules', {
|
|
2153
|
+
method: 'POST', credentials: 'same-origin', redirect: 'error', cache: 'no-store',
|
|
2154
|
+
headers: { 'content-type': 'application/json', 'x-dsh-mobile-csrf': csrf },
|
|
2155
|
+
body: JSON.stringify({ excludedClientModules: [] }),
|
|
2156
|
+
})
|
|
2157
|
+
if (!response.ok) throw new Error('recovery_failed')
|
|
2158
|
+
location.reload()
|
|
2159
|
+
} catch {
|
|
2160
|
+
status.textContent = ${JSON.stringify(copy.failed)}
|
|
2161
|
+
button.disabled = false
|
|
2162
|
+
}
|
|
2163
|
+
})
|
|
2164
|
+
})()`;
|
|
2165
|
+
}
|
|
2166
|
+
//#endregion
|
|
1982
2167
|
//#region src/mobile-compat-bootstrap.ts
|
|
1983
2168
|
const MOBILE_COMPAT_PATH = `${AUTH_PREFIX}/compat.js`;
|
|
1984
2169
|
/**
|
|
@@ -2076,59 +2261,6 @@ function weakenEntityTag(etag) {
|
|
|
2076
2261
|
return opaque === void 0 ? void 0 : `W/${opaque}`;
|
|
2077
2262
|
}
|
|
2078
2263
|
//#endregion
|
|
2079
|
-
//#region src/browser-auth-cookies.ts
|
|
2080
|
-
const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
|
|
2081
|
-
const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
|
|
2082
|
-
/** Non-secret browser identity; the persistent device credential keeps its narrow renewal path. */
|
|
2083
|
-
const HOST_DEVICE_COOKIE = "__Host-dsh_ma_device_id";
|
|
2084
|
-
const AUTH_COOKIE_NAMES = /* @__PURE__ */ new Set([
|
|
2085
|
-
SESSION_COOKIE,
|
|
2086
|
-
DEVICE_COOKIE,
|
|
2087
|
-
CSRF_COOKIE,
|
|
2088
|
-
HOST_SESSION_COOKIE,
|
|
2089
|
-
HOST_CSRF_COOKIE,
|
|
2090
|
-
HOST_DEVICE_COOKIE
|
|
2091
|
-
]);
|
|
2092
|
-
/** Read only gateway authentication cookies, retaining distinct values from different scopes. */
|
|
2093
|
-
function browserAuthCookies(header) {
|
|
2094
|
-
if (header === void 0) return /* @__PURE__ */ new Map();
|
|
2095
|
-
if (header.length > 8192) throw new HttpError(401, "authentication_failed");
|
|
2096
|
-
const cookies = /* @__PURE__ */ new Map();
|
|
2097
|
-
for (const part of header.split(";")) {
|
|
2098
|
-
const equals = part.indexOf("=");
|
|
2099
|
-
if (equals <= 0) continue;
|
|
2100
|
-
const name = part.slice(0, equals).trim();
|
|
2101
|
-
if (!AUTH_COOKIE_NAMES.has(name)) continue;
|
|
2102
|
-
const values = cookies.get(name) ?? [];
|
|
2103
|
-
cookies.set(name, values);
|
|
2104
|
-
const value = part.slice(equals + 1).trim();
|
|
2105
|
-
if (!/^[\w\-.~+/=]{1,512}$/u.test(value) || values.includes(value)) continue;
|
|
2106
|
-
values.push(value);
|
|
2107
|
-
}
|
|
2108
|
-
return cookies;
|
|
2109
|
-
}
|
|
2110
|
-
/** Select exactly one authenticated value; never let cookie order choose between valid identities. */
|
|
2111
|
-
function authenticatedCookie(values, authenticate, accepts = () => true) {
|
|
2112
|
-
let selected;
|
|
2113
|
-
for (const value of values) {
|
|
2114
|
-
let result;
|
|
2115
|
-
try {
|
|
2116
|
-
result = authenticate(value);
|
|
2117
|
-
} catch (error) {
|
|
2118
|
-
if (error instanceof AccessError && error.status === 401) continue;
|
|
2119
|
-
throw error;
|
|
2120
|
-
}
|
|
2121
|
-
if (!accepts(result)) continue;
|
|
2122
|
-
if (selected !== void 0) throw new HttpError(401, "authentication_failed");
|
|
2123
|
-
selected = {
|
|
2124
|
-
value,
|
|
2125
|
-
result
|
|
2126
|
-
};
|
|
2127
|
-
}
|
|
2128
|
-
if (selected === void 0) throw new HttpError(401, "authentication_failed");
|
|
2129
|
-
return selected;
|
|
2130
|
-
}
|
|
2131
|
-
//#endregion
|
|
2132
2264
|
//#region src/websocket-compression.ts
|
|
2133
2265
|
function payloadBytes(data) {
|
|
2134
2266
|
if (Array.isArray(data)) return data.reduce((bytes, part) => bytes + part.byteLength, 0);
|
|
@@ -5280,19 +5412,25 @@ var MobileAccessGateway = class {
|
|
|
5280
5412
|
closeTask;
|
|
5281
5413
|
removeSessionListener;
|
|
5282
5414
|
removeExtensionContentListener;
|
|
5283
|
-
|
|
5284
|
-
|
|
5415
|
+
renewFailureLimiter;
|
|
5416
|
+
renewDeviceLimiter;
|
|
5417
|
+
renewInFlight = /* @__PURE__ */ new Set();
|
|
5418
|
+
probeFailureLimiter;
|
|
5419
|
+
probeDeviceLimiter;
|
|
5420
|
+
probeInFlight = /* @__PURE__ */ new Set();
|
|
5285
5421
|
clientModulePreferences;
|
|
5286
5422
|
clientModuleOperations = /* @__PURE__ */ new Set();
|
|
5287
5423
|
clientModuleTasks = /* @__PURE__ */ new Set();
|
|
5288
5424
|
clientModuleRevocations = /* @__PURE__ */ new Set();
|
|
5289
|
-
|
|
5425
|
+
authFailures;
|
|
5426
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded, clientModulePreferences, authFailureLog) {
|
|
5290
5427
|
this.config = config;
|
|
5291
5428
|
this.extensions = extensions;
|
|
5292
5429
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
5293
5430
|
this.extraWebSocketPaths = extraWebSocketPaths;
|
|
5294
5431
|
this.blockedUpgradeLog = blockedUpgradeLog;
|
|
5295
5432
|
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
5433
|
+
this.authFailures = new AuthFailureRecorder(authFailureLog);
|
|
5296
5434
|
this.clientModulePreferences = clientModulePreferences ?? new ClientModulePreferenceStore(join(dirname(config.stateFile), `${basename(config.stateFile)}.client-modules.json`), config.excludedClientModules);
|
|
5297
5435
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
5298
5436
|
this.tlsEnabled = config.publicTls;
|
|
@@ -5308,8 +5446,12 @@ var MobileAccessGateway = class {
|
|
|
5308
5446
|
maxPairingAttempts: config.maxPairingAttempts,
|
|
5309
5447
|
maxRateLimitKeys: config.maxRateLimitKeys
|
|
5310
5448
|
});
|
|
5311
|
-
|
|
5312
|
-
|
|
5449
|
+
const renewLimit = Math.min(100, config.maxPairingAttempts * 4);
|
|
5450
|
+
const probeLimit = Math.min(1e3, config.maxDevices * 4);
|
|
5451
|
+
this.renewFailureLimiter = new BoundedRateLimiter(renewLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5452
|
+
this.renewDeviceLimiter = new BoundedRateLimiter(renewLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5453
|
+
this.probeFailureLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5454
|
+
this.probeDeviceLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5313
5455
|
this.removeSessionListener = this.access.onSessionEnded((authorization, reason) => {
|
|
5314
5456
|
if (reason === "revoked") {
|
|
5315
5457
|
const cleanup = this.clientModulePreferences.removeDevice(authorization.deviceId).catch(() => {
|
|
@@ -5637,6 +5779,27 @@ var MobileAccessGateway = class {
|
|
|
5637
5779
|
maxAgeSeconds: (deviceExpiresAt - Date.now()) / 1e3
|
|
5638
5780
|
})]);
|
|
5639
5781
|
}
|
|
5782
|
+
/** Separate malformed or invalid requests from device-authenticated work without trusting forwarded addresses. */
|
|
5783
|
+
async withDeviceRequest(request, failureLimiter, deviceLimiter, inFlight, validate, operation) {
|
|
5784
|
+
let device;
|
|
5785
|
+
try {
|
|
5786
|
+
device = await validate();
|
|
5787
|
+
} catch (error) {
|
|
5788
|
+
if ((error instanceof HttpError || error instanceof AccessError) && error.status >= 400 && error.status < 500 && !failureLimiter.take(request.socket.remoteAddress ?? "unknown", Date.now())) throw new HttpError(429, "rate_limited");
|
|
5789
|
+
throw error;
|
|
5790
|
+
}
|
|
5791
|
+
if (inFlight.has(device.deviceId)) throw new HttpError(429, "rate_limited");
|
|
5792
|
+
const now = Date.now();
|
|
5793
|
+
const activeIds = new Set(this.access.listDevices().filter((candidate) => candidate.revokedAt === void 0 && candidate.expiresAt > now).map((candidate) => candidate.id));
|
|
5794
|
+
deviceLimiter.retainKeys(activeIds);
|
|
5795
|
+
if (!deviceLimiter.take(device.deviceId, now)) throw new HttpError(429, "rate_limited");
|
|
5796
|
+
inFlight.add(device.deviceId);
|
|
5797
|
+
try {
|
|
5798
|
+
return await operation(device);
|
|
5799
|
+
} finally {
|
|
5800
|
+
inFlight.delete(device.deviceId);
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5640
5803
|
async handlePair(request, response) {
|
|
5641
5804
|
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5642
5805
|
if (typeof body.token !== "string" || body.label !== void 0 && typeof body.label !== "string") throw new HttpError(400, "bad_request");
|
|
@@ -5659,21 +5822,28 @@ var MobileAccessGateway = class {
|
|
|
5659
5822
|
}, this.tlsEnabled);
|
|
5660
5823
|
}
|
|
5661
5824
|
async handleRenew(request, response) {
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5825
|
+
await this.withDeviceRequest(request, this.renewFailureLimiter, this.renewDeviceLimiter, this.renewInFlight, async () => {
|
|
5826
|
+
await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5827
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
5828
|
+
const bindings = this.tlsEnabled ? cookies.get(HOST_DEVICE_COOKIE) : void 0;
|
|
5829
|
+
if (bindings !== void 0 && bindings.length !== 1) throw new HttpError(401, "authentication_failed");
|
|
5830
|
+
const device = authenticatedCookie(cookies.get("dsh_ma_device") ?? [], (token) => this.access.authorizeDevice(token), (candidate) => bindings === void 0 || candidate.deviceId === bindings[0]);
|
|
5831
|
+
return {
|
|
5832
|
+
deviceId: device.result.deviceId,
|
|
5833
|
+
token: device.value,
|
|
5834
|
+
credential: device.result
|
|
5835
|
+
};
|
|
5836
|
+
}, async (selected) => {
|
|
5837
|
+
const result = await this.access.renew(selected.token);
|
|
5838
|
+
this.setSessionCookies(response, result, Date.now());
|
|
5839
|
+
this.setBrowserDeviceBinding(response, result.deviceId, selected.credential.deviceExpiresAt);
|
|
5840
|
+
sendJson(response, 200, {
|
|
5841
|
+
renewed: true,
|
|
5842
|
+
deviceId: result.deviceId,
|
|
5843
|
+
csrfToken: result.csrfToken,
|
|
5844
|
+
sessionExpiresAt: result.sessionExpiresAt
|
|
5845
|
+
}, this.tlsEnabled);
|
|
5846
|
+
});
|
|
5677
5847
|
}
|
|
5678
5848
|
/** CA-backed entries retain their pin; public entries project the authenticated credential's identity. */
|
|
5679
5849
|
nativeResponseInstanceId(result) {
|
|
@@ -5694,28 +5864,40 @@ var MobileAccessGateway = class {
|
|
|
5694
5864
|
}, this.tlsEnabled);
|
|
5695
5865
|
}
|
|
5696
5866
|
async handleNativeRenew(request, response) {
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5867
|
+
await this.withDeviceRequest(request, this.renewFailureLimiter, this.renewDeviceLimiter, this.renewInFlight, async () => {
|
|
5868
|
+
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5869
|
+
if (typeof body.deviceToken !== "string") throw new HttpError(400, "bad_request");
|
|
5870
|
+
return {
|
|
5871
|
+
deviceId: this.access.authorizeDevice(body.deviceToken).deviceId,
|
|
5872
|
+
token: body.deviceToken
|
|
5873
|
+
};
|
|
5874
|
+
}, async (selected) => {
|
|
5875
|
+
const result = await this.access.renew(selected.token);
|
|
5876
|
+
sendJson(response, 200, {
|
|
5877
|
+
instanceId: this.nativeResponseInstanceId(result),
|
|
5878
|
+
deviceId: result.deviceId,
|
|
5879
|
+
sessionToken: result.sessionToken,
|
|
5880
|
+
csrfToken: result.csrfToken,
|
|
5881
|
+
sessionExpiresAt: result.sessionExpiresAt
|
|
5882
|
+
}, this.tlsEnabled);
|
|
5883
|
+
});
|
|
5708
5884
|
}
|
|
5709
5885
|
async handleNativeProbe(request, response) {
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5886
|
+
await this.withDeviceRequest(request, this.probeFailureLimiter, this.probeDeviceLimiter, this.probeInFlight, async () => {
|
|
5887
|
+
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5888
|
+
if (typeof body.deviceToken !== "string") throw new HttpError(400, "bad_request");
|
|
5889
|
+
return {
|
|
5890
|
+
deviceId: this.access.authorizeDevice(body.deviceToken).deviceId,
|
|
5891
|
+
token: body.deviceToken
|
|
5892
|
+
};
|
|
5893
|
+
}, async (selected) => {
|
|
5894
|
+
const result = await this.access.probe(selected.token);
|
|
5895
|
+
sendJson(response, 200, {
|
|
5896
|
+
instanceId: this.nativeResponseInstanceId(result),
|
|
5897
|
+
deviceId: result.deviceId,
|
|
5898
|
+
deviceExpiresAt: result.deviceExpiresAt
|
|
5899
|
+
}, this.tlsEnabled);
|
|
5900
|
+
});
|
|
5719
5901
|
}
|
|
5720
5902
|
async handleLogout(request, response) {
|
|
5721
5903
|
await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
@@ -5879,6 +6061,7 @@ var MobileAccessGateway = class {
|
|
|
5879
6061
|
authorization = this.authorize(request);
|
|
5880
6062
|
} catch (error) {
|
|
5881
6063
|
const mapped = mapError(error);
|
|
6064
|
+
this.authFailures.record("authorize", mapped.status, request, target.decodedPathname, policy.canonicalOrigin(request.headers.origin));
|
|
5882
6065
|
const acceptsHtml = request.headers.accept?.split(",").some((value) => value.trim().split(";", 1)[0] === "text/html") ?? false;
|
|
5883
6066
|
const topLevel = request.method === "GET" && acceptsHtml && (request.headers["sec-fetch-dest"] === void 0 || request.headers["sec-fetch-dest"] === "document") && target.decodedPathname !== "/api" && !target.decodedPathname.startsWith("/api/");
|
|
5884
6067
|
if (mapped.status === 401 && topLevel) {
|
|
@@ -5893,7 +6076,12 @@ var MobileAccessGateway = class {
|
|
|
5893
6076
|
}
|
|
5894
6077
|
throw error;
|
|
5895
6078
|
}
|
|
5896
|
-
if (isMutation)
|
|
6079
|
+
if (isMutation) try {
|
|
6080
|
+
this.requireCsrf(request, authorization);
|
|
6081
|
+
} catch (error) {
|
|
6082
|
+
this.authFailures.record("csrf", mapError(error).status, request, target.decodedPathname, policy.canonicalOrigin(request.headers.origin), authorization);
|
|
6083
|
+
throw error;
|
|
6084
|
+
}
|
|
5897
6085
|
if (clientModules) {
|
|
5898
6086
|
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
5899
6087
|
if (request.method !== "GET" && request.method !== "POST") throw new HttpError(405, "method_not_allowed");
|
|
@@ -6267,7 +6455,8 @@ var MobileAccessGateway = class {
|
|
|
6267
6455
|
headers: {
|
|
6268
6456
|
host: this.config.upstreamOrigin.host,
|
|
6269
6457
|
accept: "text/html",
|
|
6270
|
-
"accept-encoding": "identity"
|
|
6458
|
+
"accept-encoding": "identity",
|
|
6459
|
+
"sec-fetch-mode": "same-origin"
|
|
6271
6460
|
},
|
|
6272
6461
|
agent: false
|
|
6273
6462
|
});
|
|
@@ -17410,7 +17599,9 @@ async function apply(ctx, config) {
|
|
|
17410
17599
|
}), frpIdentity, candidateConfig.instanceId !== void 0);
|
|
17411
17600
|
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, (source, code) => {
|
|
17412
17601
|
logger.warn("%s discovery degraded while DSH remains available: %s", source, code);
|
|
17413
|
-
}, clientModulePreferences)
|
|
17602
|
+
}, clientModulePreferences, (entry) => {
|
|
17603
|
+
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17604
|
+
});
|
|
17414
17605
|
await candidate.start();
|
|
17415
17606
|
lanGateway = candidate;
|
|
17416
17607
|
const removeTaskSink = taskEventHub.add(candidate);
|
|
@@ -17466,7 +17657,9 @@ async function apply(ctx, config) {
|
|
|
17466
17657
|
}
|
|
17467
17658
|
const createRemoteGateway = async (publicOrigin, listenPort = 0) => {
|
|
17468
17659
|
const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, remoteInstanceId, listenPort, instanceId);
|
|
17469
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences)
|
|
17660
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences, (entry) => {
|
|
17661
|
+
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17662
|
+
});
|
|
17470
17663
|
await candidate.start();
|
|
17471
17664
|
return candidate;
|
|
17472
17665
|
};
|