@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { refuseUpgrade } from "../proxy/forward.js";
|
|
2
|
+
import { STATUS_CODES } from "node:http";
|
|
3
|
+
//#region nodefony/src/servers/upgradeDispatch.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
|
|
6
|
+
* puis le serveur WebSocket de Nodefony.
|
|
7
|
+
*
|
|
8
|
+
* Pourquoi un seul écouteur : `ws` attaché par son option `server` prend TOUS
|
|
9
|
+
* les upgrades et répond `400` à ceux qu'il ne reconnaît pas. Un second
|
|
10
|
+
* écouteur, même posé avant, ne l'empêcherait pas de répondre sur un socket
|
|
11
|
+
* déjà relayé. Le serveur `ws` est donc créé en `noServer`, et c'est ici que
|
|
12
|
+
* l'upgrade lui est confié — avec ses propres contrôles intacts (`path`,
|
|
13
|
+
* `verifyClient`, `maxPayload`…), que `handleUpgrade` applique.
|
|
14
|
+
*
|
|
15
|
+
* Entre les deux, `refusal` tranche ce qui doit être refusé AVANT le `101`
|
|
16
|
+
* (RFC 6455 §4.2.2) : un chemin sans route WebSocket reçoit un statut HTTP,
|
|
17
|
+
* jamais une ouverture suivie d'une fermeture.
|
|
18
|
+
*
|
|
19
|
+
* Sans montage, le coût par connexion est une lecture de champ.
|
|
20
|
+
*
|
|
21
|
+
* @param server - serveur HTTP(S) qui reçoit les upgrades
|
|
22
|
+
* @param wss - serveur WebSocket de Nodefony (`noServer: true`)
|
|
23
|
+
* @param relay - proxy inverse, lu à chaque upgrade (il peut naître après)
|
|
24
|
+
* @param refusal - statut HTTP qui refuse l'upgrade, `null` pour l'admettre
|
|
25
|
+
* (cf `HttpKernel.websocketUpgradeRefusal`)
|
|
26
|
+
* @returns la fonction qui retire l'écouteur — à appeler à l'arrêt
|
|
27
|
+
*/
|
|
28
|
+
function attachUpgradeDispatch(server, wss, relay, refusal) {
|
|
29
|
+
const onUpgrade = (req, socket, head) => {
|
|
30
|
+
const proxy = relay();
|
|
31
|
+
if (proxy !== null && proxy.mounts !== null) {
|
|
32
|
+
if (proxy.handleUpgrade(req, socket, head)) return;
|
|
33
|
+
}
|
|
34
|
+
const status = refusal(req);
|
|
35
|
+
if (status !== null) {
|
|
36
|
+
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
40
|
+
wss.emit("connection", ws, req);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
server.on("upgrade", onUpgrade);
|
|
44
|
+
return () => {
|
|
45
|
+
server.removeListener("upgrade", onUpgrade);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { attachUpgradeDispatch };
|
|
@@ -9,6 +9,20 @@ const defaultSessionOptions = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Taille de l'identifiant opaque (octets CSPRNG → base64url, 43 chars). */
|
|
11
11
|
const SESSION_ID_BYTES = 32;
|
|
12
|
+
/** Un sac de session relu du storage : objet, ni `null` ni tableau. */
|
|
13
|
+
function isBag(v) {
|
|
14
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Une date relue du storage : `Date` d'un store mémoire, chaîne ou nombre d'un
|
|
18
|
+
* store sérialisé. Toute autre forme → `undefined` (la valeur courante reste).
|
|
19
|
+
* Convertie UNE fois à la lecture : `created`/`updated` sont alors toujours des
|
|
20
|
+
* `Date`, et les contrôles du chemin chaud lisent `getTime()` sans allouer.
|
|
21
|
+
*/
|
|
22
|
+
function toDate(v) {
|
|
23
|
+
if (v instanceof Date) return v;
|
|
24
|
+
return typeof v === "string" || typeof v === "number" ? new Date(v) : void 0;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Session serveur Nodefony — état persistant lié à une identité, indexé par un
|
|
14
28
|
* identifiant **opaque** (porté par le cookie) dans un {@link ISessionStorage}
|
|
@@ -157,16 +171,19 @@ var Session = class {
|
|
|
157
171
|
* `onSaveSession`.
|
|
158
172
|
*
|
|
159
173
|
* @param user - principal authentifié (string) lié au blob ; défaut courant.
|
|
174
|
+
* Fourni, il DEVIENT le principal de la session : une sauvegarde suivante,
|
|
175
|
+
* dans la même requête, le garde sans qu'on le lui redonne.
|
|
160
176
|
*/
|
|
161
177
|
async save(user) {
|
|
178
|
+
if (user !== void 0) this.user = user;
|
|
162
179
|
if (this.readOnly) {
|
|
163
180
|
if (this.mutated) this.log(`READONLY SESSION mutated — write skipped : ${this.name}`, "WARNING");
|
|
164
181
|
return this;
|
|
165
182
|
}
|
|
166
183
|
if (!this.mutated) return this;
|
|
167
184
|
const stored = await this.storage.write(this.id, this.serialize(user));
|
|
168
|
-
this.created = stored.createdAt ?? this.created;
|
|
169
|
-
this.updated = stored.updatedAt ?? this.updated;
|
|
185
|
+
this.created = toDate(stored.createdAt) ?? this.created;
|
|
186
|
+
this.updated = toDate(stored.updatedAt) ?? this.updated;
|
|
170
187
|
this.mutated = false;
|
|
171
188
|
this.saved = true;
|
|
172
189
|
if (this.context) await this.context.fireAsync("onSaveSession", this);
|
|
@@ -234,15 +251,14 @@ var Session = class {
|
|
|
234
251
|
}
|
|
235
252
|
const now = Date.now();
|
|
236
253
|
if (this.options.absoluteTimeoutS && this.created) {
|
|
237
|
-
if (now -
|
|
254
|
+
if (!(now - this.created.getTime() <= this.options.absoluteTimeoutS * 1e3)) {
|
|
238
255
|
this.log(`SESSION EXPIRED (absolute) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
239
256
|
return false;
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
259
|
const idleS = this.options.idleTimeoutS;
|
|
243
260
|
if (idleS && idleS > 0 && this.updated) {
|
|
244
|
-
|
|
245
|
-
if (lastUsed && lastUsed + idleS * 1e3 < now) {
|
|
261
|
+
if (!(this.updated.getTime() + idleS * 1e3 >= now)) {
|
|
246
262
|
this.log(`SESSION EXPIRED (idle) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
247
263
|
return false;
|
|
248
264
|
}
|
|
@@ -270,7 +286,7 @@ var Session = class {
|
|
|
270
286
|
if (typeof storage.touch !== "function") return;
|
|
271
287
|
if (!this.updated) return;
|
|
272
288
|
const now = Date.now();
|
|
273
|
-
const lastUsed =
|
|
289
|
+
const lastUsed = this.updated.getTime();
|
|
274
290
|
if (lastUsed && now - lastUsed < idleS * 1e3 / 2) return;
|
|
275
291
|
await storage.touch(this.id, idleS);
|
|
276
292
|
this.updated = new Date(now);
|
|
@@ -356,12 +372,12 @@ var Session = class {
|
|
|
356
372
|
}
|
|
357
373
|
deSerialize(serialized) {
|
|
358
374
|
const data = serialized;
|
|
359
|
-
if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
360
|
-
if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
361
|
-
if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
362
|
-
this.created = data.createdAt ?? this.created;
|
|
363
|
-
this.updated = data.updatedAt ?? this.updated;
|
|
364
|
-
if (data.user) this.user = data.user;
|
|
375
|
+
if (isBag(data.Attributes)) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
376
|
+
if (isBag(data.metaBag)) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
377
|
+
if (isBag(data.flashBag)) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
378
|
+
this.created = toDate(data.createdAt) ?? this.created;
|
|
379
|
+
this.updated = toDate(data.updatedAt) ?? this.updated;
|
|
380
|
+
if (typeof data.user === "string" && data.user) this.user = data.user;
|
|
365
381
|
}
|
|
366
382
|
/** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
|
|
367
383
|
clear() {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -76,10 +76,15 @@ export type { DomainPattern, ITrustedHostsConfig, } from "./nodefony/src/context
|
|
|
76
76
|
export { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
77
77
|
export type { IRateLimitStore, IRateLimitOptions, RateLimitVerdict, } from "./nodefony/src/rateLimit/IRateLimitStore.js";
|
|
78
78
|
export { Context, Session, SessionsService, HttpError, HttpKernel, Response, Http2Response, Response as HttpResponse, wsResponse, wsResponse as WebsocketResponse, Cookie, HttpContext, WebsocketContext, HttpRequest, Http2Request, DefaultErrorRenderer, DefaultRequestLogger, JsonAuditLogger, PrettyRequestLogger, Profiler, };
|
|
79
|
+
export { SseStream, openSseStream, acceptsEventStream, SSE_HEARTBEAT_MS, } from "./nodefony/src/context/http/SseStream.js";
|
|
80
|
+
export type { ISseStreamOptions, ISseSendOptions, } from "./nodefony/src/context/http/SseStream.js";
|
|
79
81
|
export { schemaMismatchOf, type SchemaMismatchConfidence, } from "./nodefony/service/error-renderer.js";
|
|
80
82
|
export type { ProfileEntry, ProfileSummary, ProfilePhase, ProfileQuery, ProfileSecurity, } from "./nodefony/src/profiler/Profiler.js";
|
|
81
83
|
export { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
|
|
82
84
|
export type { FrameProfileInit, ProfiledArea, ProfiledResolver, } from "./nodefony/src/profiler/FrameProfile.js";
|
|
85
|
+
export { default as ReverseProxy } from "./nodefony/service/reverse-proxy.js";
|
|
86
|
+
export { parseProxyOrigin, normalizePrefix, proxyMountProblems, } from "./nodefony/src/proxy/rules.js";
|
|
87
|
+
export type { IProxyMount, IProxyMountOptions, ProxyTarget, } from "./nodefony/interfaces/IReverseProxy.js";
|
|
83
88
|
export { resolveUiDelivery, PrebuiltUi, } from "./nodefony/src/assets/prebuiltUi.js";
|
|
84
89
|
export type { UiDeliveryMode, UiDeliveryResolved, IUiDeliveryResolution, IUiDeliveryOptions, IPrebuiltUiOptions, } from "./nodefony/src/assets/prebuiltUi.js";
|
|
85
90
|
export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType, WebSocketStateType, CookiesMap, ISecurityTrace, SecurityOutcome, } from "./nodefony/interfaces/IContext.js";
|
|
@@ -67,6 +67,7 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
67
67
|
key: z.ZodDefault<z.ZodString>;
|
|
68
68
|
cert: z.ZodDefault<z.ZodString>;
|
|
69
69
|
privateKeyMode: z.ZodDefault<z.ZodNumber>;
|
|
70
|
+
extraCaFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
70
71
|
san: z.ZodDefault<z.ZodObject<{
|
|
71
72
|
dns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
72
73
|
ip: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -184,6 +185,21 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
184
185
|
readinessPath: z.ZodDefault<z.ZodString>;
|
|
185
186
|
shutdownDelay: z.ZodDefault<z.ZodNumber>;
|
|
186
187
|
}, z.core.$loose>>;
|
|
188
|
+
proxy: z.ZodDefault<z.ZodObject<{
|
|
189
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
190
|
+
connectTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
191
|
+
maxSockets: z.ZodDefault<z.ZodNumber>;
|
|
192
|
+
mounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
193
|
+
target: z.ZodString;
|
|
194
|
+
methods: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
195
|
+
websocket: z.ZodDefault<z.ZodBoolean>;
|
|
196
|
+
stripPrefix: z.ZodDefault<z.ZodBoolean>;
|
|
197
|
+
preserveHost: z.ZodDefault<z.ZodBoolean>;
|
|
198
|
+
stripHeaders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
199
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
200
|
+
secure: z.ZodDefault<z.ZodBoolean>;
|
|
201
|
+
}, z.core.$strict>>>;
|
|
202
|
+
}, z.core.$strict>>;
|
|
187
203
|
wsMaxConnectionsPerIp: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
188
204
|
}, z.core.$strict>;
|
|
189
205
|
/** Type de sortie (config normalisée + défauts appliqués). */
|
|
@@ -86,6 +86,8 @@ export interface IContext {
|
|
|
86
86
|
cookieSession: ICookie | null | undefined;
|
|
87
87
|
cookies: CookiesMap;
|
|
88
88
|
user: unknown;
|
|
89
|
+
/** Identifiant de l'appelant authentifié (pare-feu), lisible au démontage. */
|
|
90
|
+
userIdentifier: string | null;
|
|
89
91
|
error: Error | null | undefined;
|
|
90
92
|
metaData: object;
|
|
91
93
|
readonly phases: PhaseTiming[];
|
|
@@ -46,7 +46,7 @@ export interface IErrorWebsocketResult {
|
|
|
46
46
|
*
|
|
47
47
|
* Override via `httpKernel.setErrorRenderer(custom)` to customise:
|
|
48
48
|
* - hide stack traces in prod
|
|
49
|
-
* - emit RFC
|
|
49
|
+
* - emit RFC 9457 problem+json
|
|
50
50
|
* - inject auth challenge headers
|
|
51
51
|
*/
|
|
52
52
|
export interface IErrorRenderer {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origine d'un amont relayé : `scheme://hôte[:port]`, sans chemin.
|
|
3
|
+
*
|
|
4
|
+
* Une fonction quand l'origine n'est connue qu'à l'exécution (un serveur
|
|
5
|
+
* enfant qui choisit son port au démarrage) : elle est rappelée à chaque
|
|
6
|
+
* requête, et `undefined` signifie « l'amont n'est pas prêt » — la requête
|
|
7
|
+
* reçoit `503` (`Retry-After: 1`), l'upgrade WebSocket `503` avant tout `101` :
|
|
8
|
+
* le préfixe appartient à l'amont, le routage n'a rien à y répondre.
|
|
9
|
+
*/
|
|
10
|
+
export type ProxyTarget = string | (() => string | undefined);
|
|
11
|
+
/**
|
|
12
|
+
* Réglages d'un montage du proxy inverse (`ReverseProxy.mount`).
|
|
13
|
+
*
|
|
14
|
+
* Les défauts sont ceux d'un relais sans surprise : toutes les méthodes, le
|
|
15
|
+
* chemin transmis tel quel, l'en-tête `Host` réécrit sur la cible, aucun
|
|
16
|
+
* en-tête retiré au-delà des en-têtes de connexion, certificat de la cible
|
|
17
|
+
* vérifié.
|
|
18
|
+
*/
|
|
19
|
+
export interface IProxyMountOptions {
|
|
20
|
+
/** Origine de l'amont (cf {@link ProxyTarget}). */
|
|
21
|
+
target: ProxyTarget;
|
|
22
|
+
/**
|
|
23
|
+
* Méthodes relayées (majuscules). Une autre méthode n'est PAS relayée : la
|
|
24
|
+
* requête poursuit jusqu'au routage. Absent = toutes.
|
|
25
|
+
*/
|
|
26
|
+
methods?: readonly string[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Relaie aussi l'upgrade WebSocket du préfixe. Sans ce drapeau, un upgrade
|
|
29
|
+
* sur le préfixe reste au serveur WebSocket de Nodefony.
|
|
30
|
+
*/
|
|
31
|
+
websocket?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Retire le préfixe avant de transmettre le chemin (`/svc/a` → `/a`).
|
|
34
|
+
* Défaut `false` : l'amont reçoit le chemin complet — c'est le cas d'un
|
|
35
|
+
* serveur qui connaît déjà son préfixe (Vite et son `base`).
|
|
36
|
+
*/
|
|
37
|
+
stripPrefix?: boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Garde l'en-tête `Host` du client au lieu de le réécrire sur la cible.
|
|
40
|
+
* Défaut `false` : un amont qui filtre les noms d'hôte (Vite et son
|
|
41
|
+
* `allowedHosts`) reçoit le sien, et le nom d'origine part dans
|
|
42
|
+
* `X-Forwarded-Host`.
|
|
43
|
+
*/
|
|
44
|
+
preserveHost?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* En-têtes de requête à ne pas transmettre (insensible à la casse) — ce
|
|
47
|
+
* dont l'amont n'a pas l'usage ne doit pas lui parvenir (`cookie`,
|
|
48
|
+
* `authorization` vers un serveur de sources, par exemple).
|
|
49
|
+
*/
|
|
50
|
+
stripHeaders?: readonly string[] | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Délai d'inactivité de l'échange avec l'amont, en ms. Dépassé avant la
|
|
53
|
+
* réponse : 504. Absent = `proxy.timeoutMs` de la configuration (30 s par
|
|
54
|
+
* défaut). Ne s'applique pas à un WebSocket établi.
|
|
55
|
+
*/
|
|
56
|
+
timeoutMs?: number | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Vérifie le certificat d'une cible `https:`. Défaut `true` — `false` n'est
|
|
59
|
+
* ACCEPTÉ que vers la boucle locale (refus au montage sinon).
|
|
60
|
+
*/
|
|
61
|
+
secure?: boolean | undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Un montage tel que le proxy le garde : préfixe normalisé + réglages. */
|
|
64
|
+
export interface IProxyMount extends IProxyMountOptions {
|
|
65
|
+
/** Préfixe d'URL normalisé (`/` en tête et en fin). */
|
|
66
|
+
readonly prefix: string;
|
|
67
|
+
}
|
|
@@ -75,6 +75,22 @@ export interface IRequestRouter {
|
|
|
75
75
|
* @returns le résolveur (`resolve === true` si une route a matché)
|
|
76
76
|
*/
|
|
77
77
|
resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): IRouteResolver;
|
|
78
|
+
/**
|
|
79
|
+
* Dit si au moins une route déclare le transport `WEBSOCKET` pour ce chemin.
|
|
80
|
+
*
|
|
81
|
+
* Lu AVANT le `101` : une socket vers un chemin sans route se refuse par un
|
|
82
|
+
* statut HTTP (RFC 6455 §4.2.2), jamais par une ouverture suivie d'une
|
|
83
|
+
* fermeture — un client qui sonde par une ouverture conclurait que le
|
|
84
|
+
* serveur l'a acceptée. La réponse est un SUR-ENSEMBLE de ce que
|
|
85
|
+
* {@link IRequestRouter.resolve} acceptera : l'hôte (`@Domain`), le
|
|
86
|
+
* sous-protocole et les exigences de variables ne sont pas jugés ici, ils
|
|
87
|
+
* restent des fermetures après ouverture — refuser à tort coûterait une
|
|
88
|
+
* connexion légitime.
|
|
89
|
+
*
|
|
90
|
+
* @param pathname - chemin de l'URL d'upgrade (WHATWG), barres finales comprises
|
|
91
|
+
* @returns `false` si aucune route WebSocket ne peut répondre à ce chemin
|
|
92
|
+
*/
|
|
93
|
+
servesWebsocket(pathname: string): boolean;
|
|
78
94
|
/**
|
|
79
95
|
* Instance partagée d'un controller `scope: "singleton"`, créée une seule fois.
|
|
80
96
|
*
|
|
@@ -42,4 +42,10 @@ export interface IFirewallGate {
|
|
|
42
42
|
handleCors(context: ContextType): number | undefined;
|
|
43
43
|
/** Pose les en-têtes de sécurité (CSP, HSTS…) de la réponse. */
|
|
44
44
|
applySecurityHeaders(context: ContextType): void;
|
|
45
|
+
/**
|
|
46
|
+
* Le revalidateur de l'identité authentifiée de la requête courante, pour une
|
|
47
|
+
* connexion longue qui la gardera (flux SSE) : la même règle que les sockets
|
|
48
|
+
* WebSocket. `null` pour un anonyme — rien à révoquer.
|
|
49
|
+
*/
|
|
50
|
+
currentRevalidator?(): ((nowMs?: number) => Promise<boolean>) | null;
|
|
45
51
|
}
|
|
@@ -7,3 +7,4 @@ export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType,
|
|
|
7
7
|
export type { IHttpKernel } from "./IHttpKernel.js";
|
|
8
8
|
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./IRouting.js";
|
|
9
9
|
export type { ISecurityZone, IFirewallGate } from "./ISecurity.js";
|
|
10
|
+
export type { IProxyMount, IProxyMountOptions, ProxyTarget, } from "./IReverseProxy.js";
|
|
@@ -17,7 +17,8 @@ import type { IHttpContext, IWebsocketContext, PhaseTiming } from "../interfaces
|
|
|
17
17
|
export interface AuditLogEntry {
|
|
18
18
|
ts: string;
|
|
19
19
|
requestId: string;
|
|
20
|
-
|
|
20
|
+
/** Identifiant de l'appelant authentifié (`context.userIdentifier`), `null` sinon. */
|
|
21
|
+
userIdentifier: string | null;
|
|
21
22
|
type: "http" | "ws";
|
|
22
23
|
scheme: string;
|
|
23
24
|
method: string | null;
|
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { Service, Module } from "nodefony";
|
|
2
2
|
import { type KeyObject } from "node:crypto";
|
|
3
3
|
import { type ICertificateAttribute } from "./x509.js";
|
|
4
|
+
/**
|
|
5
|
+
* Accès à la liste d'ancres TLS par défaut du processus — injectable pour
|
|
6
|
+
* éprouver la décision sans toucher la confiance du processus de test.
|
|
7
|
+
*/
|
|
8
|
+
export interface ITlsTrustStore {
|
|
9
|
+
getCACertificates?: (type?: "default") => string[];
|
|
10
|
+
setDefaultCACertificates?: (certs: ReadonlyArray<string>) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Issue de {@link addDefaultCaCertificate}, CONSTATÉE à l'exécution. */
|
|
13
|
+
export type CaTrustVerdict = "added" | "present" | "unsupported";
|
|
14
|
+
/**
|
|
15
|
+
* Ajoute une autorité aux ancres TLS par défaut du processus (requêtes sortantes
|
|
16
|
+
* `fetch`, `https`, `tls.connect`), sans en retirer aucune.
|
|
17
|
+
*
|
|
18
|
+
* C'est l'équivalent en cours d'exécution de `NODE_EXTRA_CA_CERTS`, qui n'est lu
|
|
19
|
+
* qu'au lancement du processus — trop tôt pour une CA que le boot vient de
|
|
20
|
+
* générer, et que seul un script de banc posait. La capacité se CONSTATE
|
|
21
|
+
* (`tls.setDefaultCACertificates` n'existe qu'à partir de Node 24.5), elle ne se
|
|
22
|
+
* déduit pas d'un numéro de version. Idempotent : une autorité déjà présente
|
|
23
|
+
* n'est pas ajoutée deux fois.
|
|
24
|
+
*
|
|
25
|
+
* @param pem - autorité au format PEM.
|
|
26
|
+
* @param store - accès aux ancres ; `node:tls` par défaut.
|
|
27
|
+
* @returns `added`, `present` (déjà de confiance), ou `unsupported`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function addDefaultCaCertificate(pem: string, store?: ITlsTrustStore): CaTrustVerdict;
|
|
30
|
+
/**
|
|
31
|
+
* Découpe un fichier PEM en ses certificats — une chaîne d'entreprise est
|
|
32
|
+
* souvent livrée en un seul fichier (racine + intermédiaires).
|
|
33
|
+
*
|
|
34
|
+
* Chaque certificat est ajouté SÉPARÉMENT : c'est ce qui rend l'ajout
|
|
35
|
+
* idempotent certificat par certificat, la liste par défaut du processus
|
|
36
|
+
* n'étant faite que de certificats isolés.
|
|
37
|
+
*
|
|
38
|
+
* @param pem - contenu d'un fichier PEM (un ou plusieurs certificats)
|
|
39
|
+
* @returns les certificats, dans l'ordre du fichier ; vide s'il n'y en a aucun
|
|
40
|
+
*/
|
|
41
|
+
export declare function splitPemCertificates(pem: string): string[];
|
|
42
|
+
/** Bilan de {@link trustCaFile}. */
|
|
43
|
+
export interface ICaFileTrust {
|
|
44
|
+
/** Fichier lu, chemin absolu. */
|
|
45
|
+
readonly file: string;
|
|
46
|
+
/** Certificats ajoutés aux ancres du processus. */
|
|
47
|
+
readonly added: number;
|
|
48
|
+
/** Certificats déjà de confiance. */
|
|
49
|
+
readonly present: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Ajoute les autorités d'un fichier PEM aux ancres TLS du processus
|
|
53
|
+
* (`certificates.extraCaFiles`) — l'équivalent, porté par l'APPLICATION, de
|
|
54
|
+
* `NODE_EXTRA_CA_CERTS`.
|
|
55
|
+
*
|
|
56
|
+
* `NODE_EXTRA_CA_CERTS` n'est lu qu'au lancement de Node, et se pose dans la
|
|
57
|
+
* syntaxe du shell — `VAR=… npm run dev` n'existe pas sous Windows. Une autorité
|
|
58
|
+
* d'entreprise (Keycloak interne, proxy sortant qui ré-signe) se déclare donc
|
|
59
|
+
* dans la configuration, et sert à TOUTES les requêtes sortantes du processus :
|
|
60
|
+
* découverte OpenID, point de jeton, jeu de clés, appels d'administration.
|
|
61
|
+
*
|
|
62
|
+
* @param file - chemin du fichier, absolu ou relatif à `root`
|
|
63
|
+
* @param root - racine de l'application
|
|
64
|
+
* @param store - accès aux ancres ; `node:tls` par défaut
|
|
65
|
+
* @returns le bilan de l'ajout
|
|
66
|
+
* @throws BootConfigurationError - fichier illisible, sans certificat, ou Node
|
|
67
|
+
* incapable d'élargir ses ancres en cours d'exécution
|
|
68
|
+
*/
|
|
69
|
+
export declare function trustCaFile(file: string, root: string, store?: ITlsTrustStore): Promise<ICaFileTrust>;
|
|
70
|
+
/**
|
|
71
|
+
* Ajoute aux ancres TLS du processus les autorités déclarées
|
|
72
|
+
* (`certificates.extraCaFiles`), au plus tôt : appelé à l'enregistrement du
|
|
73
|
+
* module, avant tout service — toute requête sortante, commande console
|
|
74
|
+
* comprise, les trouve en place.
|
|
75
|
+
*
|
|
76
|
+
* La CA de DÉVELOPPEMENT n'y figure pas : le service de certificats la pose
|
|
77
|
+
* lui-même ({@link Certificate.generateServerCertificates}).
|
|
78
|
+
*
|
|
79
|
+
* @param files - `certificates.extraCaFiles`
|
|
80
|
+
* @param root - racine de l'application
|
|
81
|
+
* @param log - journal du module
|
|
82
|
+
* @throws BootConfigurationError - cf {@link trustCaFile}
|
|
83
|
+
*/
|
|
84
|
+
export declare function trustOutboundAuthorities(files: readonly string[], root: string, log: (message: string, severity: "INFO") => void): Promise<void>;
|
|
4
85
|
/** Paire de clés RSA du certificat auto-signé. */
|
|
5
86
|
export interface IRsaKeyPair {
|
|
6
87
|
privateKey: KeyObject;
|
|
@@ -102,7 +183,7 @@ export interface CertificateInfo {
|
|
|
102
183
|
*
|
|
103
184
|
* Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
|
|
104
185
|
* RFC 5280 / CA-B Forum), numéro de série aléatoire 128 bits unique
|
|
105
|
-
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC
|
|
186
|
+
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 9525), `notBefore` reculé,
|
|
106
187
|
* clé privée écrite en `0600`.
|
|
107
188
|
*/
|
|
108
189
|
declare class Certificate extends Service {
|
|
@@ -114,8 +195,6 @@ declare class Certificate extends Service {
|
|
|
114
195
|
cert: Buffer | string | null;
|
|
115
196
|
fullchainPem: Buffer | string | null;
|
|
116
197
|
publicKeyPem: Buffer | string | null;
|
|
117
|
-
rootCertPem: Buffer | string | null;
|
|
118
|
-
intermediateCertPem: Buffer | string | null;
|
|
119
198
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
120
199
|
private mkcertCaRoot;
|
|
121
200
|
path: string;
|
|
@@ -173,6 +252,18 @@ declare class Certificate extends Service {
|
|
|
173
252
|
* @param force - forcer la régénération même si un certificat valide existe
|
|
174
253
|
*/
|
|
175
254
|
generateServerCertificates(force?: boolean): Promise<this>;
|
|
255
|
+
/**
|
|
256
|
+
* En développement, fait confiance à la CA que Nodefony vient de générer ou de
|
|
257
|
+
* relire, pour les requêtes SORTANTES du processus.
|
|
258
|
+
*
|
|
259
|
+
* Sans elle, un service du décor qui sert le certificat de l'application
|
|
260
|
+
* (Keycloak, l'auto-vérification d'un jeton MCP) est injoignable depuis
|
|
261
|
+
* `nodefony development` — `fetch failed` / `SELF_SIGNED_CERT_IN_CHAIN` — alors
|
|
262
|
+
* que `curl -k` répond : la confiance n'existait que sous le script de banc.
|
|
263
|
+
* Réservé au DÉVELOPPEMENT et aux certificats que Nodefony fabrique : un
|
|
264
|
+
* certificat `explicit` (production) n'élargit jamais la confiance du processus.
|
|
265
|
+
*/
|
|
266
|
+
private trustDevelopmentAuthority;
|
|
176
267
|
/**
|
|
177
268
|
* Résout la stratégie effective à partir de `certificates.strategy` :
|
|
178
269
|
* - `explicit` : `key` + `cert` fournis (prod, Let's Encrypt…). Forcé →
|
|
@@ -193,7 +284,7 @@ declare class Certificate extends Service {
|
|
|
193
284
|
/**
|
|
194
285
|
* SAN effectif : config explicite si fournie, sinon dérivé du kernel
|
|
195
286
|
* (localhost + domain en DNS ; loopback en IP). Une IP littérale (ex. domain
|
|
196
|
-
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC
|
|
287
|
+
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 9525).
|
|
197
288
|
*/
|
|
198
289
|
private derivedSan;
|
|
199
290
|
/** Hostnames DNS couverts par le SAN. */
|
|
@@ -217,7 +308,7 @@ declare class Certificate extends Service {
|
|
|
217
308
|
* @returns false si absent, illisible ou inadéquat → déclenche la régénération.
|
|
218
309
|
*/
|
|
219
310
|
private isCertAdequate;
|
|
220
|
-
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC
|
|
311
|
+
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 9525) ? */
|
|
221
312
|
private sanCovers;
|
|
222
313
|
/** Charge un certificat fourni en config (chemin fichier ou Buffer). */
|
|
223
314
|
private loadExplicitCert;
|
|
@@ -69,7 +69,7 @@ export declare function schemaMismatchOf(error: unknown): SchemaMismatchConfiden
|
|
|
69
69
|
* reason: error.message
|
|
70
70
|
*
|
|
71
71
|
* Stateless singleton — zero per-request allocation. Override via
|
|
72
|
-
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC
|
|
72
|
+
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 9457.
|
|
73
73
|
*/
|
|
74
74
|
declare class DefaultErrorRenderer implements IErrorRenderer {
|
|
75
75
|
renderHttp(error: Error, context: IHttpContext): IErrorHttpResult;
|
|
@@ -7,13 +7,14 @@ import type { IRateLimitStore } from "../src/rateLimit/IRateLimitStore.js";
|
|
|
7
7
|
import { type ITrustedHostsConfig } from "../src/context/domainMatcher.js";
|
|
8
8
|
import http from "node:http";
|
|
9
9
|
import http2 from "node:http2";
|
|
10
|
-
import type { IncomingMessage } from "node:http";
|
|
10
|
+
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
|
|
11
11
|
import Ws from "ws";
|
|
12
12
|
import httpServer from "../service/servers/server-http.js";
|
|
13
13
|
import httpsServer from "../service/servers/server-https.js";
|
|
14
14
|
import websocketServer from "../service/servers/server-websocket.js";
|
|
15
15
|
import websocketSecureServer from "../service/servers/server-websocket-secure.js";
|
|
16
16
|
import Statics from "./servers/server-static.js";
|
|
17
|
+
import type ReverseProxy from "./reverse-proxy.js";
|
|
17
18
|
import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
|
|
18
19
|
import HttpContext from "../src/context/http/HttpContext.js";
|
|
19
20
|
import Context, { HTTPMethod, WebSocketState } from "../src/context/Context.js";
|
|
@@ -25,9 +26,21 @@ import type { IErrorRenderer } from "../interfaces/IErrorRenderer.js";
|
|
|
25
26
|
import type { IRequestLogger } from "../interfaces/IRequestLogger.js";
|
|
26
27
|
import type { DefaultOptionsService } from "nodefony";
|
|
27
28
|
import type { IHttpConfig } from "../config/config.js";
|
|
29
|
+
/**
|
|
30
|
+
* Config interne (shape déclarée par `config/config.ts`). Locale au module —
|
|
31
|
+
* pas exportée tant que la surface publique n'a pas vocation à être étendue.
|
|
32
|
+
*/
|
|
33
|
+
/** Ce qu'il faut d'une requête pour en compter la connexion par IP. */
|
|
34
|
+
export interface IConnectionRequest {
|
|
35
|
+
readonly headers: IncomingHttpHeaders;
|
|
36
|
+
readonly socket: {
|
|
37
|
+
readonly remoteAddress?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
28
40
|
export type ProtocolType = "1.1" | "2.0" | "3.0";
|
|
29
41
|
export type httpRequest = http.IncomingMessage | http2.Http2ServerRequest;
|
|
30
42
|
export type httpResponse = http.ServerResponse | http2.Http2ServerResponse;
|
|
43
|
+
export { responseEnded } from "../src/context/responseEnded.js";
|
|
31
44
|
export type ContextType = WebsocketContext | HttpContext | Context;
|
|
32
45
|
export type ServerType = "http" | "https" | "http2" | "http3" | "websocket" | "websocket-secure";
|
|
33
46
|
export type responseTimeoutType = "http" | "https" | "http2" | "http3";
|
|
@@ -81,13 +94,28 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
81
94
|
cert: string;
|
|
82
95
|
ca: string;
|
|
83
96
|
serverStatic: Statics | null;
|
|
97
|
+
/**
|
|
98
|
+
* Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
|
|
99
|
+
* `mounts === null` tant que personne n'a monté de préfixe.
|
|
100
|
+
*/
|
|
101
|
+
reverseProxy: ReverseProxy | null;
|
|
84
102
|
domain: string;
|
|
85
103
|
trustedHosts?: ITrustedHostsConfig | undefined;
|
|
86
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Contrôle de domaine actif (`domainCheck` de la config d'application) —
|
|
106
|
+
* lu à la source, pour le HTTP comme pour le handshake WebSocket.
|
|
107
|
+
*/
|
|
108
|
+
get domainCheck(): boolean;
|
|
87
109
|
regAlias: RegExp[];
|
|
88
110
|
module: Module;
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
|
|
113
|
+
* lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
|
|
114
|
+
*/
|
|
115
|
+
get httpsPort(): number | undefined;
|
|
116
|
+
/** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
|
|
117
|
+
get httpPort(): number | undefined;
|
|
118
|
+
private boundPort;
|
|
91
119
|
responseTimeout: {
|
|
92
120
|
http: number;
|
|
93
121
|
https: number;
|
|
@@ -196,6 +224,22 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
196
224
|
* @returns la politique mémoïsée (0 alloc après le 1er handshake).
|
|
197
225
|
*/
|
|
198
226
|
private getWsOriginPolicy;
|
|
227
|
+
/**
|
|
228
|
+
* L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
|
|
229
|
+
* interrogeable SANS contexte : le serveur WebSocket de Nodefony
|
|
230
|
+
* ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
|
|
231
|
+
* avant qu'aucun contexte n'existe, décident par elle.
|
|
232
|
+
*
|
|
233
|
+
* Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
|
|
234
|
+
* en development, allowlist `allowedOrigins` ; sans `Origin` (client
|
|
235
|
+
* non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
|
|
236
|
+
*
|
|
237
|
+
* @param origin - valeur brute de l'en-tête `Origin`
|
|
238
|
+
* @param hostname - nom d'hôte servi, sans port (`Context.domain`)
|
|
239
|
+
* @param secure - handshake reçu en TLS (politique `websocketSecure`)
|
|
240
|
+
* @returns `true` si le handshake peut se poursuivre
|
|
241
|
+
*/
|
|
242
|
+
isWebsocketOriginAllowed(origin: string | undefined, hostname: string, secure: boolean): boolean;
|
|
199
243
|
/**
|
|
200
244
|
* B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
|
|
201
245
|
* WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
|
|
@@ -304,7 +348,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
304
348
|
*/
|
|
305
349
|
/**
|
|
306
350
|
* Override the default error renderer — e.g. to hide stack traces in prod
|
|
307
|
-
* or emit RFC
|
|
351
|
+
* or emit RFC 9457 problem+json. Stateless singleton expected.
|
|
308
352
|
*/
|
|
309
353
|
setErrorRenderer(renderer: IErrorRenderer): void;
|
|
310
354
|
getErrorRenderer(): IErrorRenderer;
|
|
@@ -527,5 +571,67 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
527
571
|
* couvre ce nom.
|
|
528
572
|
*/
|
|
529
573
|
isTrustedHostname(hostname: string): boolean;
|
|
574
|
+
/**
|
|
575
|
+
* Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
|
|
576
|
+
* (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
|
|
577
|
+
* inverse et avant le serveur `ws`.
|
|
578
|
+
*
|
|
579
|
+
* Pourquoi avant : accepter puis fermer fait croire au client que le serveur
|
|
580
|
+
* a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
|
|
581
|
+
* une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
|
|
582
|
+
* recharger la page avant que Vite soit prêt — page blanche.
|
|
583
|
+
*
|
|
584
|
+
* Ne juge que l'existence d'une route WebSocket pour le chemin
|
|
585
|
+
* ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
|
|
586
|
+
* quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
|
|
587
|
+
* compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
|
|
588
|
+
* flot d'upgrades vers des chemins inexistants y échapperait.
|
|
589
|
+
*
|
|
590
|
+
* @param req - requête d'upgrade
|
|
591
|
+
* @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
|
|
592
|
+
* inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
|
|
593
|
+
* ou `null` si l'upgrade est admis
|
|
594
|
+
*/
|
|
595
|
+
websocketUpgradeRefusal(req: IncomingMessage): 400 | 404 | 429 | 503 | null;
|
|
596
|
+
/**
|
|
597
|
+
* Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
|
|
598
|
+
* compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
|
|
599
|
+
* simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
|
|
600
|
+
* le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
|
|
601
|
+
* qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
|
|
602
|
+
* servie.
|
|
603
|
+
*
|
|
604
|
+
* L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
|
|
605
|
+
* un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
|
|
606
|
+
* par refus : sous un flood, il serait lui-même un amplificateur.
|
|
607
|
+
*
|
|
608
|
+
* @param req - requête d'upgrade
|
|
609
|
+
* @param connection - ce dont la fermeture libère la place comptée (le `ws`
|
|
610
|
+
* accepté, ou le socket du client pour un tunnel relayé)
|
|
611
|
+
* @returns `null` si l'upgrade est admis — sa place est alors rendue à la
|
|
612
|
+
* fermeture de `connection` —, sinon la raison du refus
|
|
613
|
+
*/
|
|
614
|
+
websocketQuotaRefusal(req: IncomingMessage, connection: {
|
|
615
|
+
once(event: "close", listener: () => void): unknown;
|
|
616
|
+
}): "rate limit" | "too many connections" | null;
|
|
617
|
+
/**
|
|
618
|
+
* Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
|
|
619
|
+
* `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
|
|
620
|
+
* un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
|
|
621
|
+
* Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
|
|
622
|
+
* à l'entrée du pipeline.
|
|
623
|
+
*
|
|
624
|
+
* @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
|
|
625
|
+
* @param connection - ce dont la fermeture rend la place (la réponse)
|
|
626
|
+
* @returns `true` si la place est prise — ou si aucun plafond n'est armé
|
|
627
|
+
*/
|
|
628
|
+
acquireConnectionSlot(req: IConnectionRequest, connection: {
|
|
629
|
+
once(event: "close", listener: () => void): unknown;
|
|
630
|
+
}): boolean;
|
|
631
|
+
/**
|
|
632
|
+
* Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
|
|
633
|
+
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
634
|
+
*/
|
|
635
|
+
revalidateStreams(): Promise<void>;
|
|
530
636
|
}
|
|
531
637
|
export default HttpKernel;
|