@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.3
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.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/config/config.js +44 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +140 -15
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +1 -9
- package/dist/nodefony/src/context/http/Response.js +16 -1
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +96 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- 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 +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +5 -1
- package/docs/observabilite.md +15 -15
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +94 -60
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +23 -23
- package/package.json +8 -8
|
@@ -4,6 +4,8 @@ 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 { responseEnded } from "../src/context/responseEnded.js";
|
|
8
|
+
import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
|
|
7
9
|
import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
|
|
8
10
|
import HttpContext from "../src/context/http/HttpContext.js";
|
|
9
11
|
import DefaultErrorRenderer from "./error-renderer.js";
|
|
@@ -12,9 +14,9 @@ import JsonAuditLogger from "./audit-logger.js";
|
|
|
12
14
|
import PrettyRequestLogger from "./pretty-request-logger.js";
|
|
13
15
|
import { resolveTraceparent } from "./trace.js";
|
|
14
16
|
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
15
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
16
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
17
|
-
import { GcScheduler, Module, RequestContext, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
17
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
18
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
19
|
+
import { GcScheduler, IDENTITY_CHANGED_EVENT, Module, 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
22
|
import Ws from "ws";
|
|
@@ -67,13 +69,37 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
67
69
|
cert = "";
|
|
68
70
|
ca = "";
|
|
69
71
|
serverStatic = null;
|
|
72
|
+
/**
|
|
73
|
+
* Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
|
|
74
|
+
* `mounts === null` tant que personne n'a monté de préfixe.
|
|
75
|
+
*/
|
|
76
|
+
reverseProxy = null;
|
|
70
77
|
domain = "";
|
|
71
78
|
trustedHosts;
|
|
72
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Contrôle de domaine actif (`domainCheck` de la config d'application) —
|
|
81
|
+
* lu à la source, pour le HTTP comme pour le handshake WebSocket.
|
|
82
|
+
*/
|
|
83
|
+
get domainCheck() {
|
|
84
|
+
return this.kernel?.options.domainCheck === true;
|
|
85
|
+
}
|
|
73
86
|
regAlias = [];
|
|
74
87
|
module;
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
|
|
90
|
+
* lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
|
|
91
|
+
*/
|
|
92
|
+
get httpsPort() {
|
|
93
|
+
return this.boundPort("server-https");
|
|
94
|
+
}
|
|
95
|
+
/** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
|
|
96
|
+
get httpPort() {
|
|
97
|
+
return this.boundPort("server-http");
|
|
98
|
+
}
|
|
99
|
+
boundPort(name) {
|
|
100
|
+
const server = this.get(name);
|
|
101
|
+
return server?.active === true && server.port > 0 ? server.port : void 0;
|
|
102
|
+
}
|
|
77
103
|
responseTimeout;
|
|
78
104
|
closeTimeOutWs;
|
|
79
105
|
secContentTypeOptions = null;
|
|
@@ -90,6 +116,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
90
116
|
rateLimiter = null;
|
|
91
117
|
rateLimitGc = null;
|
|
92
118
|
#wsConnCounter = null;
|
|
119
|
+
#streams = null;
|
|
93
120
|
#healthPaths = null;
|
|
94
121
|
#healthShutdownDelay = 0;
|
|
95
122
|
#terminating = false;
|
|
@@ -108,6 +135,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
108
135
|
wss: this.options.websocketSecure.closeTimeout
|
|
109
136
|
};
|
|
110
137
|
this.computeSecurityHeaderCaches();
|
|
138
|
+
this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
|
|
139
|
+
this.#streams?.refreshIdentity(event.identifier);
|
|
140
|
+
});
|
|
111
141
|
}
|
|
112
142
|
/**
|
|
113
143
|
* (Re)calcule les caches dérivés des en-têtes de sécurité depuis `this.options`
|
|
@@ -244,6 +274,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
244
274
|
this.kernel?.prependOnceListener("onReady", () => {
|
|
245
275
|
this.serviceCerticats = this.get("certificates");
|
|
246
276
|
this.serverStatic = this.get("server-static");
|
|
277
|
+
this.reverseProxy = this.get("reverse-proxy");
|
|
247
278
|
this.domain = this.kernel?.domain;
|
|
248
279
|
this.trustedHosts = this.options.trustedHosts;
|
|
249
280
|
this.regAlias = this.compileAlias();
|
|
@@ -307,6 +338,35 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
307
338
|
return policy;
|
|
308
339
|
}
|
|
309
340
|
/**
|
|
341
|
+
* L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
|
|
342
|
+
* interrogeable SANS contexte : le serveur WebSocket de Nodefony
|
|
343
|
+
* ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
|
|
344
|
+
* avant qu'aucun contexte n'existe, décident par elle.
|
|
345
|
+
*
|
|
346
|
+
* Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
|
|
347
|
+
* en development, allowlist `allowedOrigins` ; sans `Origin` (client
|
|
348
|
+
* non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
|
|
349
|
+
*
|
|
350
|
+
* @param origin - valeur brute de l'en-tête `Origin`
|
|
351
|
+
* @param hostname - nom d'hôte servi, sans port (`Context.domain`)
|
|
352
|
+
* @param secure - handshake reçu en TLS (politique `websocketSecure`)
|
|
353
|
+
* @returns `true` si le handshake peut se poursuivre
|
|
354
|
+
*/
|
|
355
|
+
isWebsocketOriginAllowed(origin, hostname, secure) {
|
|
356
|
+
const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
|
|
357
|
+
if (policy.disabled) return true;
|
|
358
|
+
if (!origin) return true;
|
|
359
|
+
let originHost = null;
|
|
360
|
+
try {
|
|
361
|
+
originHost = new URL(origin).hostname;
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (originHost === hostname) return true;
|
|
366
|
+
if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return true;
|
|
367
|
+
return policy.extra.length > 0 && isDomainAllowed(policy.extra, originHost);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
310
370
|
* B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
|
|
311
371
|
* WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
|
|
312
372
|
* ce contrôle, une page tierce peut ouvrir un WS **authentifié par le cookie de
|
|
@@ -322,22 +382,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
322
382
|
* @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
|
|
323
383
|
*/
|
|
324
384
|
checkWebsocketOrigin(context) {
|
|
325
|
-
const
|
|
326
|
-
const policy = this.getWsOriginPolicy(cfgKey);
|
|
327
|
-
if (policy.disabled) return;
|
|
385
|
+
const secure = context.type === "websocket-secure";
|
|
328
386
|
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
|
-
}
|
|
387
|
+
if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
|
|
341
388
|
const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
|
|
342
389
|
error.type = "origin";
|
|
343
390
|
throw error;
|
|
@@ -483,7 +530,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
483
530
|
*/
|
|
484
531
|
/**
|
|
485
532
|
* Override the default error renderer — e.g. to hide stack traces in prod
|
|
486
|
-
* or emit RFC
|
|
533
|
+
* or emit RFC 9457 problem+json. Stateless singleton expected.
|
|
487
534
|
*/
|
|
488
535
|
setErrorRenderer(renderer) {
|
|
489
536
|
this.errorRenderer = renderer;
|
|
@@ -504,6 +551,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
504
551
|
async onError(error, context, _extraHeaders) {
|
|
505
552
|
if (context) context.error = error;
|
|
506
553
|
if (context instanceof HttpContext) {
|
|
554
|
+
if (context.isRedirect && error.code === 401) {
|
|
555
|
+
if (!(context.finished || context.sended)) await context.send();
|
|
556
|
+
return context;
|
|
557
|
+
}
|
|
507
558
|
const result = this.errorRenderer.renderHttp(error, context);
|
|
508
559
|
if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
|
|
509
560
|
context.response.setStatusCode(result.status, http.STATUS_CODES[result.status] ?? "Unknown Error");
|
|
@@ -514,6 +565,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
514
565
|
return context;
|
|
515
566
|
}
|
|
516
567
|
if (!context.response.isHeaderSent()) {
|
|
568
|
+
if (context.isRedirect) {
|
|
569
|
+
context.isRedirect = false;
|
|
570
|
+
context.response.response?.removeHeader("Location");
|
|
571
|
+
}
|
|
517
572
|
try {
|
|
518
573
|
await context.render(result.body);
|
|
519
574
|
} catch (e) {
|
|
@@ -799,7 +854,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
799
854
|
perfProbe.t0 = 0n;
|
|
800
855
|
} else context = new HttpContext(scope, request, response, type);
|
|
801
856
|
response.on("close", () => {
|
|
802
|
-
if (!response
|
|
857
|
+
if (!responseEnded(response)) {
|
|
803
858
|
context._abortIfPending("Connection closed before response finished");
|
|
804
859
|
if (!context.sended) context.response.statusCode = 499;
|
|
805
860
|
}
|
|
@@ -842,14 +897,16 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
842
897
|
const profilerQueries = this.profiler ? [] : null;
|
|
843
898
|
context.profilerQueries = profilerQueries;
|
|
844
899
|
context.profiling = profilerQueries !== null;
|
|
845
|
-
|
|
900
|
+
const store = {
|
|
846
901
|
requestId: context.requestId,
|
|
847
902
|
scheme: context.scheme,
|
|
848
903
|
traceparent: context.traceparent,
|
|
849
904
|
queries: profilerQueries ?? void 0,
|
|
850
905
|
context,
|
|
851
906
|
scope
|
|
852
|
-
}
|
|
907
|
+
};
|
|
908
|
+
context.holdRequestStore(store);
|
|
909
|
+
return RequestContext.run(store, () => this.runHttpPipeline(context, request, response));
|
|
853
910
|
}
|
|
854
911
|
/**
|
|
855
912
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
@@ -887,6 +944,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
887
944
|
* N'attend que le repli statique, et seulement quand aucune route ne matche.
|
|
888
945
|
*/
|
|
889
946
|
routeHttpRequest(httpContext, request, response) {
|
|
947
|
+
if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
|
|
948
|
+
const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
|
|
949
|
+
if (relayed !== void 0) return relayed.then(() => httpContext);
|
|
950
|
+
}
|
|
890
951
|
httpContext.phaseStart("resolve");
|
|
891
952
|
httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
|
|
892
953
|
httpContext.phaseEnd("resolve");
|
|
@@ -931,7 +992,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
931
992
|
if (error) throw error;
|
|
932
993
|
const schemeHeaders = context.scheme === "https" ? this.options.https.headers : context.scheme === "http" ? this.options.http.headers : null;
|
|
933
994
|
if (schemeHeaders) context.response.setHeaders(schemeHeaders);
|
|
934
|
-
if (this.
|
|
995
|
+
if (this.domainCheck) this.checkValidDomain(context);
|
|
935
996
|
if (this.listenerCount("beforeResolve")) return this.fireAsync("beforeResolve", context).then(() => this.armAndGuard(context));
|
|
936
997
|
return this.armAndGuard(context);
|
|
937
998
|
}
|
|
@@ -942,7 +1003,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
942
1003
|
armAndGuard(context) {
|
|
943
1004
|
this.prepareFrontController(context);
|
|
944
1005
|
this.firewall?.enforceCsrf(context);
|
|
945
|
-
return thenMaybe(this.startSession(context), () => context.secure
|
|
1006
|
+
return thenMaybe(this.startSession(context), () => context.secure ? this.runFirewall(context) : context);
|
|
946
1007
|
}
|
|
947
1008
|
/**
|
|
948
1009
|
* Le pare-feu d'une requête en zone protégée : `handleSecurity`, puis les
|
|
@@ -997,22 +1058,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
997
1058
|
}
|
|
998
1059
|
}
|
|
999
1060
|
async onWebsocketRequest(ws, req, type) {
|
|
1000
|
-
const
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (this.rateLimiter?.hit(ip).limited) {
|
|
1005
|
-
if (ws.readyState === Ws.OPEN) ws.close(1013, "rate limit");
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
if (wsConnCounter !== null) {
|
|
1009
|
-
if (!wsConnCounter.tryAcquire(ip)) {
|
|
1010
|
-
if (ws.readyState === Ws.OPEN) ws.close(1013, "too many connections");
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
ws.once("close", () => wsConnCounter.release(ip));
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1061
|
+
const refusal = this.websocketQuotaRefusal(req, ws);
|
|
1062
|
+
if (refusal !== null) {
|
|
1063
|
+
if (ws.readyState === Ws.OPEN) ws.close(1013, refusal);
|
|
1064
|
+
return;
|
|
1016
1065
|
}
|
|
1017
1066
|
await this.fireAsync("onServerRequest", req, null, type);
|
|
1018
1067
|
const scope = this.container?.enterScope("request");
|
|
@@ -1034,15 +1083,17 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1034
1083
|
const wsRunId = context?.requestId ?? "ws-no-ctx";
|
|
1035
1084
|
const wsScheme = context?.scheme ?? "ws";
|
|
1036
1085
|
const wsTrace = context?.traceparent ?? null;
|
|
1037
|
-
|
|
1086
|
+
const store = {
|
|
1038
1087
|
requestId: wsRunId,
|
|
1039
1088
|
scheme: wsScheme,
|
|
1040
1089
|
...wsTrace ? { traceparent: wsTrace } : {},
|
|
1041
1090
|
context,
|
|
1042
1091
|
scope
|
|
1043
|
-
}
|
|
1092
|
+
};
|
|
1093
|
+
context?.holdRequestStore(store);
|
|
1094
|
+
return await RequestContext.run(store, async () => {
|
|
1044
1095
|
await this.onConnect(context, error);
|
|
1045
|
-
if (this.firewall &&
|
|
1096
|
+
if (this.firewall && context?.secure) {
|
|
1046
1097
|
context.phaseStart("firewall");
|
|
1047
1098
|
try {
|
|
1048
1099
|
try {
|
|
@@ -1126,8 +1177,131 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1126
1177
|
isTrustedHostname(hostname) {
|
|
1127
1178
|
return isDomainAllowed(this.regAlias, hostname);
|
|
1128
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
|
|
1182
|
+
* (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
|
|
1183
|
+
* inverse et avant le serveur `ws`.
|
|
1184
|
+
*
|
|
1185
|
+
* Pourquoi avant : accepter puis fermer fait croire au client que le serveur
|
|
1186
|
+
* a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
|
|
1187
|
+
* une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
|
|
1188
|
+
* recharger la page avant que Vite soit prêt — page blanche.
|
|
1189
|
+
*
|
|
1190
|
+
* Ne juge que l'existence d'une route WebSocket pour le chemin
|
|
1191
|
+
* ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
|
|
1192
|
+
* quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
|
|
1193
|
+
* compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
|
|
1194
|
+
* flot d'upgrades vers des chemins inexistants y échapperait.
|
|
1195
|
+
*
|
|
1196
|
+
* @param req - requête d'upgrade
|
|
1197
|
+
* @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
|
|
1198
|
+
* inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
|
|
1199
|
+
* ou `null` si l'upgrade est admis
|
|
1200
|
+
*/
|
|
1201
|
+
websocketUpgradeRefusal(req) {
|
|
1202
|
+
const router = this.router;
|
|
1203
|
+
if (router === null || router === void 0) return 503;
|
|
1204
|
+
let pathname;
|
|
1205
|
+
try {
|
|
1206
|
+
pathname = parseWebsocketUrl("ws", req.headers.host, req.url).pathname;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return this.#rateLimitedRefusal(req) ?? 400;
|
|
1209
|
+
}
|
|
1210
|
+
if (router.servesWebsocket(pathname)) return null;
|
|
1211
|
+
return this.#rateLimitedRefusal(req) ?? 404;
|
|
1212
|
+
}
|
|
1213
|
+
/** Compte un upgrade refusé au limiteur de débit : `429` s'il le dépasse. */
|
|
1214
|
+
#rateLimitedRefusal(req) {
|
|
1215
|
+
if (this.rateLimiter === null) return null;
|
|
1216
|
+
const ip = this.#clientIp(req);
|
|
1217
|
+
return ip !== null && this.rateLimiter.hit(ip).limited ? 429 : null;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
|
|
1221
|
+
* compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
|
|
1222
|
+
* simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
|
|
1223
|
+
* le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
|
|
1224
|
+
* qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
|
|
1225
|
+
* servie.
|
|
1226
|
+
*
|
|
1227
|
+
* L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
|
|
1228
|
+
* un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
|
|
1229
|
+
* par refus : sous un flood, il serait lui-même un amplificateur.
|
|
1230
|
+
*
|
|
1231
|
+
* @param req - requête d'upgrade
|
|
1232
|
+
* @param connection - ce dont la fermeture libère la place comptée (le `ws`
|
|
1233
|
+
* accepté, ou le socket du client pour un tunnel relayé)
|
|
1234
|
+
* @returns `null` si l'upgrade est admis — sa place est alors rendue à la
|
|
1235
|
+
* fermeture de `connection` —, sinon la raison du refus
|
|
1236
|
+
*/
|
|
1237
|
+
websocketQuotaRefusal(req, connection) {
|
|
1238
|
+
if (this.rateLimiter === null && this.#wsConnCounter === null) return null;
|
|
1239
|
+
const ip = this.#clientIp(req);
|
|
1240
|
+
if (ip === null) return null;
|
|
1241
|
+
if (this.rateLimiter?.hit(ip).limited) return "rate limit";
|
|
1242
|
+
return this.#acquireSlot(ip, connection) ? null : "too many connections";
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
|
|
1246
|
+
* `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
|
|
1247
|
+
* un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
|
|
1248
|
+
* Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
|
|
1249
|
+
* à l'entrée du pipeline.
|
|
1250
|
+
*
|
|
1251
|
+
* @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
|
|
1252
|
+
* @param connection - ce dont la fermeture rend la place (la réponse)
|
|
1253
|
+
* @returns `true` si la place est prise — ou si aucun plafond n'est armé
|
|
1254
|
+
*/
|
|
1255
|
+
acquireConnectionSlot(req, connection) {
|
|
1256
|
+
if (this.#wsConnCounter === null) return true;
|
|
1257
|
+
const ip = this.#clientIp(req);
|
|
1258
|
+
return ip === null || this.#acquireSlot(ip, connection);
|
|
1259
|
+
}
|
|
1260
|
+
/** IP cliente, résolue comme en HTTP (RFC 7239 + `trustProxy`). */
|
|
1261
|
+
#clientIp(req) {
|
|
1262
|
+
return resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Compte une connexion de `ip` ; sa place est rendue à la fermeture de
|
|
1266
|
+
* `connection`. Le compteur est capturé : la connexion décrémente TOUJOURS
|
|
1267
|
+
* l'instance qui l'a comptée, même après reconfiguration — et `once("close")`
|
|
1268
|
+
* part à toute fermeture (`terminate` du battement, rejet du pare-feu…).
|
|
1269
|
+
*/
|
|
1270
|
+
#acquireSlot(ip, connection) {
|
|
1271
|
+
const counter = this.#wsConnCounter;
|
|
1272
|
+
if (counter === null) return true;
|
|
1273
|
+
if (!counter.tryAcquire(ip)) return false;
|
|
1274
|
+
connection.once("close", () => counter.release(ip));
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Inscrit un flux SSE à identité révocable : il sera fermé dans la fenêtre de
|
|
1279
|
+
* re-validation qui suit la mort de son identité (déconnexion, session
|
|
1280
|
+
* révoquée, jeton expiré ou annulé) — le même registre et la même règle que
|
|
1281
|
+
* les sockets WebSocket.
|
|
1282
|
+
*
|
|
1283
|
+
* @internal — appelé par le flux lui-même.
|
|
1284
|
+
*/
|
|
1285
|
+
watchStream(stream) {
|
|
1286
|
+
(this.#streams ??= new RevocationWatch({
|
|
1287
|
+
isValid: (entry) => entry.revalidate(),
|
|
1288
|
+
revoke: (entry) => void entry.close(),
|
|
1289
|
+
identityOf: (entry) => entry.identifier ?? void 0
|
|
1290
|
+
})).register(stream);
|
|
1291
|
+
}
|
|
1292
|
+
/** Retire un flux du registre de révocation (à sa fermeture). @internal */
|
|
1293
|
+
unwatchStream(stream) {
|
|
1294
|
+
this.#streams?.unregister(stream);
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
|
|
1298
|
+
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
1299
|
+
*/
|
|
1300
|
+
revalidateStreams() {
|
|
1301
|
+
return this.#streams?.revalidate() ?? Promise.resolve();
|
|
1302
|
+
}
|
|
1129
1303
|
};
|
|
1130
1304
|
HttpKernel = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], HttpKernel);
|
|
1131
1305
|
var http_kernel_default = HttpKernel;
|
|
1132
1306
|
//#endregion
|
|
1133
|
-
export { http_kernel_default as default };
|
|
1307
|
+
export { http_kernel_default as default, responseEnded };
|