@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.2
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 +12 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -2
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +26 -4
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +87 -144
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +22 -15
- package/dist/nodefony/service/trace.js +3 -2
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +108 -46
- package/dist/nodefony/src/context/http/Response.js +134 -54
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +65 -23
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +23 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +42 -57
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/index.md +4 -2
- package/docs/observabilite.md +27 -27
- package/docs/rate-limit.md +20 -20
- package/docs/servers.md +82 -79
- package/docs/session.md +74 -69
- package/docs/upload.md +49 -43
- package/package.json +9 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Cookie from "../../cookies/cookie.js";
|
|
2
|
-
import { typeOf } from "nodefony";
|
|
2
|
+
import { thenMaybe, typeOf } from "nodefony";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import mime from "mime-types";
|
|
5
5
|
//#region nodefony/src/context/http/Response.ts
|
|
@@ -14,12 +14,30 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
|
14
14
|
307,
|
|
15
15
|
308
|
|
16
16
|
]);
|
|
17
|
-
const NO_CONTENT_LENGTH_METHODS = /* @__PURE__ */ new Set([
|
|
18
|
-
"HEAD",
|
|
19
|
-
"OPTIONS",
|
|
20
|
-
"TRACE"
|
|
21
|
-
]);
|
|
22
17
|
const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
|
|
18
|
+
const CONTENT_TYPE_CACHE_MAX = 64;
|
|
19
|
+
const contentTypeCache = /* @__PURE__ */ new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Résout `type` (extension ou type MIME) comme `mime.contentType`, avec
|
|
22
|
+
* mémorisation bornée des résolutions positives.
|
|
23
|
+
*
|
|
24
|
+
* @param type - extension (`json`) ou type MIME (`text/html`).
|
|
25
|
+
* @returns la résolution, ou `null` si `mime-types` ne connaît pas le type.
|
|
26
|
+
*/
|
|
27
|
+
function resolveContentType(type) {
|
|
28
|
+
const hit = contentTypeCache.get(type);
|
|
29
|
+
if (hit !== void 0) return hit;
|
|
30
|
+
const full = mime.contentType(type);
|
|
31
|
+
if (!full) return null;
|
|
32
|
+
const semi = full.indexOf(";");
|
|
33
|
+
const resolved = {
|
|
34
|
+
full,
|
|
35
|
+
bare: semi === -1 ? full : full.slice(0, semi),
|
|
36
|
+
charset: mime.charset(full)
|
|
37
|
+
};
|
|
38
|
+
if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
23
41
|
var HttpResponse = class {
|
|
24
42
|
context;
|
|
25
43
|
response;
|
|
@@ -27,7 +45,9 @@ var HttpResponse = class {
|
|
|
27
45
|
statusMessage = "";
|
|
28
46
|
flushing = false;
|
|
29
47
|
encoding = "utf-8";
|
|
30
|
-
|
|
48
|
+
#text = null;
|
|
49
|
+
#textEncoding = "utf-8";
|
|
50
|
+
#buffer = null;
|
|
31
51
|
contentType = "application/octet-stream";
|
|
32
52
|
headers = {};
|
|
33
53
|
timeout;
|
|
@@ -35,7 +55,7 @@ var HttpResponse = class {
|
|
|
35
55
|
constructor(response, context) {
|
|
36
56
|
this.context = context;
|
|
37
57
|
this.response = response;
|
|
38
|
-
this.timeout = this.context
|
|
58
|
+
this.timeout = this.context.httpKernel?.responseTimeout[this.context.type];
|
|
39
59
|
}
|
|
40
60
|
/**
|
|
41
61
|
* Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
|
|
@@ -46,6 +66,33 @@ var HttpResponse = class {
|
|
|
46
66
|
ensureContentTypeHeader() {
|
|
47
67
|
if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
|
|
48
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la
|
|
71
|
+
* première lecture de cette propriété — l'envoi, lui, écrit le texte tel quel.
|
|
72
|
+
*/
|
|
73
|
+
get body() {
|
|
74
|
+
if (this.#text !== null) {
|
|
75
|
+
this.#buffer = Buffer.from(this.#text, this.#textEncoding);
|
|
76
|
+
this.#text = null;
|
|
77
|
+
}
|
|
78
|
+
return this.#buffer;
|
|
79
|
+
}
|
|
80
|
+
set body(value) {
|
|
81
|
+
this.#text = null;
|
|
82
|
+
this.#buffer = value;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
|
|
86
|
+
* les octets — vide légal si rien n'a été posé (`write(null)` jetterait
|
|
87
|
+
* ERR_STREAM_NULL_VALUES).
|
|
88
|
+
*/
|
|
89
|
+
get payload() {
|
|
90
|
+
return this.#text ?? this.#buffer ?? "";
|
|
91
|
+
}
|
|
92
|
+
/** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
|
|
93
|
+
get payloadEncoding() {
|
|
94
|
+
return this.#text !== null ? this.#textEncoding : this.encoding;
|
|
95
|
+
}
|
|
49
96
|
clean() {
|
|
50
97
|
this.response = null;
|
|
51
98
|
this.body = null;
|
|
@@ -68,7 +115,7 @@ var HttpResponse = class {
|
|
|
68
115
|
}
|
|
69
116
|
deleteCookie(cookie) {
|
|
70
117
|
if (cookie instanceof Cookie) {
|
|
71
|
-
if (this.cookies
|
|
118
|
+
if (Object.hasOwn(this.cookies, cookie.name)) {
|
|
72
119
|
delete this.cookies[cookie.name];
|
|
73
120
|
return true;
|
|
74
121
|
}
|
|
@@ -77,19 +124,20 @@ var HttpResponse = class {
|
|
|
77
124
|
throw new Error("Response delCookie not valid cookies");
|
|
78
125
|
}
|
|
79
126
|
deleteCookieByName(name) {
|
|
80
|
-
if (this.cookies
|
|
127
|
+
if (Object.hasOwn(this.cookies, name)) {
|
|
81
128
|
delete this.cookies[name];
|
|
82
129
|
return true;
|
|
83
130
|
}
|
|
84
131
|
return false;
|
|
85
132
|
}
|
|
86
133
|
setCookies() {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
if (
|
|
134
|
+
const cookies = Object.values(this.cookies);
|
|
135
|
+
const first = cookies.at(0);
|
|
136
|
+
if (first === void 0) return;
|
|
137
|
+
if (cookies.length === 1) return this.setCookie(first);
|
|
90
138
|
const serialized = [];
|
|
91
|
-
for (const
|
|
92
|
-
const s =
|
|
139
|
+
for (const cookie of cookies) {
|
|
140
|
+
const s = cookie.serialize();
|
|
93
141
|
this.log(`ADD COOKIE ==> ${s}`, "DEBUG");
|
|
94
142
|
serialized.push(s);
|
|
95
143
|
}
|
|
@@ -105,7 +153,8 @@ var HttpResponse = class {
|
|
|
105
153
|
if (this.flushing) {
|
|
106
154
|
const obj = {};
|
|
107
155
|
obj[name] = value;
|
|
108
|
-
|
|
156
|
+
this.addTrailers(obj);
|
|
157
|
+
return;
|
|
109
158
|
}
|
|
110
159
|
if (!this.response.headersSent) {
|
|
111
160
|
const lower = name.toLowerCase();
|
|
@@ -125,7 +174,7 @@ var HttpResponse = class {
|
|
|
125
174
|
*/
|
|
126
175
|
#mergeVary(value) {
|
|
127
176
|
const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
|
|
128
|
-
const current = this.response?.getHeader
|
|
177
|
+
const current = this.response?.getHeader("vary");
|
|
129
178
|
const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
|
|
130
179
|
if (all.some((t) => t === "*")) return "*";
|
|
131
180
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -151,9 +200,9 @@ var HttpResponse = class {
|
|
|
151
200
|
}
|
|
152
201
|
setContentType(type, encoding) {
|
|
153
202
|
if (type && encoding) {
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
mytype =
|
|
203
|
+
const resolved = resolveContentType(type);
|
|
204
|
+
if (resolved) {
|
|
205
|
+
const mytype = resolved.bare;
|
|
157
206
|
this.contentType = mytype;
|
|
158
207
|
this.encoding = encoding;
|
|
159
208
|
if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
|
|
@@ -161,10 +210,11 @@ var HttpResponse = class {
|
|
|
161
210
|
}
|
|
162
211
|
}
|
|
163
212
|
if (type && !encoding) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
213
|
+
const resolved = resolveContentType(type);
|
|
214
|
+
if (resolved) {
|
|
215
|
+
const mytype = resolved.full;
|
|
166
216
|
this.contentType = mytype;
|
|
167
|
-
|
|
217
|
+
const charset = resolved.charset;
|
|
168
218
|
if (charset) this.encoding = charset;
|
|
169
219
|
return this.setHeader("Content-Type", mytype);
|
|
170
220
|
}
|
|
@@ -230,30 +280,45 @@ var HttpResponse = class {
|
|
|
230
280
|
if (this.response) return this.statusMessage || this.response.statusMessage || http.STATUS_CODES[this.statusCode];
|
|
231
281
|
return this.statusMessage || http.STATUS_CODES[this.statusCode];
|
|
232
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Pose le corps de la réponse : texte gardé tel quel, octets copiés sur leur
|
|
285
|
+
* seule fenêtre, tout autre valeur sérialisée en JSON.
|
|
286
|
+
*
|
|
287
|
+
* @param ele - corps à envoyer.
|
|
288
|
+
* @param encoding - encodage d'un corps texte (défaut : `this.encoding`).
|
|
289
|
+
*/
|
|
233
290
|
setBody(ele, encoding) {
|
|
234
|
-
if (typeof ele === "string") this
|
|
291
|
+
if (typeof ele === "string") this.#setText(ele, encoding || this.encoding);
|
|
235
292
|
else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
|
|
236
293
|
else if (ArrayBuffer.isView(ele) && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
|
|
237
|
-
else
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
294
|
+
else {
|
|
295
|
+
let text;
|
|
296
|
+
try {
|
|
297
|
+
text = JSON.stringify(ele);
|
|
298
|
+
} catch {
|
|
299
|
+
text = String(ele);
|
|
300
|
+
}
|
|
301
|
+
this.#setText(text, "utf-8");
|
|
241
302
|
}
|
|
242
|
-
|
|
303
|
+
}
|
|
304
|
+
#setText(text, encoding) {
|
|
305
|
+
this.#buffer = null;
|
|
306
|
+
this.#text = text;
|
|
307
|
+
this.#textEncoding = encoding;
|
|
243
308
|
}
|
|
244
309
|
setLength(body) {
|
|
245
310
|
if (this.response?.headersSent) throw new Error("Headers already sended");
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!NO_CONTENT_LENGTH_METHODS.has(this.context?.method) && !NO_CONTENT_LENGTH_STATUS.has(this.statusCode) && !isChunked) {
|
|
249
|
-
if (actualBody) {
|
|
250
|
-
const length = Buffer.byteLength(actualBody);
|
|
251
|
-
this.setHeader("Content-Length", String(length));
|
|
252
|
-
return length;
|
|
253
|
-
}
|
|
311
|
+
if (this.getHeader("Transfer-Encoding") === "chunked") {
|
|
312
|
+
this.response?.removeHeader("Content-Length");
|
|
254
313
|
return 0;
|
|
255
314
|
}
|
|
256
|
-
if (
|
|
315
|
+
if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
|
|
316
|
+
const actualBody = body || this.#text || this.#buffer;
|
|
317
|
+
if (actualBody) {
|
|
318
|
+
const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
|
|
319
|
+
this.setHeader("Content-Length", String(length));
|
|
320
|
+
return length;
|
|
321
|
+
}
|
|
257
322
|
return 0;
|
|
258
323
|
}
|
|
259
324
|
writeHead(statusCode, headers) {
|
|
@@ -264,8 +329,8 @@ var HttpResponse = class {
|
|
|
264
329
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
265
330
|
}
|
|
266
331
|
this.statusMessage = this.getStatusMessage();
|
|
267
|
-
if (this.context.requestId
|
|
268
|
-
if (this.context.traceparent
|
|
332
|
+
if (this.context.requestId) this.response.setHeader("x-request-id", this.context.requestId);
|
|
333
|
+
if (this.context.traceparent) this.response.setHeader("traceparent", this.context.traceparent);
|
|
269
334
|
this.setLength();
|
|
270
335
|
this.ensureContentTypeHeader();
|
|
271
336
|
const std = http.STATUS_CODES[this.statusCode];
|
|
@@ -287,18 +352,29 @@ var HttpResponse = class {
|
|
|
287
352
|
this.setHeader("Transfer-Encoding", "chunked");
|
|
288
353
|
return this.send(chunk, encoding, true);
|
|
289
354
|
}
|
|
290
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Écrit le corps sur le flux et, pour une réponse unique, la termine.
|
|
357
|
+
*
|
|
358
|
+
* Synchrone pour une réponse unique ou une redirection : `end(corps)` est un
|
|
359
|
+
* appel synchrone de Node, rien n'y est à attendre (#505). Une promesse
|
|
360
|
+
* seulement en streaming chunké (`flush()`), réglée quand le flux accepte
|
|
361
|
+
* l'écriture suivante (`drain`) — c'est la contre-pression.
|
|
362
|
+
*
|
|
363
|
+
* @param chunk - le corps (posé par `setBody`) ; absent, le corps déjà posé.
|
|
364
|
+
* @param encoding - l'encodage d'un corps texte.
|
|
365
|
+
* @returns la réponse, ou sa promesse en streaming chunké.
|
|
366
|
+
* @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
|
|
367
|
+
*/
|
|
368
|
+
send(chunk, encoding, _flush = false) {
|
|
291
369
|
if (this.context.isRedirect) {
|
|
292
370
|
if (!this.response?.headersSent) this.writeHead();
|
|
293
|
-
|
|
294
|
-
return this;
|
|
371
|
+
return thenMaybe(this.end(), () => this);
|
|
295
372
|
}
|
|
296
|
-
if (chunk) this.setBody(chunk);
|
|
373
|
+
if (chunk) this.setBody(chunk, encoding);
|
|
297
374
|
if (!this.response) throw new Error(`Http Response not found`);
|
|
298
|
-
if (this.body === null) this.body = Buffer.alloc(0);
|
|
299
375
|
const res = this.response;
|
|
300
376
|
if (!_flush && !this.flushing && !res.writableEnded) {
|
|
301
|
-
res.end(this.
|
|
377
|
+
res.end(this.payload, this.payloadEncoding);
|
|
302
378
|
return this;
|
|
303
379
|
}
|
|
304
380
|
return new Promise((resolve) => {
|
|
@@ -310,7 +386,7 @@ var HttpResponse = class {
|
|
|
310
386
|
}
|
|
311
387
|
};
|
|
312
388
|
const onDrain = () => done();
|
|
313
|
-
if (res.write(this.
|
|
389
|
+
if (res.write(this.payload, this.payloadEncoding, (error) => {
|
|
314
390
|
if (error) {
|
|
315
391
|
this.log(error, "ERROR");
|
|
316
392
|
res.removeListener("drain", onDrain);
|
|
@@ -320,17 +396,21 @@ var HttpResponse = class {
|
|
|
320
396
|
else res.once("drain", onDrain);
|
|
321
397
|
});
|
|
322
398
|
}
|
|
323
|
-
|
|
324
|
-
return
|
|
399
|
+
write(chunk, encoding) {
|
|
400
|
+
return this.send(chunk, encoding || this.encoding);
|
|
325
401
|
}
|
|
326
402
|
writeContinue() {
|
|
327
403
|
return this.response?.writeContinue();
|
|
328
404
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Termine la réponse Node — un appel synchrone, rien n'y est à attendre (#505).
|
|
407
|
+
*
|
|
408
|
+
* @returns le flux terminé (valeur de `ServerResponse.end`).
|
|
409
|
+
* @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
|
|
410
|
+
*/
|
|
411
|
+
end(chunk, encoding) {
|
|
412
|
+
if (!this.response) throw new Error(`response not found`);
|
|
413
|
+
return this.response.end(chunk, encoding || this.encoding);
|
|
334
414
|
}
|
|
335
415
|
getHeader(name) {
|
|
336
416
|
return this.response?.getHeader(name);
|
|
@@ -19,7 +19,7 @@ var Parser = class {
|
|
|
19
19
|
}
|
|
20
20
|
write(buffer) {
|
|
21
21
|
if (this.aborted) return buffer;
|
|
22
|
-
const max = this.request.maxBodySize
|
|
22
|
+
const max = this.request.maxBodySize;
|
|
23
23
|
this.received += buffer.length;
|
|
24
24
|
if (max > 0 && this.received > max) {
|
|
25
25
|
this.aborted = true;
|
|
@@ -78,7 +78,7 @@ var ParserQs = class extends Parser {
|
|
|
78
78
|
charset = "utf8";
|
|
79
79
|
constructor(request) {
|
|
80
80
|
super(request);
|
|
81
|
-
this.parserOptions = this.request.queryStringOptions
|
|
81
|
+
this.parserOptions = this.request.queryStringOptions ?? {};
|
|
82
82
|
this.charset = this.request.charset;
|
|
83
83
|
}
|
|
84
84
|
async parse() {
|
|
@@ -151,13 +151,15 @@ const ACCEPT_ANY = [{
|
|
|
151
151
|
* corrige aussi la négociation (match du token ENTIER, pas d'une sous-chaîne).
|
|
152
152
|
*/
|
|
153
153
|
const acceptMatcher = (token) => !token || token === "*" ? /.*/ : new RegExp(`^${escapeRegExp(token)}$`);
|
|
154
|
+
/** Qualité `q` d'une entrée `Accept` : sa valeur si c'est un nombre fini, 1 sinon. */
|
|
155
|
+
const qualityOf = (entry) => typeof entry.q === "number" && Number.isFinite(entry.q) ? entry.q : 1;
|
|
154
156
|
const acceptParser = function(acc) {
|
|
155
157
|
if (!acc) return ACCEPT_ANY;
|
|
156
158
|
const arr = [];
|
|
157
159
|
try {
|
|
158
160
|
const types = acc.split(",");
|
|
159
|
-
for (
|
|
160
|
-
const type =
|
|
161
|
+
for (const segment of types) {
|
|
162
|
+
const type = segment.split(";");
|
|
161
163
|
const mine = type.shift();
|
|
162
164
|
if (!mine) continue;
|
|
163
165
|
const dec = mine.trim().split("/");
|
|
@@ -167,8 +169,8 @@ const acceptParser = function(acc) {
|
|
|
167
169
|
type: acceptMatcher(ele1),
|
|
168
170
|
subtype: acceptMatcher(ele2)
|
|
169
171
|
};
|
|
170
|
-
for (
|
|
171
|
-
const params =
|
|
172
|
+
for (const param of type) {
|
|
173
|
+
const params = param.split("=");
|
|
172
174
|
const name = params.shift();
|
|
173
175
|
const value = params.shift();
|
|
174
176
|
obj[name] = name === "q" ? parseFloat(value) : value;
|
|
@@ -176,10 +178,7 @@ const acceptParser = function(acc) {
|
|
|
176
178
|
arr.push(obj);
|
|
177
179
|
}
|
|
178
180
|
if (arr.length === 0) return ACCEPT_ANY;
|
|
179
|
-
return arr.sort((a, b) =>
|
|
180
|
-
const qA = a.q || 1;
|
|
181
|
-
return (b.q || 1) - qA;
|
|
182
|
-
});
|
|
181
|
+
return arr.sort((a, b) => qualityOf(b) - qualityOf(a));
|
|
183
182
|
} catch {
|
|
184
183
|
return ACCEPT_ANY;
|
|
185
184
|
}
|
|
@@ -79,21 +79,45 @@ function isCanonicalAuthority(host, scheme) {
|
|
|
79
79
|
if (port.length === 0 || port.charCodeAt(0) === 48) return false;
|
|
80
80
|
if (scheme === "https" && port === "443" || scheme === "http" && port === "80") return false;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
let labels = 1;
|
|
83
|
+
let start = 0;
|
|
84
|
+
for (let i = 0; i < name.length; i++) if (name.charCodeAt(i) === DOT) {
|
|
85
|
+
if (i === start) return false;
|
|
86
|
+
labels++;
|
|
87
|
+
start = i + 1;
|
|
88
|
+
}
|
|
89
|
+
if (start === name.length) return false;
|
|
90
|
+
if (isDigitsFrom(name, start)) {
|
|
91
|
+
if (labels !== 4) return false;
|
|
92
|
+
return isCanonicalDottedQuad(name);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Les quatre labels (non vides, déjà comptés) forment-ils une dotted-quad
|
|
98
|
+
* canonique ? Chacun : chiffres seuls, 3 au plus, sans zéro de tête, ≤ 255.
|
|
99
|
+
*/
|
|
100
|
+
function isCanonicalDottedQuad(name) {
|
|
101
|
+
let start = 0;
|
|
102
|
+
for (let i = 0; i <= name.length; i++) {
|
|
103
|
+
if (i < name.length && name.charCodeAt(i) !== DOT) continue;
|
|
104
|
+
const len = i - start;
|
|
105
|
+
if (len > 3) return false;
|
|
106
|
+
if (len > 1 && name.charCodeAt(start) === 48) return false;
|
|
107
|
+
let value = 0;
|
|
108
|
+
for (let k = start; k < i; k++) {
|
|
109
|
+
const c = name.charCodeAt(k);
|
|
110
|
+
if (c < 48 || c > 57) return false;
|
|
111
|
+
value = value * 10 + (c - 48);
|
|
91
112
|
}
|
|
113
|
+
if (value > 255) return false;
|
|
114
|
+
start = i + 1;
|
|
92
115
|
}
|
|
93
116
|
return true;
|
|
94
117
|
}
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
/** `s` ne contient-elle que des chiffres ASCII à partir de `from` ? */
|
|
119
|
+
function isDigitsFrom(s, from) {
|
|
120
|
+
for (let i = from; i < s.length; i++) {
|
|
97
121
|
const c = s.charCodeAt(i);
|
|
98
122
|
if (c < 48 || c > 57) return false;
|
|
99
123
|
}
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import HttpResponse from "../http/Response.js";
|
|
2
|
-
import {
|
|
2
|
+
import { thenMaybe } from "nodefony";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import http2 from "node:http2";
|
|
5
5
|
//#region nodefony/src/context/http2/Response.ts
|
|
6
6
|
const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
|
|
7
|
+
/**
|
|
8
|
+
* Compose les en-têtes d'une réponse HTTP/2 : ceux posés sur la réponse, puis
|
|
9
|
+
* ceux passés à `writeHead`, qui gagnent.
|
|
10
|
+
*
|
|
11
|
+
* Les clés sont ramenées en MINUSCULES : `getHeaders()` rend `content-type`,
|
|
12
|
+
* un contrôleur passe `Content-Type` — fusionnées telles quelles, les deux
|
|
13
|
+
* clés partaient ensemble et `stream.respond()` refusait la réponse
|
|
14
|
+
* (`ERR_HTTP2_HEADER_SINGLE_VALUE`), soit une 500 sur tout média servi en
|
|
15
|
+
* HTTP/2. HTTP/2 n'a de toute façon que des noms en minuscules (RFC 9113 §8.2.1).
|
|
16
|
+
*
|
|
17
|
+
* @param statusMessage - message de statut, recopié en `x-status-message`
|
|
18
|
+
* @param current - en-têtes déjà posés sur la réponse
|
|
19
|
+
* @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
|
|
20
|
+
* @returns les en-têtes à transmettre à `respond()`
|
|
21
|
+
*/
|
|
22
|
+
function mergeResponseHeaders(statusMessage, current, extra) {
|
|
23
|
+
const merged = { "x-status-message": statusMessage };
|
|
24
|
+
for (const key in current) merged[key.toLowerCase()] = current[key];
|
|
25
|
+
if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
|
|
26
|
+
else if (extra) for (const key in extra) {
|
|
27
|
+
const value = extra[key];
|
|
28
|
+
if (value !== void 0) merged[key.toLowerCase()] = value;
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
7
32
|
var Http2Response = class extends HttpResponse {
|
|
8
33
|
statusCode = 200;
|
|
9
34
|
stream = null;
|
|
10
35
|
streamId;
|
|
11
36
|
constructor(response, context) {
|
|
12
37
|
super(response, context);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
if (this.stream && this.stream.pushAllowed) this.context.pushAllowed = true;
|
|
38
|
+
this.stream = response.stream;
|
|
39
|
+
this.streamId = this.stream.id;
|
|
40
|
+
if (this.stream.pushAllowed) this.context.pushAllowed = true;
|
|
18
41
|
}
|
|
19
42
|
isHeaderSent() {
|
|
20
43
|
if (this.stream) return this.stream.headersSent;
|
|
@@ -35,7 +58,7 @@ var Http2Response = class extends HttpResponse {
|
|
|
35
58
|
this.statusMessage = this.getStatusMessage();
|
|
36
59
|
this.setLength();
|
|
37
60
|
this.ensureContentTypeHeader();
|
|
38
|
-
this.headers =
|
|
61
|
+
this.headers = mergeResponseHeaders(this.statusMessage, this.getHeaders(), headers);
|
|
39
62
|
this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
|
|
40
63
|
const requestId = this.context.requestId;
|
|
41
64
|
if (requestId) this.headers["x-request-id"] = requestId;
|
|
@@ -45,39 +68,44 @@ var Http2Response = class extends HttpResponse {
|
|
|
45
68
|
} else this.log("Headers already sent !!", "WARNING");
|
|
46
69
|
} else return super.writeHead(statusCode, headers);
|
|
47
70
|
}
|
|
48
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Écrit le corps sur le flux HTTP/2. Synchrone pour une redirection, un flux
|
|
73
|
+
* fermé ou le repli HTTP/1 ; une promesse quand le corps est écrit sur le
|
|
74
|
+
* flux, réglée par le rappel d'écriture (#505).
|
|
75
|
+
*
|
|
76
|
+
* @returns la réponse, ou sa promesse.
|
|
77
|
+
*/
|
|
78
|
+
send(chunk, encoding, _flush = false) {
|
|
49
79
|
if (this.context.isRedirect) {
|
|
50
80
|
if (this.stream && !this.stream.headersSent) this.writeHead();
|
|
51
|
-
|
|
52
|
-
return this;
|
|
81
|
+
return thenMaybe(this.end(), () => this);
|
|
53
82
|
}
|
|
54
83
|
const stream = this.stream;
|
|
55
|
-
if (!stream) return
|
|
84
|
+
if (!stream) return super.send(chunk, encoding);
|
|
56
85
|
if (stream.destroyed || !stream.writable) {
|
|
57
86
|
this.log("HTTP/2 stream not writable before send — skip", "DEBUG");
|
|
58
87
|
return this;
|
|
59
88
|
}
|
|
60
89
|
if (chunk) this.setBody(chunk);
|
|
61
|
-
if (this.body === null) this.body = Buffer.alloc(0);
|
|
62
90
|
return new Promise((resolve, reject) => {
|
|
63
|
-
stream.write(this.
|
|
91
|
+
stream.write(this.payload, this.payloadEncoding, (error) => {
|
|
64
92
|
if (error) return reject(error);
|
|
65
93
|
return resolve(this);
|
|
66
94
|
});
|
|
67
95
|
});
|
|
68
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Termine le flux HTTP/2 (ou la réponse de repli) — synchrone (#505).
|
|
99
|
+
*
|
|
100
|
+
* @returns le flux terminé.
|
|
101
|
+
* @throws de façon SYNCHRONE ce que lève la fin du flux.
|
|
102
|
+
*/
|
|
69
103
|
end(chunk, encoding) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return resolve(super.end(chunk, encoding));
|
|
77
|
-
} catch (e) {
|
|
78
|
-
return reject(e);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
104
|
+
if (this.stream) {
|
|
105
|
+
if (this.stream.destroyed || this.stream.closed) return this.stream;
|
|
106
|
+
return this.stream.end(chunk, encoding || this.encoding);
|
|
107
|
+
}
|
|
108
|
+
return super.end(chunk, encoding);
|
|
81
109
|
}
|
|
82
110
|
getStatusMessage(code) {
|
|
83
111
|
if (code) {
|
|
@@ -94,4 +122,4 @@ var Http2Response = class extends HttpResponse {
|
|
|
94
122
|
}
|
|
95
123
|
};
|
|
96
124
|
//#endregion
|
|
97
|
-
export { Http2Response as default };
|
|
125
|
+
export { Http2Response as default, mergeResponseHeaders };
|
|
@@ -31,7 +31,7 @@ function buildMetaData(target, src, override) {
|
|
|
31
31
|
nf.scheme = src.scheme;
|
|
32
32
|
nf.requestId = src.requestId;
|
|
33
33
|
const r = src.resolver;
|
|
34
|
-
nf.route = r
|
|
34
|
+
nf.route = r?.route ? {
|
|
35
35
|
name: r.route.name,
|
|
36
36
|
path: r.route.path,
|
|
37
37
|
variablesMap: r.getMatchedParams()
|
|
@@ -54,7 +54,8 @@ const TRUST_NONE = { isTrusted: () => false };
|
|
|
54
54
|
function addEntry(list, raw) {
|
|
55
55
|
const entry = raw.trim();
|
|
56
56
|
if (!entry) return;
|
|
57
|
-
const
|
|
57
|
+
const key = entry.toLowerCase();
|
|
58
|
+
const preset = Object.hasOwn(PRESETS, key) ? PRESETS[key] : void 0;
|
|
58
59
|
if (preset) {
|
|
59
60
|
for (const [net, prefix, family] of preset) list.addSubnet(net, prefix, family);
|
|
60
61
|
return;
|
|
@@ -124,7 +125,7 @@ function resolveFromRight(chain, socketAddress, checker) {
|
|
|
124
125
|
let candidate = socketAddress;
|
|
125
126
|
for (let i = chain.length - 1; i >= 0; i -= 1) {
|
|
126
127
|
if (!checker.isTrusted(candidate)) return candidate;
|
|
127
|
-
const next = chain[i];
|
|
128
|
+
const next = chain[i] ?? null;
|
|
128
129
|
if (next === null) return candidate;
|
|
129
130
|
candidate = next;
|
|
130
131
|
}
|
|
@@ -156,7 +157,7 @@ function extractClientIp(xff, socketAddress, checker) {
|
|
|
156
157
|
if (!socketAddress) return null;
|
|
157
158
|
if (!xff) return socketAddress;
|
|
158
159
|
const s = Array.isArray(xff) ? xff.join(",") : xff;
|
|
159
|
-
if (s.
|
|
160
|
+
if (!s.includes(",")) {
|
|
160
161
|
const ip = s.trim();
|
|
161
162
|
if (!ip) return socketAddress;
|
|
162
163
|
return checker.isTrusted(socketAddress) ? ip : socketAddress;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Cookie from "../../cookies/cookie.js";
|
|
2
2
|
import { decideSend, readBackpressureOptions } from "./wsBackpressure.js";
|
|
3
|
-
import
|
|
3
|
+
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
4
4
|
import http from "node:http";
|
|
5
|
+
import Ws from "ws";
|
|
5
6
|
//#region nodefony/src/context/websocket/Response.ts
|
|
6
7
|
/**
|
|
7
8
|
* Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
|
|
@@ -57,7 +58,7 @@ var WebsocketResponse = class {
|
|
|
57
58
|
this.connection = connection;
|
|
58
59
|
}
|
|
59
60
|
log(pci, severity, msgid, msg) {
|
|
60
|
-
const syslog = this.context
|
|
61
|
+
const syslog = this.context.container?.get("syslog");
|
|
61
62
|
if (!msgid) msgid = "WEBSOCKET RESPONSE";
|
|
62
63
|
return syslog?.log(pci, severity, msgid, msg);
|
|
63
64
|
}
|
|
@@ -70,7 +71,7 @@ var WebsocketResponse = class {
|
|
|
70
71
|
if (payload == null) throw new Error("no data");
|
|
71
72
|
return new Promise((resolve, reject) => {
|
|
72
73
|
if (!this.connection || this.connection.readyState !== Ws.OPEN) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
|
|
73
|
-
const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context
|
|
74
|
+
const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context.server);
|
|
74
75
|
const decision = decideSend(this.connection, max, policy, closeAfterDrops);
|
|
75
76
|
if (decision !== "send") {
|
|
76
77
|
this.logFirstDrop(this.connection, decision, max);
|
|
@@ -93,7 +94,7 @@ var WebsocketResponse = class {
|
|
|
93
94
|
broadcast(data, type) {
|
|
94
95
|
const payload = data ?? this.body;
|
|
95
96
|
if (!payload) return;
|
|
96
|
-
const wss = this.context
|
|
97
|
+
const wss = this.context.server;
|
|
97
98
|
if (!wss) return;
|
|
98
99
|
const sendData = type === "binary" && Buffer.isBuffer(payload) ? payload : payload instanceof Buffer ? payload.toString(this.encoding) : payload;
|
|
99
100
|
const { max, policy, closeAfterDrops } = readBackpressureOptions(wss);
|
|
@@ -124,11 +125,17 @@ var WebsocketResponse = class {
|
|
|
124
125
|
return this.body;
|
|
125
126
|
}
|
|
126
127
|
drop(reasonCode, description) {
|
|
127
|
-
if (this.connection && this.connection.readyState === Ws.OPEN)
|
|
128
|
+
if (this.connection && this.connection.readyState === Ws.OPEN) {
|
|
129
|
+
this.connection.close(reasonCode ?? this.statusCode, toWsCloseReason(description));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
128
132
|
throw new Error("Connection already closed");
|
|
129
133
|
}
|
|
130
134
|
close(reasonCode, description) {
|
|
131
|
-
if (this.connection && this.connection.readyState === Ws.OPEN)
|
|
135
|
+
if (this.connection && this.connection.readyState === Ws.OPEN) {
|
|
136
|
+
this.connection.close(reasonCode ?? this.statusCode, toWsCloseReason(description ?? "closed"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
132
139
|
throw new Error("Connection already closed");
|
|
133
140
|
}
|
|
134
141
|
getStatus() {
|