dsh-mobile 0.6.11 → 0.6.13
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 +17 -0
- package/CONTRIBUTORS.md +9 -3
- package/README.en.md +8 -4
- package/README.md +8 -4
- package/SECURITY.md +8 -0
- package/apps/mobile/README.md +2 -0
- package/apps/mobile/README.zh-CN.md +2 -0
- package/docs/SELF_HOSTED_ORIGIN.en.md +2 -0
- package/docs/SELF_HOSTED_ORIGIN.md +2 -0
- package/docs/SLOW_CONNECTIONS.en.md +14 -0
- package/docs/SLOW_CONNECTIONS.md +14 -0
- package/lib/index.d.mts +42 -3
- package/lib/index.mjs +292 -80
- 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 +4 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { performance as performance$1 } from "node:perf_hooks";
|
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { createSocket } from "node:dgram";
|
|
11
11
|
import { homedir, hostname, networkInterfaces, tmpdir } from "node:os";
|
|
12
|
-
import { createServer as createServer$1, request } from "node:http";
|
|
12
|
+
import { STATUS_CODES, createServer as createServer$1, request } from "node:http";
|
|
13
13
|
import { createServer as createServer$2, request as request$1 } from "node:https";
|
|
14
14
|
import { Readable, Transform, finished } from "node:stream";
|
|
15
15
|
import { finished as finished$1, pipeline } from "node:stream/promises";
|
|
@@ -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) {
|
|
@@ -1951,7 +1968,7 @@ function authenticatedCookie(values, authenticate, accepts = () => true) {
|
|
|
1951
1968
|
}
|
|
1952
1969
|
//#endregion
|
|
1953
1970
|
//#region src/auth-failure-log.ts
|
|
1954
|
-
const LOG_INTERVAL_MS = 3e4;
|
|
1971
|
+
const LOG_INTERVAL_MS$1 = 3e4;
|
|
1955
1972
|
const MAX_LOGGED_ORIGIN_LENGTH = 512;
|
|
1956
1973
|
function requestPathCategory(path) {
|
|
1957
1974
|
switch (path) {
|
|
@@ -2048,7 +2065,7 @@ var AuthFailureRecorder = class {
|
|
|
2048
2065
|
return;
|
|
2049
2066
|
}
|
|
2050
2067
|
const suppressed = window.suppressed;
|
|
2051
|
-
window.nextLogAt = now + LOG_INTERVAL_MS;
|
|
2068
|
+
window.nextLogAt = now + LOG_INTERVAL_MS$1;
|
|
2052
2069
|
window.suppressed = 0;
|
|
2053
2070
|
try {
|
|
2054
2071
|
this.observe({
|
|
@@ -2065,6 +2082,129 @@ var AuthFailureRecorder = class {
|
|
|
2065
2082
|
}
|
|
2066
2083
|
};
|
|
2067
2084
|
//#endregion
|
|
2085
|
+
//#region src/http-failure-log.ts
|
|
2086
|
+
function safeParserCode(code) {
|
|
2087
|
+
switch (code) {
|
|
2088
|
+
case "ERR_HTTP_REQUEST_TIMEOUT":
|
|
2089
|
+
case "HPE_HEADER_OVERFLOW":
|
|
2090
|
+
case "HPE_INVALID_CHUNK_SIZE":
|
|
2091
|
+
case "HPE_INVALID_CONTENT_LENGTH":
|
|
2092
|
+
case "HPE_INVALID_EOF_STATE":
|
|
2093
|
+
case "HPE_INVALID_HEADER_TOKEN":
|
|
2094
|
+
case "HPE_INVALID_TRANSFER_ENCODING":
|
|
2095
|
+
case "HPE_UNEXPECTED_CONTENT_LENGTH": return code;
|
|
2096
|
+
default: return "other";
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Classify Node's HTTP rejection without reflecting an unrecognized error code.
|
|
2101
|
+
* @param nodeCode - The parser error's machine code, not its message or raw packet.
|
|
2102
|
+
* @returns The fixed public HTTP status/category and allowlisted diagnostic code.
|
|
2103
|
+
*/
|
|
2104
|
+
function classifyHttpParserFailure(nodeCode) {
|
|
2105
|
+
const parserCode = safeParserCode(nodeCode);
|
|
2106
|
+
if (parserCode === "ERR_HTTP_REQUEST_TIMEOUT") return {
|
|
2107
|
+
status: 408,
|
|
2108
|
+
code: "request_timeout",
|
|
2109
|
+
parserCode
|
|
2110
|
+
};
|
|
2111
|
+
if (parserCode === "HPE_HEADER_OVERFLOW") return {
|
|
2112
|
+
status: 431,
|
|
2113
|
+
code: "headers_too_large",
|
|
2114
|
+
parserCode
|
|
2115
|
+
};
|
|
2116
|
+
return {
|
|
2117
|
+
status: 400,
|
|
2118
|
+
code: "bad_request",
|
|
2119
|
+
parserCode
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
const LOG_INTERVAL_MS = 3e4;
|
|
2123
|
+
/**
|
|
2124
|
+
* Per-Gateway diagnostics with three fixed sampling slots and no timers or request-selected keys.
|
|
2125
|
+
* Each kind records once immediately, then at most once per 30 seconds with suppressed counts.
|
|
2126
|
+
* A sink failure cannot change a response, retry a write or expose the diagnostic sink's error.
|
|
2127
|
+
*/
|
|
2128
|
+
var HttpFailureRecorder = class {
|
|
2129
|
+
observe;
|
|
2130
|
+
now;
|
|
2131
|
+
reportObserverFailure;
|
|
2132
|
+
windows = {
|
|
2133
|
+
parser: {
|
|
2134
|
+
nextLogAt: 0,
|
|
2135
|
+
suppressed: 0
|
|
2136
|
+
},
|
|
2137
|
+
target: {
|
|
2138
|
+
nextLogAt: 0,
|
|
2139
|
+
suppressed: 0
|
|
2140
|
+
},
|
|
2141
|
+
upstream: {
|
|
2142
|
+
nextLogAt: 0,
|
|
2143
|
+
suppressed: 0
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
observerFailureReported = false;
|
|
2147
|
+
constructor(observe, now = () => performance.now(), reportObserverFailure = () => {
|
|
2148
|
+
process.emitWarning("Mobile access HTTP diagnostics could not be recorded", { code: "DSH_MOBILE_HTTP_LOG_FAILED" });
|
|
2149
|
+
}) {
|
|
2150
|
+
this.observe = observe;
|
|
2151
|
+
this.now = now;
|
|
2152
|
+
this.reportObserverFailure = reportObserverFailure;
|
|
2153
|
+
}
|
|
2154
|
+
/** Classify an inbound parser rejection and sample only its fixed status and safe code. */
|
|
2155
|
+
recordParser(nodeCode) {
|
|
2156
|
+
const failure = classifyHttpParserFailure(nodeCode);
|
|
2157
|
+
this.record({
|
|
2158
|
+
kind: "parser",
|
|
2159
|
+
status: failure.status,
|
|
2160
|
+
code: failure.parserCode
|
|
2161
|
+
});
|
|
2162
|
+
return failure;
|
|
2163
|
+
}
|
|
2164
|
+
/** Sample an invalid request target without retaining its raw or decoded URL. */
|
|
2165
|
+
recordTarget() {
|
|
2166
|
+
this.record({
|
|
2167
|
+
kind: "target",
|
|
2168
|
+
status: 400,
|
|
2169
|
+
code: "bad_request"
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
/** Sample only authenticated prompt rejections; the caller must authorize before forwarding. */
|
|
2173
|
+
recordUpstream(method, pathname, status) {
|
|
2174
|
+
if (method !== "POST" || pathname !== "/api/session/prompt" || !Number.isInteger(status) || status < 400 || status > 599) return;
|
|
2175
|
+
this.record({
|
|
2176
|
+
kind: "upstream",
|
|
2177
|
+
status,
|
|
2178
|
+
method: "POST",
|
|
2179
|
+
path: "/api/session/prompt"
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
record(entry) {
|
|
2183
|
+
if (this.observe === void 0) return;
|
|
2184
|
+
const now = this.now();
|
|
2185
|
+
const window = this.windows[entry.kind];
|
|
2186
|
+
if (now < window.nextLogAt) {
|
|
2187
|
+
window.suppressed = Math.min(Number.MAX_SAFE_INTEGER, window.suppressed + 1);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const suppressed = window.suppressed;
|
|
2191
|
+
window.nextLogAt = now + LOG_INTERVAL_MS;
|
|
2192
|
+
window.suppressed = 0;
|
|
2193
|
+
try {
|
|
2194
|
+
this.observe({
|
|
2195
|
+
...entry,
|
|
2196
|
+
...suppressed === 0 ? {} : { suppressed }
|
|
2197
|
+
});
|
|
2198
|
+
} catch (observerError) {
|
|
2199
|
+
if (this.observerFailureReported) return;
|
|
2200
|
+
this.observerFailureReported = true;
|
|
2201
|
+
try {
|
|
2202
|
+
this.reportObserverFailure();
|
|
2203
|
+
} catch (reporterError) {}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
//#endregion
|
|
2068
2208
|
//#region src/client-module-recovery.ts
|
|
2069
2209
|
const CLIENT_MODULE_RECOVERY_COPY = {
|
|
2070
2210
|
zh: {
|
|
@@ -5035,7 +5175,7 @@ function rejectUpgrade(socket, status, code) {
|
|
|
5035
5175
|
if (socket.destroyed) return;
|
|
5036
5176
|
const body = `${JSON.stringify({ error: code })}\n`;
|
|
5037
5177
|
socket.end([
|
|
5038
|
-
`HTTP/1.1 ${String(status)} ${status
|
|
5178
|
+
`HTTP/1.1 ${String(status)} ${STATUS_CODES[status] ?? "Bad Request"}`,
|
|
5039
5179
|
"Connection: close",
|
|
5040
5180
|
"Cache-Control: no-store",
|
|
5041
5181
|
"Content-Type: application/json; charset=utf-8",
|
|
@@ -5395,14 +5535,19 @@ var MobileAccessGateway = class {
|
|
|
5395
5535
|
closeTask;
|
|
5396
5536
|
removeSessionListener;
|
|
5397
5537
|
removeExtensionContentListener;
|
|
5398
|
-
|
|
5399
|
-
|
|
5538
|
+
renewFailureLimiter;
|
|
5539
|
+
renewDeviceLimiter;
|
|
5540
|
+
renewInFlight = /* @__PURE__ */ new Set();
|
|
5541
|
+
probeFailureLimiter;
|
|
5542
|
+
probeDeviceLimiter;
|
|
5543
|
+
probeInFlight = /* @__PURE__ */ new Set();
|
|
5400
5544
|
clientModulePreferences;
|
|
5401
5545
|
clientModuleOperations = /* @__PURE__ */ new Set();
|
|
5402
5546
|
clientModuleTasks = /* @__PURE__ */ new Set();
|
|
5403
5547
|
clientModuleRevocations = /* @__PURE__ */ new Set();
|
|
5404
5548
|
authFailures;
|
|
5405
|
-
|
|
5549
|
+
httpFailures;
|
|
5550
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded, clientModulePreferences, authFailureLog, httpFailureLog) {
|
|
5406
5551
|
this.config = config;
|
|
5407
5552
|
this.extensions = extensions;
|
|
5408
5553
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
@@ -5410,6 +5555,7 @@ var MobileAccessGateway = class {
|
|
|
5410
5555
|
this.blockedUpgradeLog = blockedUpgradeLog;
|
|
5411
5556
|
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
5412
5557
|
this.authFailures = new AuthFailureRecorder(authFailureLog);
|
|
5558
|
+
this.httpFailures = new HttpFailureRecorder(httpFailureLog);
|
|
5413
5559
|
this.clientModulePreferences = clientModulePreferences ?? new ClientModulePreferenceStore(join(dirname(config.stateFile), `${basename(config.stateFile)}.client-modules.json`), config.excludedClientModules);
|
|
5414
5560
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
5415
5561
|
this.tlsEnabled = config.publicTls;
|
|
@@ -5425,8 +5571,12 @@ var MobileAccessGateway = class {
|
|
|
5425
5571
|
maxPairingAttempts: config.maxPairingAttempts,
|
|
5426
5572
|
maxRateLimitKeys: config.maxRateLimitKeys
|
|
5427
5573
|
});
|
|
5428
|
-
|
|
5429
|
-
|
|
5574
|
+
const renewLimit = Math.min(100, config.maxPairingAttempts * 4);
|
|
5575
|
+
const probeLimit = Math.min(1e3, config.maxDevices * 4);
|
|
5576
|
+
this.renewFailureLimiter = new BoundedRateLimiter(renewLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5577
|
+
this.renewDeviceLimiter = new BoundedRateLimiter(renewLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5578
|
+
this.probeFailureLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxRateLimitKeys);
|
|
5579
|
+
this.probeDeviceLimiter = new BoundedRateLimiter(probeLimit, config.rateLimitWindowMs, config.maxDevices);
|
|
5430
5580
|
this.removeSessionListener = this.access.onSessionEnded((authorization, reason) => {
|
|
5431
5581
|
if (reason === "revoked") {
|
|
5432
5582
|
const cleanup = this.clientModulePreferences.removeDevice(authorization.deviceId).catch(() => {
|
|
@@ -5502,8 +5652,10 @@ var MobileAccessGateway = class {
|
|
|
5502
5652
|
rejectUpgrade(client, mapped.status, mapped.code);
|
|
5503
5653
|
});
|
|
5504
5654
|
});
|
|
5505
|
-
server.on("clientError", (
|
|
5506
|
-
|
|
5655
|
+
server.on("clientError", (error, socket) => {
|
|
5656
|
+
const nodeCode = "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
5657
|
+
const failure = this.httpFailures.recordParser(nodeCode);
|
|
5658
|
+
rejectUpgrade(socket, failure.status, failure.code);
|
|
5507
5659
|
});
|
|
5508
5660
|
await new Promise((resolve, reject) => {
|
|
5509
5661
|
const failed = (error) => {
|
|
@@ -5754,6 +5906,27 @@ var MobileAccessGateway = class {
|
|
|
5754
5906
|
maxAgeSeconds: (deviceExpiresAt - Date.now()) / 1e3
|
|
5755
5907
|
})]);
|
|
5756
5908
|
}
|
|
5909
|
+
/** Separate malformed or invalid requests from device-authenticated work without trusting forwarded addresses. */
|
|
5910
|
+
async withDeviceRequest(request, failureLimiter, deviceLimiter, inFlight, validate, operation) {
|
|
5911
|
+
let device;
|
|
5912
|
+
try {
|
|
5913
|
+
device = await validate();
|
|
5914
|
+
} catch (error) {
|
|
5915
|
+
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");
|
|
5916
|
+
throw error;
|
|
5917
|
+
}
|
|
5918
|
+
if (inFlight.has(device.deviceId)) throw new HttpError(429, "rate_limited");
|
|
5919
|
+
const now = Date.now();
|
|
5920
|
+
const activeIds = new Set(this.access.listDevices().filter((candidate) => candidate.revokedAt === void 0 && candidate.expiresAt > now).map((candidate) => candidate.id));
|
|
5921
|
+
deviceLimiter.retainKeys(activeIds);
|
|
5922
|
+
if (!deviceLimiter.take(device.deviceId, now)) throw new HttpError(429, "rate_limited");
|
|
5923
|
+
inFlight.add(device.deviceId);
|
|
5924
|
+
try {
|
|
5925
|
+
return await operation(device);
|
|
5926
|
+
} finally {
|
|
5927
|
+
inFlight.delete(device.deviceId);
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5757
5930
|
async handlePair(request, response) {
|
|
5758
5931
|
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5759
5932
|
if (typeof body.token !== "string" || body.label !== void 0 && typeof body.label !== "string") throw new HttpError(400, "bad_request");
|
|
@@ -5776,21 +5949,28 @@ var MobileAccessGateway = class {
|
|
|
5776
5949
|
}, this.tlsEnabled);
|
|
5777
5950
|
}
|
|
5778
5951
|
async handleRenew(request, response) {
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5952
|
+
await this.withDeviceRequest(request, this.renewFailureLimiter, this.renewDeviceLimiter, this.renewInFlight, async () => {
|
|
5953
|
+
await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5954
|
+
const cookies = browserAuthCookies(request.headers.cookie);
|
|
5955
|
+
const bindings = this.tlsEnabled ? cookies.get(HOST_DEVICE_COOKIE) : void 0;
|
|
5956
|
+
if (bindings !== void 0 && bindings.length !== 1) throw new HttpError(401, "authentication_failed");
|
|
5957
|
+
const device = authenticatedCookie(cookies.get("dsh_ma_device") ?? [], (token) => this.access.authorizeDevice(token), (candidate) => bindings === void 0 || candidate.deviceId === bindings[0]);
|
|
5958
|
+
return {
|
|
5959
|
+
deviceId: device.result.deviceId,
|
|
5960
|
+
token: device.value,
|
|
5961
|
+
credential: device.result
|
|
5962
|
+
};
|
|
5963
|
+
}, async (selected) => {
|
|
5964
|
+
const result = await this.access.renew(selected.token);
|
|
5965
|
+
this.setSessionCookies(response, result, Date.now());
|
|
5966
|
+
this.setBrowserDeviceBinding(response, result.deviceId, selected.credential.deviceExpiresAt);
|
|
5967
|
+
sendJson(response, 200, {
|
|
5968
|
+
renewed: true,
|
|
5969
|
+
deviceId: result.deviceId,
|
|
5970
|
+
csrfToken: result.csrfToken,
|
|
5971
|
+
sessionExpiresAt: result.sessionExpiresAt
|
|
5972
|
+
}, this.tlsEnabled);
|
|
5973
|
+
});
|
|
5794
5974
|
}
|
|
5795
5975
|
/** CA-backed entries retain their pin; public entries project the authenticated credential's identity. */
|
|
5796
5976
|
nativeResponseInstanceId(result) {
|
|
@@ -5811,28 +5991,40 @@ var MobileAccessGateway = class {
|
|
|
5811
5991
|
}, this.tlsEnabled);
|
|
5812
5992
|
}
|
|
5813
5993
|
async handleNativeRenew(request, response) {
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5994
|
+
await this.withDeviceRequest(request, this.renewFailureLimiter, this.renewDeviceLimiter, this.renewInFlight, async () => {
|
|
5995
|
+
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
5996
|
+
if (typeof body.deviceToken !== "string") throw new HttpError(400, "bad_request");
|
|
5997
|
+
return {
|
|
5998
|
+
deviceId: this.access.authorizeDevice(body.deviceToken).deviceId,
|
|
5999
|
+
token: body.deviceToken
|
|
6000
|
+
};
|
|
6001
|
+
}, async (selected) => {
|
|
6002
|
+
const result = await this.access.renew(selected.token);
|
|
6003
|
+
sendJson(response, 200, {
|
|
6004
|
+
instanceId: this.nativeResponseInstanceId(result),
|
|
6005
|
+
deviceId: result.deviceId,
|
|
6006
|
+
sessionToken: result.sessionToken,
|
|
6007
|
+
csrfToken: result.csrfToken,
|
|
6008
|
+
sessionExpiresAt: result.sessionExpiresAt
|
|
6009
|
+
}, this.tlsEnabled);
|
|
6010
|
+
});
|
|
5825
6011
|
}
|
|
5826
6012
|
async handleNativeProbe(request, response) {
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
6013
|
+
await this.withDeviceRequest(request, this.probeFailureLimiter, this.probeDeviceLimiter, this.probeInFlight, async () => {
|
|
6014
|
+
const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
6015
|
+
if (typeof body.deviceToken !== "string") throw new HttpError(400, "bad_request");
|
|
6016
|
+
return {
|
|
6017
|
+
deviceId: this.access.authorizeDevice(body.deviceToken).deviceId,
|
|
6018
|
+
token: body.deviceToken
|
|
6019
|
+
};
|
|
6020
|
+
}, async (selected) => {
|
|
6021
|
+
const result = await this.access.probe(selected.token);
|
|
6022
|
+
sendJson(response, 200, {
|
|
6023
|
+
instanceId: this.nativeResponseInstanceId(result),
|
|
6024
|
+
deviceId: result.deviceId,
|
|
6025
|
+
deviceExpiresAt: result.deviceExpiresAt
|
|
6026
|
+
}, this.tlsEnabled);
|
|
6027
|
+
});
|
|
5836
6028
|
}
|
|
5837
6029
|
async handleLogout(request, response) {
|
|
5838
6030
|
await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
|
|
@@ -5867,7 +6059,13 @@ var MobileAccessGateway = class {
|
|
|
5867
6059
|
sendJson(response, 200, { loggedOut: true }, this.tlsEnabled);
|
|
5868
6060
|
}
|
|
5869
6061
|
async handleExternalRequest(request, response) {
|
|
5870
|
-
|
|
6062
|
+
let target;
|
|
6063
|
+
try {
|
|
6064
|
+
target = parseRequestTarget(request.url);
|
|
6065
|
+
} catch (error) {
|
|
6066
|
+
if (error instanceof HttpError && error.status === 400) this.httpFailures.recordTarget();
|
|
6067
|
+
throw error;
|
|
6068
|
+
}
|
|
5871
6069
|
const policy = this.requirePolicy();
|
|
5872
6070
|
const isMutation = request.method !== "GET" && request.method !== "HEAD";
|
|
5873
6071
|
assertExternalTrust(request, policy, isMutation);
|
|
@@ -6390,7 +6588,8 @@ var MobileAccessGateway = class {
|
|
|
6390
6588
|
headers: {
|
|
6391
6589
|
host: this.config.upstreamOrigin.host,
|
|
6392
6590
|
accept: "text/html",
|
|
6393
|
-
"accept-encoding": "identity"
|
|
6591
|
+
"accept-encoding": "identity",
|
|
6592
|
+
"sec-fetch-mode": "same-origin"
|
|
6394
6593
|
},
|
|
6395
6594
|
agent: false
|
|
6396
6595
|
});
|
|
@@ -7027,6 +7226,7 @@ var MobileAccessGateway = class {
|
|
|
7027
7226
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
7028
7227
|
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
7029
7228
|
const statusCode = proxied.statusCode ?? 502;
|
|
7229
|
+
this.httpFailures.recordUpstream(request$2.method, request$2.url?.split("?", 1)[0] ?? "", statusCode);
|
|
7030
7230
|
const cacheControl = revisionedStaticCacheControl(request$2, statusCode);
|
|
7031
7231
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
7032
7232
|
const compressible = canCompressResponse(request$2, proxied);
|
|
@@ -17535,6 +17735,8 @@ async function apply(ctx, config) {
|
|
|
17535
17735
|
logger.warn("%s discovery degraded while DSH remains available: %s", source, code);
|
|
17536
17736
|
}, clientModulePreferences, (entry) => {
|
|
17537
17737
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17738
|
+
}, (entry) => {
|
|
17739
|
+
logger.warn("http failure %s", JSON.stringify(entry));
|
|
17538
17740
|
});
|
|
17539
17741
|
await candidate.start();
|
|
17540
17742
|
lanGateway = candidate;
|
|
@@ -17593,6 +17795,8 @@ async function apply(ctx, config) {
|
|
|
17593
17795
|
const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, remoteInstanceId, listenPort, instanceId);
|
|
17594
17796
|
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences, (entry) => {
|
|
17595
17797
|
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17798
|
+
}, (entry) => {
|
|
17799
|
+
logger.warn("http failure %s", JSON.stringify(entry));
|
|
17596
17800
|
});
|
|
17597
17801
|
await candidate.start();
|
|
17598
17802
|
return candidate;
|
|
@@ -17609,7 +17813,11 @@ async function apply(ctx, config) {
|
|
|
17609
17813
|
if (!isFrpSelfSignedIngress(settings)) return createRemoteGateway(publicOrigin);
|
|
17610
17814
|
const ingress = await ensureFrpIngressCertificate(settings, remoteDeviceFile, Date.now(), void 0, stateDirectory);
|
|
17611
17815
|
const resolved = frpIngressGatewayConfig(template, settings, remoteDeviceFile, ingress);
|
|
17612
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences)
|
|
17816
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences, (entry) => {
|
|
17817
|
+
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17818
|
+
}, (entry) => {
|
|
17819
|
+
logger.warn("http failure %s", JSON.stringify(entry));
|
|
17820
|
+
});
|
|
17613
17821
|
try {
|
|
17614
17822
|
await candidate.start();
|
|
17615
17823
|
} catch (error) {
|
|
@@ -17620,7 +17828,11 @@ async function apply(ctx, config) {
|
|
|
17620
17828
|
};
|
|
17621
17829
|
const createOriginGateway = async (settings) => {
|
|
17622
17830
|
const resolved = originGatewayConfig(template, settings, remoteDeviceFile, remoteInstanceId, settings.listenPort, instanceId);
|
|
17623
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences)
|
|
17831
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences, (entry) => {
|
|
17832
|
+
logger.warn("auth failure %s", JSON.stringify(entry));
|
|
17833
|
+
}, (entry) => {
|
|
17834
|
+
logger.warn("http failure %s", JSON.stringify(entry));
|
|
17835
|
+
});
|
|
17624
17836
|
try {
|
|
17625
17837
|
await candidate.start();
|
|
17626
17838
|
} catch (error) {
|