@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.6
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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +4 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +112 -34
- package/dist/nodefony/service/pretty-request-logger.js +1 -1
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +55 -26
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +8 -4
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +71 -14
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +6 -0
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +15 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +45 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +5 -3
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +40 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { extend } from "nodefony";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { parseCookie } from "cookie";
|
|
3
|
+
import { parseCookie, stringifySetCookie } from "cookie";
|
|
4
4
|
import MS from "ms";
|
|
5
5
|
// nodefony/src/cookies/cookie.ts
|
|
6
6
|
const encode = encodeURIComponent;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Décode une valeur de cookie sans jamais lever : une séquence `%` invalide
|
|
9
|
+
* rend la valeur brute (règle du paquet `cookie`). Un `decodeURIComponent`
|
|
10
|
+
* nu levait dans le constructeur du contexte, et la requête restait sans
|
|
11
|
+
* réponse (`Cookie: a=50%25`).
|
|
12
|
+
*/
|
|
13
|
+
function decode(value) {
|
|
14
|
+
if (!value.includes("%")) return value;
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lecture de l'en-tête `Cookie` SANS décodage : la valeur est décodée une
|
|
23
|
+
* seule fois, par {@link Cookie.setValue}. Décoder aux deux endroits
|
|
24
|
+
* transformait `a%2541` en `aA` au lieu de `a%41`.
|
|
25
|
+
*/
|
|
26
|
+
const RAW_VALUES = { decode: (value) => value };
|
|
8
27
|
const cookieDefaultSettings = {
|
|
9
28
|
maxAge: 0,
|
|
10
29
|
path: "/",
|
|
@@ -15,9 +34,6 @@ const cookieDefaultSettings = {
|
|
|
15
34
|
signed: false,
|
|
16
35
|
secret: "!nodefony.secret!"
|
|
17
36
|
};
|
|
18
|
-
function parser(strToParse) {
|
|
19
|
-
return parseCookie(strToParse);
|
|
20
|
-
}
|
|
21
37
|
function cookiesParser(context) {
|
|
22
38
|
let cookies = null;
|
|
23
39
|
let co;
|
|
@@ -25,7 +41,7 @@ function cookiesParser(context) {
|
|
|
25
41
|
case "http":
|
|
26
42
|
if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
|
|
27
43
|
if (cookies) {
|
|
28
|
-
const obj =
|
|
44
|
+
const obj = parseCookie(cookies, RAW_VALUES);
|
|
29
45
|
for (const cookie in obj) {
|
|
30
46
|
co = new Cookie(cookie, obj[cookie]);
|
|
31
47
|
context.addRequestCookie(co);
|
|
@@ -36,7 +52,7 @@ function cookiesParser(context) {
|
|
|
36
52
|
case "websocket": {
|
|
37
53
|
const cookieHeader = context.request?.headers.cookie;
|
|
38
54
|
if (cookieHeader) {
|
|
39
|
-
const parsed =
|
|
55
|
+
const parsed = parseCookie(cookieHeader, RAW_VALUES);
|
|
40
56
|
for (const name in parsed) {
|
|
41
57
|
co = new Cookie(name, parsed[name]);
|
|
42
58
|
context.addRequestCookie(co);
|
|
@@ -165,14 +181,39 @@ var Cookie = class Cookie {
|
|
|
165
181
|
setPriority(val) {
|
|
166
182
|
return val;
|
|
167
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Durée de vie restante en SECONDES ENTIÈRES, `undefined` sous une seconde.
|
|
186
|
+
*
|
|
187
|
+
* RFC 6265 §4.1.1 : `max-age-av = "Max-Age=" non-zero-digit *DIGIT`. Un
|
|
188
|
+
* `Max-Age=3599.874` est ignoré par le navigateur, et le cookie devient un
|
|
189
|
+
* cookie de session. Arrondi INFÉRIEUR : le cookie ne survit jamais à son
|
|
190
|
+
* `Expires`.
|
|
191
|
+
*/
|
|
168
192
|
getMaxAge() {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
else this.maxAge = void 0;
|
|
173
|
-
} else this.maxAge = this.originalMaxAge;
|
|
193
|
+
const seconds = this.expires instanceof Date ? (this.expires.getTime() - Date.now()) / 1e3 : this.originalMaxAge;
|
|
194
|
+
const whole = seconds === void 0 ? 0 : Math.floor(seconds);
|
|
195
|
+
this.maxAge = whole >= 1 ? whole : void 0;
|
|
174
196
|
return this.maxAge;
|
|
175
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Vérifie la grammaire RFC 6265 §4.1.1 du nom, du `Path` et du `Domain`,
|
|
200
|
+
* au moment où le cookie est POSÉ sur la réponse.
|
|
201
|
+
*
|
|
202
|
+
* Sans elle, `path: "/; Domain=evil.example"` injectait un attribut, et un
|
|
203
|
+
* nom porteur de `;` partait tel quel. La grammaire est celle du paquet
|
|
204
|
+
* `cookie` : une seule implémentation.
|
|
205
|
+
*
|
|
206
|
+
* @throws {TypeError} nommant l'attribut hors grammaire.
|
|
207
|
+
*/
|
|
208
|
+
assertGrammar() {
|
|
209
|
+
const attributes = {
|
|
210
|
+
name: this.name,
|
|
211
|
+
value: ""
|
|
212
|
+
};
|
|
213
|
+
if (this.path !== void 0) attributes.path = this.path;
|
|
214
|
+
if (this.domain !== void 0) attributes.domain = this.domain;
|
|
215
|
+
stringifySetCookie(attributes);
|
|
216
|
+
}
|
|
176
217
|
toString() {
|
|
177
218
|
return `${this.name}=${encode(String(this.value))}`;
|
|
178
219
|
}
|
|
@@ -251,4 +292,4 @@ var Cookie = class Cookie {
|
|
|
251
292
|
return obj;
|
|
252
293
|
}
|
|
253
294
|
};
|
|
254
|
-
export { cookiesParser, Cookie as default
|
|
295
|
+
export { cookiesParser, Cookie as default };
|
|
@@ -31,6 +31,14 @@ var Profiler = class {
|
|
|
31
31
|
/** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
|
|
32
32
|
_seq = 0;
|
|
33
33
|
/**
|
|
34
|
+
* Démarrage du profileur, donc du processus (ms epoch, horloge du serveur) :
|
|
35
|
+
* aucune requête servie avant n'est connue. Rendu avec un profil absent, pour
|
|
36
|
+
* que le lecteur dise « le serveur a redémarré » plutôt que « évincé ».
|
|
37
|
+
*/
|
|
38
|
+
startedAt = Date.now();
|
|
39
|
+
/** Dernier vidage des anneaux (ms epoch), `null` s'il n'y en a jamais eu. */
|
|
40
|
+
_clearedAt = null;
|
|
41
|
+
/**
|
|
34
42
|
* @param cap - capacité de l'anneau de l'application.
|
|
35
43
|
* @param sideCap - capacité de chacun des anneaux de l'outillage et des
|
|
36
44
|
* assets relayés.
|
|
@@ -151,12 +159,17 @@ var Profiler = class {
|
|
|
151
159
|
const { app, tooling, asset } = this._lanes;
|
|
152
160
|
return app.size + tooling.size + asset.size;
|
|
153
161
|
}
|
|
154
|
-
/** Vide les anneaux. */
|
|
162
|
+
/** Vide les anneaux, et retient l'instant du vidage ({@link clearedAt}). */
|
|
155
163
|
clear() {
|
|
156
164
|
const { app, tooling, asset } = this._lanes;
|
|
157
165
|
app.clear();
|
|
158
166
|
tooling.clear();
|
|
159
167
|
asset.clear();
|
|
168
|
+
this._clearedAt = Date.now();
|
|
169
|
+
}
|
|
170
|
+
/** Dernier vidage des anneaux (ms epoch, horloge du serveur), `null` si aucun. */
|
|
171
|
+
get clearedAt() {
|
|
172
|
+
return this._clearedAt;
|
|
160
173
|
}
|
|
161
174
|
/**
|
|
162
175
|
* Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUnsupportedTransferCoding } from "./rules.js";
|
|
1
2
|
import http from "node:http";
|
|
2
3
|
import https from "node:https";
|
|
3
4
|
// nodefony/src/proxy/forward.ts
|
|
@@ -83,6 +84,71 @@ function viaLoops(via, pseudonym) {
|
|
|
83
84
|
for (const hop of hops) if (hop.trim().split(/\s+/)[1] === pseudonym) return true;
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
87
|
+
/** `1*DIGIT` (RFC 9110 §7.6.2) — compilé une fois. */
|
|
88
|
+
const DIGITS = /^[0-9]+$/;
|
|
89
|
+
/**
|
|
90
|
+
* `Max-Forwards` reçu, tel qu'un intermédiaire doit le lire (RFC 9110 §7.6.2) :
|
|
91
|
+
* sur `OPTIONS` et `TRACE` seulement — la même section permet de l'ignorer
|
|
92
|
+
* pour toute autre méthode, et il part alors tel quel.
|
|
93
|
+
*
|
|
94
|
+
* @param method - méthode de la requête
|
|
95
|
+
* @param value - en-tête `Max-Forwards` reçu
|
|
96
|
+
* @returns `null` s'il n'y a rien à faire (autre méthode, en-tête absent) ;
|
|
97
|
+
* `-1` si la valeur n'est pas `1*DIGIT` — elle ne se décrémente pas, l'en-tête
|
|
98
|
+
* n'est donc pas transmis ; sinon la valeur reçue (`0` : le proxy répond
|
|
99
|
+
* lui-même, sans joindre l'amont).
|
|
100
|
+
*/
|
|
101
|
+
function receivedMaxForwards(method, value) {
|
|
102
|
+
if (value === void 0 || method !== "OPTIONS" && method !== "TRACE") return null;
|
|
103
|
+
const raw = (Array.isArray(value) ? value[0] : value)?.trim() ?? "";
|
|
104
|
+
return DIGITS.test(raw) ? Number(raw) : -1;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* En-têtes qu'une réponse à `TRACE` ne renvoie pas : ils portent des secrets
|
|
108
|
+
* (RFC 9110 §9.3.8 : le destinataire final DEVRAIT exclure les champs
|
|
109
|
+
* susceptibles de contenir des données sensibles).
|
|
110
|
+
*/
|
|
111
|
+
const TRACE_SENSITIVE = /^(?:authorization|proxy-authorization|cookie)$|token|secret|session|api-?key|csrf/i;
|
|
112
|
+
/**
|
|
113
|
+
* Corps `message/http` d'une réponse à `TRACE` : la requête reçue, en-têtes
|
|
114
|
+
* sensibles et pseudo-en-têtes HTTP/2 retirés (RFC 9110 §9.3.8).
|
|
115
|
+
*/
|
|
116
|
+
function traceEcho(req) {
|
|
117
|
+
let echo = `TRACE ${req.url ?? "/"} HTTP/${req.httpVersion}\r\n`;
|
|
118
|
+
const authority = req.headers[":authority"];
|
|
119
|
+
if (req.headers.host === void 0 && typeof authority === "string") echo += `host: ${authority}\r\n`;
|
|
120
|
+
for (const name of Object.keys(req.headers)) {
|
|
121
|
+
if (name.charCodeAt(0) === 58 || TRACE_SENSITIVE.test(name)) continue;
|
|
122
|
+
echo += `${name}: ${flat(req.headers[name])}\r\n`;
|
|
123
|
+
}
|
|
124
|
+
return `${echo}\r\n`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Réponse du proxy en DESTINATAIRE FINAL, quand `Max-Forwards` est épuisé
|
|
128
|
+
* (RFC 9110 §7.6.2) : `TRACE` renvoie la requête reçue (§9.3.8), `OPTIONS`
|
|
129
|
+
* rend 200 avec `Allow` si le montage restreint ses méthodes, et
|
|
130
|
+
* `Content-Length: 0` (§9.3.7). L'amont n'est jamais joint.
|
|
131
|
+
*/
|
|
132
|
+
function answerAsFinalRecipient(req, out, mount) {
|
|
133
|
+
req.resume();
|
|
134
|
+
if (req.method === "TRACE") {
|
|
135
|
+
const body = traceEcho(req);
|
|
136
|
+
out.writeHead(200, {
|
|
137
|
+
"content-type": "message/http",
|
|
138
|
+
"content-length": Buffer.byteLength(body),
|
|
139
|
+
"cache-control": "no-store"
|
|
140
|
+
});
|
|
141
|
+
out.end(body);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const headers = {
|
|
145
|
+
"content-length": 0,
|
|
146
|
+
"cache-control": "no-store"
|
|
147
|
+
};
|
|
148
|
+
if (mount.methods !== void 0) headers.allow = mount.methods.join(", ");
|
|
149
|
+
out.writeHead(200, headers);
|
|
150
|
+
out.end();
|
|
151
|
+
}
|
|
86
152
|
/** Entrée `Via` de ce saut : `<version> <pseudonyme>` (RFC 9110 §7.6.3). */
|
|
87
153
|
function viaEntry(httpVersion, pseudonym) {
|
|
88
154
|
return `${httpVersion.startsWith("2") ? "2" : httpVersion || "1.1"} ${pseudonym}`;
|
|
@@ -98,10 +164,7 @@ function viaEntry(httpVersion, pseudonym) {
|
|
|
98
164
|
*/
|
|
99
165
|
function bodyFramingOf(req) {
|
|
100
166
|
const coding = req.headers["transfer-encoding"];
|
|
101
|
-
if (coding !== void 0)
|
|
102
|
-
if (coding === "chunked") return "chunked";
|
|
103
|
-
return coding.trim().toLowerCase() === "chunked" ? "chunked" : "unsupported";
|
|
104
|
-
}
|
|
167
|
+
if (coding !== void 0) return isUnsupportedTransferCoding(coding) ? "unsupported" : "chunked";
|
|
105
168
|
if (req.headers["content-length"] !== void 0) return "length";
|
|
106
169
|
const stream = req.stream;
|
|
107
170
|
if (stream !== void 0 && !stream.endAfterHeaders) return "chunked";
|
|
@@ -264,8 +327,9 @@ function armConnectTimeout(upstream, ms, onTimeout) {
|
|
|
264
327
|
* Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
|
|
265
328
|
*
|
|
266
329
|
* Le corps est transmis avec un cadrage cohérent ({@link bodyFramingOf}). Un
|
|
267
|
-
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont.
|
|
268
|
-
*
|
|
330
|
+
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Sur
|
|
331
|
+
* `OPTIONS` et `TRACE`, `Max-Forwards` est décrémenté ; à `0`, le proxy répond
|
|
332
|
+
* lui-même en destinataire final (RFC 9110 §7.6.2). Un client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
|
|
269
333
|
* connexion non établie à temps ou un amont muet rendent 504 — à condition
|
|
270
334
|
* que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
|
|
271
335
|
* statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
|
|
@@ -307,10 +371,21 @@ function forwardRequest(req, res, mount, target, from, relay) {
|
|
|
307
371
|
fail(501, NOT_IMPLEMENTED);
|
|
308
372
|
return;
|
|
309
373
|
}
|
|
374
|
+
const method = req.method ?? "GET";
|
|
375
|
+
const hops = receivedMaxForwards(method, req.headers["max-forwards"]);
|
|
376
|
+
if (hops === 0) {
|
|
377
|
+
answerAsFinalRecipient(req, out, mount);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
310
380
|
const options = requestOptions(target, mount, relay);
|
|
311
|
-
options.method =
|
|
381
|
+
options.method = method;
|
|
312
382
|
options.path = upstreamPath(req.url ?? "/", mount);
|
|
313
|
-
|
|
383
|
+
const headers = requestHeadersFor(req.headers, mount, target, from, relay.pseudonym, framing);
|
|
384
|
+
if (hops !== null) {
|
|
385
|
+
if (hops > 0) headers["max-forwards"] = String(Math.min(hops - 1, 10));
|
|
386
|
+
else delete headers["max-forwards"];
|
|
387
|
+
}
|
|
388
|
+
options.headers = headers;
|
|
314
389
|
if (mount.timeoutMs !== void 0) options.timeout = mount.timeoutMs;
|
|
315
390
|
const request = requestFn(target)(options);
|
|
316
391
|
upstream = request;
|
|
@@ -414,6 +489,31 @@ function refuseUpgrade(socket, status, reason, headers) {
|
|
|
414
489
|
}
|
|
415
490
|
socket.end(`HTTP/1.1 ${status} ${reason}\r\n${extra}Connection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
416
491
|
}
|
|
492
|
+
/** En-têtes du refus d'une version WebSocket inconnue — figés, partagés. */
|
|
493
|
+
const WS_VERSION_REFUSAL_HEADERS = Object.freeze({
|
|
494
|
+
Upgrade: "websocket",
|
|
495
|
+
"Sec-WebSocket-Version": "13"
|
|
496
|
+
});
|
|
497
|
+
/**
|
|
498
|
+
* Refuse un handshake WebSocket non conforme — la réponse qui convient à la
|
|
499
|
+
* raison rendue par `websocketHandshakeProblem`, seule implémentation pour le
|
|
500
|
+
* serveur WebSocket ET le proxy inverse.
|
|
501
|
+
*
|
|
502
|
+
* Une version inconnue reçoit `426` avec `Sec-WebSocket-Version: 13` : le
|
|
503
|
+
* client apprend vers quelle version se rabattre (RFC 6455 §4.2.2 point 6,
|
|
504
|
+
* §4.4), et le 426 porte l'`Upgrade` qu'exige RFC 9110 §15.5.22. Tout autre
|
|
505
|
+
* défaut de forme reçoit `400`.
|
|
506
|
+
*
|
|
507
|
+
* @param socket - socket du client, encore en attente de sa réponse
|
|
508
|
+
* @param problem - la raison rendue par `websocketHandshakeProblem`
|
|
509
|
+
*/
|
|
510
|
+
function refuseMalformedHandshake(socket, problem) {
|
|
511
|
+
if (problem === "Sec-WebSocket-Version ≠ 13") {
|
|
512
|
+
refuseUpgrade(socket, 426, "Upgrade Required", WS_VERSION_REFUSAL_HEADERS);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
refuseUpgrade(socket, 400, "Bad Request");
|
|
516
|
+
}
|
|
417
517
|
/** En-têtes d'une réponse d'amont REFUSANT l'upgrade, à ne pas recopier tels quels. */
|
|
418
518
|
const REFUSAL_SKIP = /* @__PURE__ */ new Set([
|
|
419
519
|
"transfer-encoding",
|
|
@@ -497,4 +597,4 @@ function forwardUpgrade(req, socket, head, mount, target, from, relay) {
|
|
|
497
597
|
});
|
|
498
598
|
upstream.end();
|
|
499
599
|
}
|
|
500
|
-
export { MAX_VIA_HOPS, bodyFramingOf, forwardRequest, forwardUpgrade, rawHead, refuseUpgrade, requestHeadersFor, responseHeadersFor, rewriteLocation, upstreamPath, viaLoops };
|
|
600
|
+
export { MAX_VIA_HOPS, bodyFramingOf, forwardRequest, forwardUpgrade, rawHead, receivedMaxForwards, refuseMalformedHandshake, refuseUpgrade, requestHeadersFor, responseHeadersFor, rewriteLocation, upstreamPath, viaLoops };
|
|
@@ -152,6 +152,12 @@ defaults
|
|
|
152
152
|
# \`timeout server\` ne s'applique plus au tunnel. Sans cette ligne, la valeur
|
|
153
153
|
# implicite coupe des sockets que le heartbeat gardait pourtant vivantes.
|
|
154
154
|
timeout tunnel ${idleSeconds}s
|
|
155
|
+
# Forwarded (RFC 7239) posé par haproxy lui-même (>= 2.8) : il met entre
|
|
156
|
+
# guillemets ce qui n'est pas un jeton — une IPv6 (\`for="[2001:db8::1]"\`),
|
|
157
|
+
# un \`host\` qui porte un port. Un \`set-header\` écrit à la main laissait
|
|
158
|
+
# \`for=2001:db8::1\`, valeur invalide (§6). \`proto\` suit \`ssl_fc\` : le
|
|
159
|
+
# scheme vu par le CLIENT. Vaut pour tous les backends.
|
|
160
|
+
option forwarded proto host for
|
|
155
161
|
|
|
156
162
|
frontend fe_nodefony
|
|
157
163
|
bind *:${intro.listen}
|
|
@@ -173,8 +179,6 @@ frontend fe_nodefony
|
|
|
173
179
|
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
|
|
174
180
|
http-request set-header X-Forwarded-Host %[req.hdr(host)]
|
|
175
181
|
http-request set-header X-Real-IP %[src]
|
|
176
|
-
http-request set-header Forwarded "for=%[src];proto=https;host=%[req.hdr(host)]" if { ssl_fc }
|
|
177
|
-
http-request set-header Forwarded "for=%[src];proto=http;host=%[req.hdr(host)]" unless { ssl_fc }
|
|
178
182
|
|
|
179
183
|
${h2cPort === null ? " default_backend be_nodefony\n" : " # h2c (servers.h2c) : HTTP/2 en clair vers le backend, plusieurs flux par\n # connexion. WebSocket reste en HTTP/1.1 sur le port HTTP : le port h2c\n # n'annonce pas le CONNECT étendu (RFC 8441).\n use_backend be_nodefony if { hdr(upgrade) -i websocket }\n default_backend be_nodefony_h2c\n"}
|
|
180
184
|
backend be_nodefony
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { isLoopbackHostname } from "nodefony";
|
|
1
|
+
import { isLoopbackHostname, parseBareOrigin } from "nodefony";
|
|
2
2
|
// nodefony/src/proxy/rules.ts
|
|
3
|
+
/** Schémas d’amont d’un relais : un proxy inverse ne parle que HTTP. */
|
|
4
|
+
const PROXY_ORIGIN_PROTOCOLS = ["http:", "https:"];
|
|
3
5
|
/**
|
|
4
6
|
* Règles d'un montage du proxy inverse — UNIQUE source, lue par le schéma de
|
|
5
7
|
* configuration (`proxy.mounts`) ET par `ReverseProxy.mount()` : un montage
|
|
@@ -49,17 +51,7 @@ function normalizePrefix(prefix) {
|
|
|
49
51
|
* @returns l'URL analysée, ou `null` si ce n'est pas une origine http(s) nue
|
|
50
52
|
*/
|
|
51
53
|
function parseProxyOrigin(origin) {
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
url = new URL(origin);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
59
|
-
if (!/^https?:\/\/[^/?#]+\/?$/i.test(origin.trim())) return null;
|
|
60
|
-
if (url.search !== "" || url.hash !== "") return null;
|
|
61
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
62
|
-
return url;
|
|
54
|
+
return parseBareOrigin(origin, PROXY_ORIGIN_PROTOCOLS);
|
|
63
55
|
}
|
|
64
56
|
/**
|
|
65
57
|
* Examine un montage. Rend TOUS les refus, pas le premier : une configuration
|
|
@@ -163,6 +155,29 @@ const UPGRADE_WEBSOCKET = /^\s*websocket\s*$/i;
|
|
|
163
155
|
/** Le jeton `upgrade` dans une liste `Connection` (RFC 9110 §7.6.1). */
|
|
164
156
|
const CONNECTION_UPGRADE = /(?:^|,)\s*upgrade\s*(?:,|$)/i;
|
|
165
157
|
/**
|
|
158
|
+
* Le corps reçu porte-t-il un codage de transfert que le serveur ne sait pas
|
|
159
|
+
* défaire ? Seul `chunked` est compris : Node le retire, mais laisse en place
|
|
160
|
+
* tout codage qui le précède (`gzip, chunked`) — le corps livré serait encore
|
|
161
|
+
* compressé. RFC 9112 §6.1 : un tel message reçoit 501. Un `chunked` qui n'est
|
|
162
|
+
* pas le DERNIER codage est déjà refusé par Node en 400 (§6.3).
|
|
163
|
+
*
|
|
164
|
+
* Seule règle, pour le serveur d'origine et pour le proxy inverse. Chemin de
|
|
165
|
+
* chaque requête : une lecture d'en-tête, rien d'autre sans `Transfer-Encoding`.
|
|
166
|
+
*
|
|
167
|
+
* @param coding - valeur de l'en-tête `Transfer-Encoding`, absente le plus souvent
|
|
168
|
+
* @returns `true` quand le corps ne peut pas être lu tel qu'annoncé
|
|
169
|
+
*/
|
|
170
|
+
function isUnsupportedTransferCoding(coding) {
|
|
171
|
+
if (coding === void 0 || coding === "chunked") return false;
|
|
172
|
+
return coding.trim().toLowerCase() !== "chunked";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Raison rendue par {@link websocketHandshakeProblem} quand le client parle une
|
|
176
|
+
* autre version du protocole : la seule qui appelle une réponse distincte —
|
|
177
|
+
* le client doit apprendre vers quelle version se rabattre (RFC 6455 §4.4).
|
|
178
|
+
*/
|
|
179
|
+
const WS_UNSUPPORTED_VERSION = "Sec-WebSocket-Version ≠ 13";
|
|
180
|
+
/**
|
|
166
181
|
* Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
|
|
167
182
|
* un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
|
|
168
183
|
* octet pour octet ouvrirait un tunnel que rien n'inspecte.
|
|
@@ -180,9 +195,9 @@ function websocketHandshakeProblem(method, headers) {
|
|
|
180
195
|
if (typeof upgrade !== "string" || !UPGRADE_WEBSOCKET.test(upgrade)) return "Upgrade autre que websocket";
|
|
181
196
|
const connection = headers.connection;
|
|
182
197
|
if (!(typeof connection === "string" ? CONNECTION_UPGRADE.test(connection) : Array.isArray(connection) && connection.some((token) => CONNECTION_UPGRADE.test(token)))) return "Connection sans upgrade";
|
|
183
|
-
if (headers["sec-websocket-version"] !== "13") return
|
|
198
|
+
if (headers["sec-websocket-version"] !== "13") return WS_UNSUPPORTED_VERSION;
|
|
184
199
|
const key = headers["sec-websocket-key"];
|
|
185
200
|
if (typeof key !== "string" || !WS_KEY.test(key)) return "Sec-WebSocket-Key invalide";
|
|
186
201
|
return null;
|
|
187
202
|
}
|
|
188
|
-
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
203
|
+
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, WS_UNSUPPORTED_VERSION, isAmbiguousPath, isUnsupportedTransferCoding, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
@@ -1,7 +1,60 @@
|
|
|
1
|
+
import { refuseMalformedHandshake, refuseUpgrade } from "../proxy/forward.js";
|
|
1
2
|
import { websocketHandshakeProblem } from "../proxy/rules.js";
|
|
2
|
-
import { refuseUpgrade } from "../proxy/forward.js";
|
|
3
3
|
import { STATUS_CODES } from "node:http";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
4
5
|
// nodefony/src/servers/upgradeDispatch.ts
|
|
6
|
+
/**
|
|
7
|
+
* Sous-protocole choisi par handshake, entre {@link IUpgradeAdmission.selectSubprotocol}
|
|
8
|
+
* et le rappel `handleProtocols` de `ws` — qui ne reçoit que la requête. Une
|
|
9
|
+
* table faible plutôt qu'une variable « en cours » : un `verifyClient`
|
|
10
|
+
* asynchrone sépare les deux instants, et deux handshakes s'y croiseraient.
|
|
11
|
+
* Une entrée n'existe que pour un handshake qui a choisi.
|
|
12
|
+
*/
|
|
13
|
+
const chosenSubprotocols = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Fabrique l'option `handleProtocols` du serveur `ws` de Nodefony : le choix
|
|
16
|
+
* retenu par l'admission ({@link IUpgradeAdmission.selectSubprotocol}) s'il y
|
|
17
|
+
* en a un, sinon `fallback` (celle de la configuration), sinon le premier
|
|
18
|
+
* offert — le défaut de `ws` 8, conservé pour tout contrôleur qui ne choisit
|
|
19
|
+
* pas.
|
|
20
|
+
*
|
|
21
|
+
* @param fallback - l'option `handleProtocols` de la configuration, s'il y en a une
|
|
22
|
+
* @returns le rappel à passer à `new WebSocketServer({ handleProtocols })`
|
|
23
|
+
*/
|
|
24
|
+
function createSubprotocolHandler(fallback) {
|
|
25
|
+
return (protocols, request) => {
|
|
26
|
+
const chosen = chosenSubprotocols.get(request);
|
|
27
|
+
if (chosen !== void 0) {
|
|
28
|
+
chosenSubprotocols.delete(request);
|
|
29
|
+
return chosen !== false && protocols.has(chosen) ? chosen : false;
|
|
30
|
+
}
|
|
31
|
+
if (fallback !== void 0) return fallback(protocols, request);
|
|
32
|
+
const first = protocols.values().next();
|
|
33
|
+
return first.done === true ? false : first.value;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Construit le serveur `ws` de Nodefony à partir des options de la
|
|
38
|
+
* configuration, en forçant ce que Nodefony gère — seul endroit où ces
|
|
39
|
+
* options sont posées, pour le serveur clair comme pour le sécurisé :
|
|
40
|
+
*
|
|
41
|
+
* - `noServer` : l'upgrade passe d'abord par le proxy inverse, puis par
|
|
42
|
+
* {@link attachUpgradeDispatch} ;
|
|
43
|
+
* - `clientTracking` : requis par `broadcast()` et le heartbeat ;
|
|
44
|
+
* - `handleProtocols` : le sous-protocole CHOISI par le contrôleur
|
|
45
|
+
* ({@link createSubprotocolHandler}), celui de la configuration à défaut.
|
|
46
|
+
*
|
|
47
|
+
* @param options - options `ws` issues de la configuration du serveur WebSocket
|
|
48
|
+
* @returns le serveur, sans écoute propre
|
|
49
|
+
*/
|
|
50
|
+
function createNodefonyWebSocketServer(options) {
|
|
51
|
+
return new WebSocketServer({
|
|
52
|
+
...options,
|
|
53
|
+
noServer: true,
|
|
54
|
+
clientTracking: true,
|
|
55
|
+
handleProtocols: createSubprotocolHandler(options.handleProtocols)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
5
58
|
/** Défaut sans noyau : tout upgrade conforme est accepté puis émis en `connection`. */
|
|
6
59
|
function acceptAll(wss) {
|
|
7
60
|
return (req, admission) => {
|
|
@@ -38,6 +91,9 @@ var UpgradeAdmission = class {
|
|
|
38
91
|
this.#onPendingError = () => socket.destroy();
|
|
39
92
|
socket.on("error", this.#onPendingError);
|
|
40
93
|
}
|
|
94
|
+
selectSubprotocol(protocol) {
|
|
95
|
+
if (!this.#settled) chosenSubprotocols.set(this.req, protocol);
|
|
96
|
+
}
|
|
41
97
|
#settle() {
|
|
42
98
|
if (this.#settled) return false;
|
|
43
99
|
this.#settled = true;
|
|
@@ -71,6 +127,7 @@ var UpgradeAdmission = class {
|
|
|
71
127
|
}
|
|
72
128
|
refuse(status, headers) {
|
|
73
129
|
if (!this.#settle()) return;
|
|
130
|
+
chosenSubprotocols.delete(this.req);
|
|
74
131
|
refuseUpgrade(this.socket, status, STATUS_CODES[status] ?? "Refused", headers);
|
|
75
132
|
}
|
|
76
133
|
};
|
|
@@ -114,8 +171,9 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
114
171
|
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
115
172
|
return;
|
|
116
173
|
}
|
|
117
|
-
|
|
118
|
-
|
|
174
|
+
const problem = websocketHandshakeProblem(req.method, req.headers);
|
|
175
|
+
if (problem !== null) {
|
|
176
|
+
refuseMalformedHandshake(socket, problem);
|
|
119
177
|
return;
|
|
120
178
|
}
|
|
121
179
|
admit(req, new UpgradeAdmission(wss, req, socket, head));
|
|
@@ -125,4 +183,4 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
125
183
|
server.removeListener("upgrade", onUpgrade);
|
|
126
184
|
};
|
|
127
185
|
}
|
|
128
|
-
export { attachUpgradeDispatch };
|
|
186
|
+
export { attachUpgradeDispatch, createNodefonyWebSocketServer, createSubprotocolHandler };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -68,9 +68,13 @@ declare class Http extends Module<IHttpConfig> {
|
|
|
68
68
|
}
|
|
69
69
|
export default Http;
|
|
70
70
|
export { defineHttpConfig, httpConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
71
|
+
export { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
72
|
+
export { isContentlessStatus } from "./nodefony/src/context/http/Response.js";
|
|
73
|
+
export type { ISplitTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
71
74
|
export { httpConfigSchema } from "./nodefony/config/config.js";
|
|
72
75
|
export type { IHttpConfig, IHttpConfigInput, } from "./nodefony/interfaces/IHttpConfig.js";
|
|
73
76
|
export { decideSend, readBackpressureOptions, type WsBackpressurePolicy, type WsSendDecision, type IWsBackpressureOptions, type IBackpressureSocket, type IBackpressureTarget, } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
77
|
+
export { admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter, FRAME_RATE_WINDOW_MS, type IFrameRateWindow, type IWsFrameRateOptions, } from "./nodefony/src/context/websocket/wsFrameRate.js";
|
|
74
78
|
export { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
|
|
75
79
|
export { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
76
80
|
export type { IRequestTrace, RequestTraceProtocol, } from "./nodefony/interfaces/IRequestTrace.js";
|
|
@@ -81,6 +85,8 @@ export type { IRateLimitStore, IRateLimitOptions, RateLimitVerdict, } from "./no
|
|
|
81
85
|
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, };
|
|
82
86
|
export { SseStream, openSseStream, acceptsEventStream, SSE_HEARTBEAT_MS, } from "./nodefony/src/context/http/SseStream.js";
|
|
83
87
|
export type { ISseStreamOptions, ISseSendOptions, } from "./nodefony/src/context/http/SseStream.js";
|
|
88
|
+
export { openByteStream } from "./nodefony/src/context/http/ByteStream.js";
|
|
89
|
+
export type { ByteStreamEndReason, ByteStreamSource, IByteStreamOptions, IByteStreamResult, } from "./nodefony/src/context/http/ByteStream.js";
|
|
84
90
|
export { schemaMismatchOf, type SchemaMismatchConfidence, } from "./nodefony/service/error-renderer.js";
|
|
85
91
|
export { toValidationFields, type IValidationField, } from "./nodefony/service/error-renderer.js";
|
|
86
92
|
export type { ProfileEntry, ProfileSummary, ProfilePhase, ProfileQuery, ProfileSecurity, IProfileBatch, IProfilerReader, } from "./nodefony/src/profiler/Profiler.js";
|
|
@@ -115,6 +115,8 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
115
115
|
close: "close";
|
|
116
116
|
drop: "drop";
|
|
117
117
|
}>>;
|
|
118
|
+
maxFramesPerSecond: z.ZodDefault<z.ZodNumber>;
|
|
119
|
+
frameRateCloseAfterRejects: z.ZodDefault<z.ZodNumber>;
|
|
118
120
|
}, z.core.$loose>>;
|
|
119
121
|
websocketSecure: z.ZodDefault<z.ZodObject<{
|
|
120
122
|
keepaliveInterval: z.ZodDefault<z.ZodNumber>;
|
|
@@ -139,6 +141,8 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
139
141
|
close: "close";
|
|
140
142
|
drop: "drop";
|
|
141
143
|
}>>;
|
|
144
|
+
maxFramesPerSecond: z.ZodDefault<z.ZodNumber>;
|
|
145
|
+
frameRateCloseAfterRejects: z.ZodDefault<z.ZodNumber>;
|
|
142
146
|
}, z.core.$loose>>;
|
|
143
147
|
statics: z.ZodDefault<z.ZodObject<{
|
|
144
148
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -40,12 +40,37 @@ export interface IFirewallGate {
|
|
|
40
40
|
enforceCsrf(context: ContextType): void;
|
|
41
41
|
/** Répond au preflight CORS ; `204` quand la réponse est déjà envoyée. */
|
|
42
42
|
handleCors(context: ContextType): number | undefined;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Pose les en-têtes de sécurité applicatifs (CSP, Permissions-Policy…) de la
|
|
45
|
+
* réponse, appelé par la réponse juste avant son `writeHead`.
|
|
46
|
+
*
|
|
47
|
+
* @param contentType - `Content-Type` de la réponse, qui choisit la politique ;
|
|
48
|
+
* `null` = réponse sans contenu ; omis = type inconnu (politique complète).
|
|
49
|
+
* @param servedAsFile - corps servi tel quel (fichier statique, relais) : aucun
|
|
50
|
+
* gabarit ne l'a écrit, il ne peut pas porter de nonce.
|
|
51
|
+
*/
|
|
52
|
+
applySecurityHeaders(context: ContextType, contentType?: string | null, servedAsFile?: boolean): void;
|
|
45
53
|
/**
|
|
46
54
|
* Le revalidateur de l'identité authentifiée de la requête courante, pour une
|
|
47
55
|
* connexion longue qui la gardera (flux SSE) : la même règle que les sockets
|
|
48
56
|
* WebSocket. `null` pour un anonyme — rien à révoquer.
|
|
49
57
|
*/
|
|
50
58
|
currentRevalidator?(): ((nowMs?: number) => Promise<boolean>) | null;
|
|
59
|
+
/**
|
|
60
|
+
* Prévenu quand le noyau HTTP a recalculé ses en-têtes de transport (édition à
|
|
61
|
+
* chaud de `securityHeaders`) : la politique de contenu suit `frameOptions`,
|
|
62
|
+
* qui pilote aussi `frame-ancestors`.
|
|
63
|
+
*/
|
|
64
|
+
onTransportHeadersChanged?(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Déclare les sources qu'un module ajoute à la politique de contenu
|
|
67
|
+
* (`{ "connect-src": ["wss://rt.example.com"] }`). Un second appel du même
|
|
68
|
+
* module REMPLACE sa contribution. Absent quand aucun pare-feu ne pose de CSP.
|
|
69
|
+
*
|
|
70
|
+
* @param moduleName - module qui contribue, clé de sa contribution
|
|
71
|
+
* @param fragment - sources par directive, origines exactes (sans joker)
|
|
72
|
+
*/
|
|
73
|
+
registerCspOrigins?(moduleName: string, fragment: Readonly<Record<string, readonly string[]>>): void;
|
|
74
|
+
/** Retire la contribution CSP de `moduleName`. */
|
|
75
|
+
unregisterCspOrigins?(moduleName: string): void;
|
|
51
76
|
}
|
|
@@ -13,7 +13,8 @@ import type { Profiler } from "../src/profiler/Profiler.js";
|
|
|
13
13
|
* `?connection=<requestId>` : les seules frames de cette socket WebSocket
|
|
14
14
|
* - `GET /nodefony/profiler/api/summary` → les profils de l'anneau agrégés par
|
|
15
15
|
* route (percentiles, phases, formes SQL, N+1, erreurs — `aggregateProfiles`)
|
|
16
|
-
* - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId
|
|
16
|
+
* - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId ;
|
|
17
|
+
* absent → 404 `{ startedAt, clearedAt }` : depuis quand l'anneau sait quelque chose
|
|
17
18
|
* - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
|
|
18
19
|
*
|
|
19
20
|
* La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
|
|
@@ -34,10 +34,11 @@ export declare function toValidationFields(error: Error): IValidationField[] | n
|
|
|
34
34
|
/**
|
|
35
35
|
* Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier)
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Trois sources le disent : MongoDB par une `CastError` de Mongoose (la clé est
|
|
38
|
+
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier), et
|
|
39
|
+
* le kit de colonnes portable par une `ColumnValueError` de genre `uuid` ou
|
|
40
|
+
* `integer` — il refuse sur les trois moteurs la clé que PostgreSQL refuserait. Une clé rangée en
|
|
41
|
+
* texte NATIF ne lève rien, la lecture rend « rien » : le même 404.
|
|
41
42
|
*
|
|
42
43
|
* Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
|
|
43
44
|
*
|
|
@@ -147,11 +147,20 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
147
147
|
* dans `onHttpRequest` (0 alloc/concat par requête).
|
|
148
148
|
*/
|
|
149
149
|
private computeSecurityHeaderCaches;
|
|
150
|
+
/**
|
|
151
|
+
* En-têtes du transport d'une réponse, figés au démarrage — ce que
|
|
152
|
+
* `HttpResponse` place en tête de sa réserve.
|
|
153
|
+
*
|
|
154
|
+
* @param type - famille du serveur qui a reçu la requête.
|
|
155
|
+
* @returns la liste plate `[nom, valeur, …]`, noms en minuscules.
|
|
156
|
+
*/
|
|
157
|
+
transportHeaders(type: ServerType): readonly string[];
|
|
150
158
|
/**
|
|
151
159
|
* Ce que ce kernel ÉMET réellement comme en-têtes de sécurité transport.
|
|
152
160
|
*
|
|
153
161
|
* Ces trois en-têtes (HSTS, X-Frame-Options, X-Content-Type-Options) sont posés
|
|
154
|
-
*
|
|
162
|
+
* par `@nodefony/http` sur TOUTE réponse (en tête de la réserve de `HttpResponse`),
|
|
163
|
+
* statiques et pages d'erreur comprises
|
|
155
164
|
* — pas par `@nodefony/security`, dont les clés homonymes sont inertes. Toute
|
|
156
165
|
* console d'administration doit donc lire CE point, sinon elle affiche une
|
|
157
166
|
* intention de configuration au lieu de l'état appliqué.
|
|
@@ -522,6 +531,10 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
522
531
|
/**
|
|
523
532
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
524
533
|
* n'a pas pu se construire, puis rend l'erreur au client (`onError`).
|
|
534
|
+
*
|
|
535
|
+
* Sans contexte, `onError` n'a rien sur quoi écrire : la réponse BRUTE
|
|
536
|
+
* reçoit alors un 500 nu. Sinon HTTP/1.1 coupait la connexion sans un octet
|
|
537
|
+
* et HTTP/2 laissait le flux pendu.
|
|
525
538
|
*/
|
|
526
539
|
private failHttp;
|
|
527
540
|
/**
|
|
@@ -715,7 +728,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
715
728
|
once(event: "close", listener: () => void): unknown;
|
|
716
729
|
}): boolean;
|
|
717
730
|
/**
|
|
718
|
-
* Re-valide tous les flux
|
|
731
|
+
* Re-valide tous les flux en cours et ferme ceux dont l'identité est morte.
|
|
719
732
|
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
720
733
|
*/
|
|
721
734
|
revalidateStreams(): Promise<void>;
|