@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -4,21 +4,22 @@ import { resolveForwarded } from "../src/context/forwarded.js";
|
|
|
4
4
|
import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
|
|
5
5
|
import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
|
|
6
6
|
import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
|
|
7
|
-
import
|
|
7
|
+
import { responseEnded } from "../src/context/responseEnded.js";
|
|
8
|
+
import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
|
|
9
|
+
import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
10
|
+
import { trackPong } from "./servers/wsHeartbeat.js";
|
|
8
11
|
import HttpContext from "../src/context/http/HttpContext.js";
|
|
9
12
|
import DefaultErrorRenderer from "./error-renderer.js";
|
|
10
|
-
import DefaultRequestLogger from "./request-logger.js";
|
|
11
13
|
import JsonAuditLogger from "./audit-logger.js";
|
|
14
|
+
import DefaultRequestLogger from "./request-logger.js";
|
|
12
15
|
import PrettyRequestLogger from "./pretty-request-logger.js";
|
|
13
16
|
import { resolveTraceparent } from "./trace.js";
|
|
14
17
|
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import { GcScheduler, Module, RequestContext, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
18
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
19
|
+
import { GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
18
20
|
import cluster from "node:cluster";
|
|
19
21
|
import http from "node:http";
|
|
20
|
-
|
|
21
|
-
//#region nodefony/service/http-kernel.ts
|
|
22
|
+
// nodefony/service/http-kernel.ts
|
|
22
23
|
const HEALTH_HEADERS = {
|
|
23
24
|
"content-type": "application/json",
|
|
24
25
|
"cache-control": "no-store"
|
|
@@ -67,13 +68,37 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
67
68
|
cert = "";
|
|
68
69
|
ca = "";
|
|
69
70
|
serverStatic = null;
|
|
71
|
+
/**
|
|
72
|
+
* Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
|
|
73
|
+
* `mounts === null` tant que personne n'a monté de préfixe.
|
|
74
|
+
*/
|
|
75
|
+
reverseProxy = null;
|
|
70
76
|
domain = "";
|
|
71
77
|
trustedHosts;
|
|
72
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Contrôle de domaine actif (`domainCheck` de la config d'application) —
|
|
80
|
+
* lu à la source, pour le HTTP comme pour le handshake WebSocket.
|
|
81
|
+
*/
|
|
82
|
+
get domainCheck() {
|
|
83
|
+
return this.kernel?.options.domainCheck === true;
|
|
84
|
+
}
|
|
73
85
|
regAlias = [];
|
|
74
86
|
module;
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
|
|
89
|
+
* lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
|
|
90
|
+
*/
|
|
91
|
+
get httpsPort() {
|
|
92
|
+
return this.boundPort("server-https");
|
|
93
|
+
}
|
|
94
|
+
/** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
|
|
95
|
+
get httpPort() {
|
|
96
|
+
return this.boundPort("server-http");
|
|
97
|
+
}
|
|
98
|
+
boundPort(name) {
|
|
99
|
+
const server = this.get(name);
|
|
100
|
+
return server?.active === true && server.port > 0 ? server.port : void 0;
|
|
101
|
+
}
|
|
77
102
|
responseTimeout;
|
|
78
103
|
closeTimeOutWs;
|
|
79
104
|
secContentTypeOptions = null;
|
|
@@ -90,6 +115,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
90
115
|
rateLimiter = null;
|
|
91
116
|
rateLimitGc = null;
|
|
92
117
|
#wsConnCounter = null;
|
|
118
|
+
#streams = null;
|
|
93
119
|
#healthPaths = null;
|
|
94
120
|
#healthShutdownDelay = 0;
|
|
95
121
|
#terminating = false;
|
|
@@ -99,15 +125,16 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
99
125
|
this.container?.addScope("request");
|
|
100
126
|
this.responseTimeout = {
|
|
101
127
|
http: this.options.http.responseTimeout,
|
|
102
|
-
https: this.options.https.responseTimeout
|
|
103
|
-
http2: this.options.https.responseTimeout,
|
|
104
|
-
http3: this.options.https.responseTimeout
|
|
128
|
+
https: this.options.https.responseTimeout
|
|
105
129
|
};
|
|
106
130
|
this.closeTimeOutWs = {
|
|
107
131
|
ws: this.options.websocket.closeTimeout,
|
|
108
132
|
wss: this.options.websocketSecure.closeTimeout
|
|
109
133
|
};
|
|
110
134
|
this.computeSecurityHeaderCaches();
|
|
135
|
+
this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
|
|
136
|
+
this.#streams?.refreshIdentity(event.identifier);
|
|
137
|
+
});
|
|
111
138
|
}
|
|
112
139
|
/**
|
|
113
140
|
* (Re)calcule les caches dérivés des en-têtes de sécurité depuis `this.options`
|
|
@@ -244,6 +271,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
244
271
|
this.kernel?.prependOnceListener("onReady", () => {
|
|
245
272
|
this.serviceCerticats = this.get("certificates");
|
|
246
273
|
this.serverStatic = this.get("server-static");
|
|
274
|
+
this.reverseProxy = this.get("reverse-proxy");
|
|
247
275
|
this.domain = this.kernel?.domain;
|
|
248
276
|
this.trustedHosts = this.options.trustedHosts;
|
|
249
277
|
this.regAlias = this.compileAlias();
|
|
@@ -307,6 +335,35 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
307
335
|
return policy;
|
|
308
336
|
}
|
|
309
337
|
/**
|
|
338
|
+
* L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
|
|
339
|
+
* interrogeable SANS contexte : le serveur WebSocket de Nodefony
|
|
340
|
+
* ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
|
|
341
|
+
* avant qu'aucun contexte n'existe, décident par elle.
|
|
342
|
+
*
|
|
343
|
+
* Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
|
|
344
|
+
* en development, allowlist `allowedOrigins` ; sans `Origin` (client
|
|
345
|
+
* non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
|
|
346
|
+
*
|
|
347
|
+
* @param origin - valeur brute de l'en-tête `Origin`
|
|
348
|
+
* @param hostname - nom d'hôte servi, sans port (`Context.domain`)
|
|
349
|
+
* @param secure - handshake reçu en TLS (politique `websocketSecure`)
|
|
350
|
+
* @returns `true` si le handshake peut se poursuivre
|
|
351
|
+
*/
|
|
352
|
+
isWebsocketOriginAllowed(origin, hostname, secure) {
|
|
353
|
+
const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
|
|
354
|
+
if (policy.disabled) return true;
|
|
355
|
+
if (!origin) return true;
|
|
356
|
+
let originHost;
|
|
357
|
+
try {
|
|
358
|
+
originHost = new URL(origin).hostname;
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (originHost === hostname) return true;
|
|
363
|
+
if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return true;
|
|
364
|
+
return policy.extra.length > 0 && isDomainAllowed(policy.extra, originHost);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
310
367
|
* B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
|
|
311
368
|
* WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
|
|
312
369
|
* ce contrôle, une page tierce peut ouvrir un WS **authentifié par le cookie de
|
|
@@ -322,22 +379,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
322
379
|
* @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
|
|
323
380
|
*/
|
|
324
381
|
checkWebsocketOrigin(context) {
|
|
325
|
-
const
|
|
326
|
-
const policy = this.getWsOriginPolicy(cfgKey);
|
|
327
|
-
if (policy.disabled) return;
|
|
382
|
+
const secure = context.serverType === "wss";
|
|
328
383
|
const raw = context.origin;
|
|
329
|
-
if (
|
|
330
|
-
let originHost = null;
|
|
331
|
-
try {
|
|
332
|
-
originHost = new URL(raw).hostname;
|
|
333
|
-
} catch {
|
|
334
|
-
originHost = null;
|
|
335
|
-
}
|
|
336
|
-
if (originHost) {
|
|
337
|
-
if (originHost === context.domain) return;
|
|
338
|
-
if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return;
|
|
339
|
-
if (policy.extra.length && isDomainAllowed(policy.extra, originHost)) return;
|
|
340
|
-
}
|
|
384
|
+
if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
|
|
341
385
|
const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
|
|
342
386
|
error.type = "origin";
|
|
343
387
|
throw error;
|
|
@@ -434,7 +478,14 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
434
478
|
context.resolver = resolver;
|
|
435
479
|
return resolver;
|
|
436
480
|
}
|
|
437
|
-
if (resolver.exception)
|
|
481
|
+
if (resolver.exception) {
|
|
482
|
+
const exception = resolver.exception;
|
|
483
|
+
if (context.secure && "code" in exception && exception.code === 426) {
|
|
484
|
+
context.resolver = resolver;
|
|
485
|
+
return resolver;
|
|
486
|
+
}
|
|
487
|
+
throw resolver.exception;
|
|
488
|
+
}
|
|
438
489
|
throw new HttpError("Not Found", 404, context);
|
|
439
490
|
}
|
|
440
491
|
/**
|
|
@@ -483,7 +534,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
483
534
|
*/
|
|
484
535
|
/**
|
|
485
536
|
* Override the default error renderer — e.g. to hide stack traces in prod
|
|
486
|
-
* or emit RFC
|
|
537
|
+
* or emit RFC 9457 problem+json. Stateless singleton expected.
|
|
487
538
|
*/
|
|
488
539
|
setErrorRenderer(renderer) {
|
|
489
540
|
this.errorRenderer = renderer;
|
|
@@ -504,6 +555,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
504
555
|
async onError(error, context, _extraHeaders) {
|
|
505
556
|
if (context) context.error = error;
|
|
506
557
|
if (context instanceof HttpContext) {
|
|
558
|
+
if (context.isRedirect && error.code === 401) {
|
|
559
|
+
if (!(context.finished || context.sended)) await context.send();
|
|
560
|
+
return context;
|
|
561
|
+
}
|
|
507
562
|
const result = this.errorRenderer.renderHttp(error, context);
|
|
508
563
|
if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
|
|
509
564
|
context.response.setStatusCode(result.status, http.STATUS_CODES[result.status] ?? "Unknown Error");
|
|
@@ -514,6 +569,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
514
569
|
return context;
|
|
515
570
|
}
|
|
516
571
|
if (!context.response.isHeaderSent()) {
|
|
572
|
+
if (context.isRedirect) {
|
|
573
|
+
context.isRedirect = false;
|
|
574
|
+
context.response.response?.removeHeader("Location");
|
|
575
|
+
}
|
|
517
576
|
try {
|
|
518
577
|
await context.render(result.body);
|
|
519
578
|
} catch (e) {
|
|
@@ -525,18 +584,26 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
525
584
|
await context.close();
|
|
526
585
|
return context;
|
|
527
586
|
}
|
|
528
|
-
if (context instanceof WebsocketContext)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
context.
|
|
587
|
+
if (context instanceof WebsocketContext) {
|
|
588
|
+
if (context.admission !== null) {
|
|
589
|
+
const status = toHandshakeStatus("code" in error && typeof error.code === "number" ? error.code : void 0);
|
|
590
|
+
context.refuseHandshake(status);
|
|
591
|
+
if (this.kernel?.debug) this.log(`${status} ${error.message}`, "DEBUG", "WEBSOCKET REFUSED");
|
|
532
592
|
return context;
|
|
533
593
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
594
|
+
try {
|
|
595
|
+
const wsResult = this.errorRenderer.renderWebsocket(error, context);
|
|
596
|
+
if (context.response?.connection) {
|
|
597
|
+
context.close(wsResult.code, wsResult.reason);
|
|
598
|
+
return context;
|
|
599
|
+
}
|
|
600
|
+
if (context.request && !context.rejected) {
|
|
601
|
+
context.reject(wsResult.code, wsResult.reason);
|
|
602
|
+
return context;
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
throw error;
|
|
537
606
|
}
|
|
538
|
-
} catch {
|
|
539
|
-
throw error;
|
|
540
607
|
}
|
|
541
608
|
throw error;
|
|
542
609
|
}
|
|
@@ -578,7 +645,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
578
645
|
if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
|
|
579
646
|
if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
|
|
580
647
|
if (this.secFrameOptions !== null) response.setHeader("X-Frame-Options", this.secFrameOptions);
|
|
581
|
-
if (this.secHsts !== null &&
|
|
648
|
+
if (this.secHsts !== null && type === "https") response.setHeader("Strict-Transport-Security", this.secHsts);
|
|
582
649
|
if (this.#healthPaths !== null) {
|
|
583
650
|
const url = request.url;
|
|
584
651
|
if (url === this.#healthPaths.liveness) {
|
|
@@ -621,6 +688,11 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
621
688
|
await serverHttps.createServer();
|
|
622
689
|
servers.push(serverHttps);
|
|
623
690
|
}
|
|
691
|
+
const serverH2c = this.get("server-h2c");
|
|
692
|
+
if (serverH2c?.active) {
|
|
693
|
+
await serverH2c.createServer();
|
|
694
|
+
servers.push(serverH2c);
|
|
695
|
+
}
|
|
624
696
|
const serverWebsocket = this.get("server-websocket");
|
|
625
697
|
if (serverWebsocket && serverHttp?.active) {
|
|
626
698
|
await serverWebsocket.createServer(serverHttp);
|
|
@@ -631,7 +703,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
631
703
|
await serverWebsocketSecure.createServer(serverHttps);
|
|
632
704
|
servers.push(serverWebsocketSecure);
|
|
633
705
|
}
|
|
634
|
-
this.publishRuntimePorts(serverHttp, serverHttps);
|
|
706
|
+
this.publishRuntimePorts(serverHttp, serverHttps, serverH2c);
|
|
635
707
|
return servers;
|
|
636
708
|
}
|
|
637
709
|
/**
|
|
@@ -665,7 +737,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
665
737
|
* {@link readRuntimeState} — `status`/`stop` redeviendraient aveugles alors que
|
|
666
738
|
* l'app écoute toujours. Le master, lui, est le propriétaire du runtime.
|
|
667
739
|
*/
|
|
668
|
-
publishRuntimePorts(serverHttp, serverHttps) {
|
|
740
|
+
publishRuntimePorts(serverHttp, serverHttps, serverH2c) {
|
|
669
741
|
const ports = [];
|
|
670
742
|
const desired = [];
|
|
671
743
|
const urls = [];
|
|
@@ -681,6 +753,11 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
681
753
|
const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "https");
|
|
682
754
|
if (port) desired.push(port);
|
|
683
755
|
}
|
|
756
|
+
if (serverH2c?.active && serverH2c.port > 0) {
|
|
757
|
+
ports.push(serverH2c.port);
|
|
758
|
+
const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "h2c");
|
|
759
|
+
if (port) desired.push(port);
|
|
760
|
+
}
|
|
684
761
|
if (ports.length === 0) return;
|
|
685
762
|
writeRuntimeState(process.cwd(), {
|
|
686
763
|
pid: cluster.isWorker ? process.ppid : process.pid,
|
|
@@ -799,9 +876,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
799
876
|
perfProbe.t0 = 0n;
|
|
800
877
|
} else context = new HttpContext(scope, request, response, type);
|
|
801
878
|
response.on("close", () => {
|
|
802
|
-
if (!response
|
|
879
|
+
if (!responseEnded(response)) {
|
|
803
880
|
context._abortIfPending("Connection closed before response finished");
|
|
804
|
-
if (
|
|
881
|
+
if (context.response.statusCode < 400) context.response.statusCode = 499;
|
|
805
882
|
}
|
|
806
883
|
this.teardownHttp(context, scope);
|
|
807
884
|
});
|
|
@@ -842,14 +919,16 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
842
919
|
const profilerQueries = this.profiler ? [] : null;
|
|
843
920
|
context.profilerQueries = profilerQueries;
|
|
844
921
|
context.profiling = profilerQueries !== null;
|
|
845
|
-
|
|
922
|
+
const store = {
|
|
846
923
|
requestId: context.requestId,
|
|
847
924
|
scheme: context.scheme,
|
|
848
925
|
traceparent: context.traceparent,
|
|
849
926
|
queries: profilerQueries ?? void 0,
|
|
850
927
|
context,
|
|
851
928
|
scope
|
|
852
|
-
}
|
|
929
|
+
};
|
|
930
|
+
context.holdRequestStore(store);
|
|
931
|
+
return RequestContext.run(store, () => this.runHttpPipeline(context, request, response));
|
|
853
932
|
}
|
|
854
933
|
/**
|
|
855
934
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
@@ -887,6 +966,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
887
966
|
* N'attend que le repli statique, et seulement quand aucune route ne matche.
|
|
888
967
|
*/
|
|
889
968
|
routeHttpRequest(httpContext, request, response) {
|
|
969
|
+
if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
|
|
970
|
+
const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
|
|
971
|
+
if (relayed !== void 0) return relayed.then(() => httpContext);
|
|
972
|
+
}
|
|
890
973
|
httpContext.phaseStart("resolve");
|
|
891
974
|
httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
|
|
892
975
|
httpContext.phaseEnd("resolve");
|
|
@@ -931,7 +1014,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
931
1014
|
if (error) throw error;
|
|
932
1015
|
const schemeHeaders = context.scheme === "https" ? this.options.https.headers : context.scheme === "http" ? this.options.http.headers : null;
|
|
933
1016
|
if (schemeHeaders) context.response.setHeaders(schemeHeaders);
|
|
934
|
-
if (this.
|
|
1017
|
+
if (this.domainCheck) this.checkValidDomain(context);
|
|
935
1018
|
if (this.listenerCount("beforeResolve")) return this.fireAsync("beforeResolve", context).then(() => this.armAndGuard(context));
|
|
936
1019
|
return this.armAndGuard(context);
|
|
937
1020
|
}
|
|
@@ -942,7 +1025,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
942
1025
|
armAndGuard(context) {
|
|
943
1026
|
this.prepareFrontController(context);
|
|
944
1027
|
this.firewall?.enforceCsrf(context);
|
|
945
|
-
return thenMaybe(this.startSession(context), () => context.secure
|
|
1028
|
+
return thenMaybe(this.startSession(context), () => context.secure ? this.runFirewall(context) : context);
|
|
946
1029
|
}
|
|
947
1030
|
/**
|
|
948
1031
|
* Le pare-feu d'une requête en zone protégée : `handleSecurity`, puis les
|
|
@@ -958,9 +1041,15 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
958
1041
|
await this.firewall?.handleSecurity(context);
|
|
959
1042
|
if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
|
|
960
1043
|
} catch (authError) {
|
|
1044
|
+
if (context.resolver?.exception) {
|
|
1045
|
+
context.response.response?.removeHeader("Upgrade");
|
|
1046
|
+
context.response.response?.removeHeader("Connection");
|
|
1047
|
+
}
|
|
961
1048
|
await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
|
|
962
1049
|
throw authError;
|
|
963
1050
|
}
|
|
1051
|
+
const pending = context.resolver?.exception;
|
|
1052
|
+
if (pending) throw pending;
|
|
964
1053
|
} finally {
|
|
965
1054
|
context.phaseEnd("firewall");
|
|
966
1055
|
}
|
|
@@ -996,33 +1085,48 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
996
1085
|
this.log(e, "ERROR", "WS onFinish");
|
|
997
1086
|
}
|
|
998
1087
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Point d'entrée d'un upgrade vers une route WebSocket de Nodefony, appelé
|
|
1090
|
+
* par le répartiteur d'upgrade AVANT le `101` (#596) : rien ne s'ouvre tant
|
|
1091
|
+
* que le pipeline n'a pas dit oui.
|
|
1092
|
+
*
|
|
1093
|
+
* Les bornes par IP passent en premier, avant tout scope, ALS ou pipeline
|
|
1094
|
+
* (symétrie avec le 429 HTTP et avec le proxy inverse) : le vrai vecteur DoS
|
|
1095
|
+
* est d'accumuler des sockets ouvertes. La place est rendue à la fermeture du
|
|
1096
|
+
* socket du client — même règle que pour un tunnel relayé.
|
|
1097
|
+
*
|
|
1098
|
+
* @param req - requête d'upgrade
|
|
1099
|
+
* @param admission - le verdict à rendre : accepter (`101`) ou refuser (HTTP)
|
|
1100
|
+
* @param type - serveur qui a reçu l'upgrade
|
|
1101
|
+
* @returns la promesse du traitement de la connexion
|
|
1102
|
+
*/
|
|
1103
|
+
async onWebsocketUpgrade(req, admission, type) {
|
|
1104
|
+
if (this.websocketQuotaRefusal(req, req.socket) !== null) {
|
|
1105
|
+
admission.refuse(429);
|
|
1106
|
+
return;
|
|
1016
1107
|
}
|
|
1017
1108
|
await this.fireAsync("onServerRequest", req, null, type);
|
|
1018
1109
|
const scope = this.container?.enterScope("request");
|
|
1019
|
-
return this.handleWebsocket(scope,
|
|
1110
|
+
return this.handleWebsocket(scope, req, admission, type);
|
|
1020
1111
|
}
|
|
1021
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* Pipeline d'un handshake WebSocket, dans la bulle ALS de la connexion :
|
|
1114
|
+
* jugement (`onConnect`), pare-feu, gardes de l'action — PUIS seulement
|
|
1115
|
+
* l'acceptation, la socket et l'action de handshake. Tout refus antérieur à
|
|
1116
|
+
* l'acceptation part en statut HTTP (`onError` → `refuseHandshake`).
|
|
1117
|
+
*
|
|
1118
|
+
* @param scope - scope DI de la connexion
|
|
1119
|
+
* @param req - requête d'upgrade
|
|
1120
|
+
* @param admission - le verdict à rendre
|
|
1121
|
+
* @param type - serveur qui a reçu l'upgrade
|
|
1122
|
+
* @returns ce que rend l'action de handshake
|
|
1123
|
+
*/
|
|
1124
|
+
async handleWebsocket(scope, req, admission, type) {
|
|
1022
1125
|
let context = null;
|
|
1023
1126
|
let error = null;
|
|
1024
1127
|
try {
|
|
1025
|
-
context = this.createWebsocketContext(scope, req,
|
|
1128
|
+
context = this.createWebsocketContext(scope, req, null, type);
|
|
1129
|
+
context.admission = admission;
|
|
1026
1130
|
} catch (e) {
|
|
1027
1131
|
error = e;
|
|
1028
1132
|
}
|
|
@@ -1034,15 +1138,17 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1034
1138
|
const wsRunId = context?.requestId ?? "ws-no-ctx";
|
|
1035
1139
|
const wsScheme = context?.scheme ?? "ws";
|
|
1036
1140
|
const wsTrace = context?.traceparent ?? null;
|
|
1037
|
-
|
|
1141
|
+
const store = {
|
|
1038
1142
|
requestId: wsRunId,
|
|
1039
1143
|
scheme: wsScheme,
|
|
1040
1144
|
...wsTrace ? { traceparent: wsTrace } : {},
|
|
1041
1145
|
context,
|
|
1042
1146
|
scope
|
|
1043
|
-
}
|
|
1147
|
+
};
|
|
1148
|
+
context?.holdRequestStore(store);
|
|
1149
|
+
return await RequestContext.run(store, async () => {
|
|
1044
1150
|
await this.onConnect(context, error);
|
|
1045
|
-
if (this.firewall &&
|
|
1151
|
+
if (this.firewall && context?.secure) {
|
|
1046
1152
|
context.phaseStart("firewall");
|
|
1047
1153
|
try {
|
|
1048
1154
|
try {
|
|
@@ -1056,22 +1162,34 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1056
1162
|
context.phaseEnd("firewall");
|
|
1057
1163
|
}
|
|
1058
1164
|
}
|
|
1059
|
-
if (context)
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1165
|
+
if (!context) return;
|
|
1166
|
+
const guarded = context.resolver?.authorizeAction();
|
|
1167
|
+
if (guarded !== void 0) await guarded;
|
|
1168
|
+
const accepted = admission.accept();
|
|
1169
|
+
const ws = accepted instanceof Promise ? await accepted : accepted;
|
|
1170
|
+
if (ws === null) {
|
|
1171
|
+
this.releaseOrphanWsScope(scope, context);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
trackPong(ws);
|
|
1175
|
+
context.acceptConnection(ws);
|
|
1176
|
+
await context.connect();
|
|
1177
|
+
context.phaseStart("action");
|
|
1178
|
+
try {
|
|
1179
|
+
return await context.handle();
|
|
1180
|
+
} finally {
|
|
1181
|
+
context.phaseEnd("action");
|
|
1066
1182
|
}
|
|
1067
1183
|
});
|
|
1068
1184
|
} catch (e) {
|
|
1069
1185
|
try {
|
|
1070
1186
|
await this.onError(e, context);
|
|
1071
1187
|
} catch (errorHandlingError) {
|
|
1188
|
+
admission.refuse(500);
|
|
1072
1189
|
this.releaseOrphanWsScope(scope, context);
|
|
1073
1190
|
throw errorHandlingError;
|
|
1074
1191
|
}
|
|
1192
|
+
admission.refuse(500);
|
|
1075
1193
|
this.releaseOrphanWsScope(scope, context);
|
|
1076
1194
|
throw e;
|
|
1077
1195
|
}
|
|
@@ -1086,6 +1204,16 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1086
1204
|
this.container?.leaveScope(scope);
|
|
1087
1205
|
context.clean();
|
|
1088
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
|
|
1209
|
+
* `beforeResolve`, contrôleur (protocole négocié), session. N'ouvre rien —
|
|
1210
|
+
* l'acceptation n'a lieu qu'après le pare-feu et les gardes de l'action
|
|
1211
|
+
* (cf {@link handleWebsocket}).
|
|
1212
|
+
*
|
|
1213
|
+
* @param context - contexte du handshake, encore sans socket
|
|
1214
|
+
* @param error - erreur levée à la construction du contexte, relancée telle quelle
|
|
1215
|
+
* @throws la première erreur du jugement — rendue en statut HTTP par `onError`
|
|
1216
|
+
*/
|
|
1089
1217
|
async onConnect(context, error = null) {
|
|
1090
1218
|
if (error) throw error;
|
|
1091
1219
|
if (!context) throw new nodefonyError("Bad context", 500);
|
|
@@ -1099,7 +1227,6 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1099
1227
|
throw e;
|
|
1100
1228
|
}
|
|
1101
1229
|
if (!context.sessionStarting) await this.startSession(context);
|
|
1102
|
-
return context.connect();
|
|
1103
1230
|
}
|
|
1104
1231
|
checkValidDomain(context) {
|
|
1105
1232
|
if (context.validDomain) return 200;
|
|
@@ -1126,8 +1253,130 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1126
1253
|
isTrustedHostname(hostname) {
|
|
1127
1254
|
return isDomainAllowed(this.regAlias, hostname);
|
|
1128
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
|
|
1258
|
+
* (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
|
|
1259
|
+
* inverse et avant le serveur `ws`.
|
|
1260
|
+
*
|
|
1261
|
+
* Pourquoi avant : accepter puis fermer fait croire au client que le serveur
|
|
1262
|
+
* a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
|
|
1263
|
+
* une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
|
|
1264
|
+
* recharger la page avant que Vite soit prêt — page blanche.
|
|
1265
|
+
*
|
|
1266
|
+
* Ne juge que l'existence d'une route WebSocket pour le chemin
|
|
1267
|
+
* ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
|
|
1268
|
+
* quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
|
|
1269
|
+
* compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
|
|
1270
|
+
* flot d'upgrades vers des chemins inexistants y échapperait.
|
|
1271
|
+
*
|
|
1272
|
+
* @param req - requête d'upgrade
|
|
1273
|
+
* @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
|
|
1274
|
+
* inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
|
|
1275
|
+
* ou `null` si l'upgrade est admis
|
|
1276
|
+
*/
|
|
1277
|
+
websocketUpgradeRefusal(req) {
|
|
1278
|
+
const router = this.router;
|
|
1279
|
+
if (router === null || router === void 0) return 503;
|
|
1280
|
+
let pathname;
|
|
1281
|
+
try {
|
|
1282
|
+
pathname = parseWebsocketUrl("ws", req.headers.host, req.url).pathname;
|
|
1283
|
+
} catch {
|
|
1284
|
+
return this.#rateLimitedRefusal(req) ?? 400;
|
|
1285
|
+
}
|
|
1286
|
+
if (router.servesWebsocket(pathname)) return null;
|
|
1287
|
+
return this.#rateLimitedRefusal(req) ?? 404;
|
|
1288
|
+
}
|
|
1289
|
+
/** Compte un upgrade refusé au limiteur de débit : `429` s'il le dépasse. */
|
|
1290
|
+
#rateLimitedRefusal(req) {
|
|
1291
|
+
if (this.rateLimiter === null) return null;
|
|
1292
|
+
const ip = this.#clientIp(req);
|
|
1293
|
+
return ip !== null && this.rateLimiter.hit(ip).limited ? 429 : null;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
|
|
1297
|
+
* compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
|
|
1298
|
+
* simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
|
|
1299
|
+
* le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
|
|
1300
|
+
* qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
|
|
1301
|
+
* servie.
|
|
1302
|
+
*
|
|
1303
|
+
* L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
|
|
1304
|
+
* un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
|
|
1305
|
+
* par refus : sous un flood, il serait lui-même un amplificateur.
|
|
1306
|
+
*
|
|
1307
|
+
* @param req - requête d'upgrade
|
|
1308
|
+
* @param connection - ce dont la fermeture libère la place comptée (le `ws`
|
|
1309
|
+
* accepté, ou le socket du client pour un tunnel relayé)
|
|
1310
|
+
* @returns `null` si l'upgrade est admis — sa place est alors rendue à la
|
|
1311
|
+
* fermeture de `connection` —, sinon la raison du refus
|
|
1312
|
+
*/
|
|
1313
|
+
websocketQuotaRefusal(req, connection) {
|
|
1314
|
+
if (this.rateLimiter === null && this.#wsConnCounter === null) return null;
|
|
1315
|
+
const ip = this.#clientIp(req);
|
|
1316
|
+
if (ip === null) return null;
|
|
1317
|
+
if (this.rateLimiter?.hit(ip).limited) return "rate limit";
|
|
1318
|
+
return this.#acquireSlot(ip, connection) ? null : "too many connections";
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
|
|
1322
|
+
* `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
|
|
1323
|
+
* un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
|
|
1324
|
+
* Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
|
|
1325
|
+
* à l'entrée du pipeline.
|
|
1326
|
+
*
|
|
1327
|
+
* @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
|
|
1328
|
+
* @param connection - ce dont la fermeture rend la place (la réponse)
|
|
1329
|
+
* @returns `true` si la place est prise — ou si aucun plafond n'est armé
|
|
1330
|
+
*/
|
|
1331
|
+
acquireConnectionSlot(req, connection) {
|
|
1332
|
+
if (this.#wsConnCounter === null) return true;
|
|
1333
|
+
const ip = this.#clientIp(req);
|
|
1334
|
+
return ip === null || this.#acquireSlot(ip, connection);
|
|
1335
|
+
}
|
|
1336
|
+
/** IP cliente, résolue comme en HTTP (RFC 7239 + `trustProxy`). */
|
|
1337
|
+
#clientIp(req) {
|
|
1338
|
+
return resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Compte une connexion de `ip` ; sa place est rendue à la fermeture de
|
|
1342
|
+
* `connection`. Le compteur est capturé : la connexion décrémente TOUJOURS
|
|
1343
|
+
* l'instance qui l'a comptée, même après reconfiguration — et `once("close")`
|
|
1344
|
+
* part à toute fermeture (`terminate` du battement, rejet du pare-feu…).
|
|
1345
|
+
*/
|
|
1346
|
+
#acquireSlot(ip, connection) {
|
|
1347
|
+
const counter = this.#wsConnCounter;
|
|
1348
|
+
if (counter === null) return true;
|
|
1349
|
+
if (!counter.tryAcquire(ip)) return false;
|
|
1350
|
+
connection.once("close", () => counter.release(ip));
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Inscrit un flux SSE à identité révocable : il sera fermé dans la fenêtre de
|
|
1355
|
+
* re-validation qui suit la mort de son identité (déconnexion, session
|
|
1356
|
+
* révoquée, jeton expiré ou annulé) — le même registre et la même règle que
|
|
1357
|
+
* les sockets WebSocket.
|
|
1358
|
+
*
|
|
1359
|
+
* @internal — appelé par le flux lui-même.
|
|
1360
|
+
*/
|
|
1361
|
+
watchStream(stream) {
|
|
1362
|
+
(this.#streams ??= new RevocationWatch({
|
|
1363
|
+
isValid: (entry) => entry.revalidate(),
|
|
1364
|
+
revoke: (entry) => void entry.close(),
|
|
1365
|
+
identityOf: (entry) => entry.identifier ?? void 0
|
|
1366
|
+
})).register(stream);
|
|
1367
|
+
}
|
|
1368
|
+
/** Retire un flux du registre de révocation (à sa fermeture). @internal */
|
|
1369
|
+
unwatchStream(stream) {
|
|
1370
|
+
this.#streams?.unregister(stream);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
|
|
1374
|
+
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
1375
|
+
*/
|
|
1376
|
+
revalidateStreams() {
|
|
1377
|
+
return this.#streams?.revalidate() ?? Promise.resolve();
|
|
1378
|
+
}
|
|
1129
1379
|
};
|
|
1130
|
-
HttpKernel = __decorate([injectable()
|
|
1380
|
+
HttpKernel = __decorate([injectable()], HttpKernel);
|
|
1131
1381
|
var http_kernel_default = HttpKernel;
|
|
1132
|
-
|
|
1133
|
-
export { http_kernel_default as default };
|
|
1382
|
+
export { http_kernel_default as default, responseEnded };
|