@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,6 @@
|
|
|
1
1
|
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
2
2
|
import Context from "../Context.js";
|
|
3
|
+
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
3
4
|
import WebsocketResponse from "./Response.js";
|
|
4
5
|
import { sanitizeRequestId } from "../requestId.js";
|
|
5
6
|
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
@@ -10,6 +11,24 @@ import { AsyncResource } from "node:async_hooks";
|
|
|
10
11
|
import url, { URL } from "node:url";
|
|
11
12
|
//#region nodefony/src/context/websocket/WebsocketContext.ts
|
|
12
13
|
/**
|
|
14
|
+
* Appelle l'action résolue et rend TOUJOURS une promesse.
|
|
15
|
+
*
|
|
16
|
+
* `callController` rend une valeur ou une promesse, et peut lever de façon
|
|
17
|
+
* synchrone (#505). Démarré dans l'exécuteur d'une promesse, l'appel transforme
|
|
18
|
+
* une exception synchrone en REJET : les deux sites WS la traitent comme avant
|
|
19
|
+
* — le handshake par son `catch` de fermeture RFC 6455, le message en la
|
|
20
|
+
* rendant à l'appelant sans passer par le `catch` 1011.
|
|
21
|
+
*
|
|
22
|
+
* @param resolver - le résolveur de la connexion.
|
|
23
|
+
* @param data - les arguments de l'action (message reçu).
|
|
24
|
+
* @returns la promesse du rendu, rompue par toute erreur de l'action.
|
|
25
|
+
*/
|
|
26
|
+
function callAsPromise(resolver, data) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
resolve(resolver.callController(data));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
13
32
|
* Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
|
|
14
33
|
* et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
|
|
15
34
|
* sens existe :
|
|
@@ -65,7 +84,7 @@ var WebsocketContext = class extends Context {
|
|
|
65
84
|
this.method = this.getMethod();
|
|
66
85
|
this.origin = req.headers.origin ?? "";
|
|
67
86
|
const checker = this.httpKernel?.getTrustProxyChecker();
|
|
68
|
-
const socketAddress = req.socket
|
|
87
|
+
const socketAddress = req.socket.remoteAddress;
|
|
69
88
|
if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
|
|
70
89
|
this.remoteAddress = this.getRemoteAddress();
|
|
71
90
|
this.acceptedProtocol = req.headers["sec-websocket-protocol"];
|
|
@@ -104,7 +123,7 @@ var WebsocketContext = class extends Context {
|
|
|
104
123
|
proxyHost: fwd.host ?? req.headers["x-forwarded-host"],
|
|
105
124
|
proxyVia: req.headers.via
|
|
106
125
|
};
|
|
107
|
-
this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy
|
|
126
|
+
this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy.proxyVia}`, "DEBUG");
|
|
108
127
|
}
|
|
109
128
|
}
|
|
110
129
|
log(pci, severity, msgid, msg) {
|
|
@@ -117,9 +136,11 @@ var WebsocketContext = class extends Context {
|
|
|
117
136
|
if (!logger) return;
|
|
118
137
|
const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
|
|
119
138
|
const pdu = this.log(entry.text, entry.severity, entry.msgid);
|
|
120
|
-
|
|
139
|
+
pdu.requestId ??= this.requestId;
|
|
121
140
|
return pdu;
|
|
122
|
-
} catch {
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
123
144
|
}
|
|
124
145
|
async connect() {
|
|
125
146
|
if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
|
|
@@ -140,37 +161,38 @@ var WebsocketContext = class extends Context {
|
|
|
140
161
|
this.webSocketState = "error";
|
|
141
162
|
throw new Error("Nodefony Websocket rejected");
|
|
142
163
|
}
|
|
143
|
-
if (!this.resolver) this.resolver = this.router?.resolve(this);
|
|
164
|
+
if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
|
|
144
165
|
else try {
|
|
145
166
|
this.resolver.match(this.resolver.route, this);
|
|
146
167
|
} catch (e) {
|
|
147
|
-
if (!this.
|
|
168
|
+
if (!this.isRejected()) this.reject(e.code ?? void 0, e.message);
|
|
148
169
|
throw e;
|
|
149
170
|
}
|
|
150
171
|
await this.fireAsync("onRequest", this, this.resolver);
|
|
151
172
|
await this.kernel?.fireAsync("onRequest", this, this.resolver);
|
|
152
|
-
if (this.resolver
|
|
173
|
+
if (this.resolver?.resolve) {
|
|
153
174
|
this.setMetaData({ nodefony: { websocket: {
|
|
154
175
|
state: this.webSocketState,
|
|
155
176
|
protocol: this.acceptedProtocol
|
|
156
177
|
} } });
|
|
157
|
-
await this.resolver
|
|
178
|
+
await callAsPromise(this.resolver, data).then(async () => {
|
|
158
179
|
await this.saveSession().then((session) => {
|
|
159
180
|
if (session) this.log(`SAVE SESSION ID : ${session.id}`, "DEBUG");
|
|
160
|
-
}).catch((e) => {
|
|
161
|
-
throw e;
|
|
162
181
|
});
|
|
163
182
|
return this;
|
|
164
183
|
}).catch((error) => {
|
|
165
184
|
if (!this.rejected) {
|
|
166
|
-
if (this.requestEnded)
|
|
185
|
+
if (this.requestEnded) {
|
|
186
|
+
this.close(error.code, error.message);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
167
189
|
this.reject(error.code ?? void 0, error.message);
|
|
168
190
|
this.rejected = true;
|
|
169
191
|
this.webSocketState = "error";
|
|
170
192
|
throw error;
|
|
171
193
|
}
|
|
172
194
|
});
|
|
173
|
-
} else if (!this.
|
|
195
|
+
} else if (!this.isRejected()) {
|
|
174
196
|
this.reject(4004, "Not Found");
|
|
175
197
|
this.rejected = true;
|
|
176
198
|
this.webSocketState = "error";
|
|
@@ -204,7 +226,8 @@ var WebsocketContext = class extends Context {
|
|
|
204
226
|
this.logMessageContent("BROADCAST", payload);
|
|
205
227
|
this.fire("onMessage", payload, this, "BROADCAST");
|
|
206
228
|
this.fire("onBroadcast", payload, this);
|
|
207
|
-
|
|
229
|
+
this.response.broadcast(payload, encoding);
|
|
230
|
+
return;
|
|
208
231
|
}
|
|
209
232
|
}
|
|
210
233
|
return null;
|
|
@@ -219,7 +242,7 @@ var WebsocketContext = class extends Context {
|
|
|
219
242
|
* @param data - charge utile (string, Buffer binaire, objet, ou null).
|
|
220
243
|
*/
|
|
221
244
|
logMessageContent(dir, data) {
|
|
222
|
-
|
|
245
|
+
wsContentLogging ??= this.kernel?.environment !== "production";
|
|
223
246
|
if (!wsContentLogging) return;
|
|
224
247
|
this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
|
|
225
248
|
}
|
|
@@ -292,23 +315,22 @@ var WebsocketContext = class extends Context {
|
|
|
292
315
|
this.webSocketState = "message";
|
|
293
316
|
const message = isBinary ? data : data.toString();
|
|
294
317
|
this.logMessageContent("RECEIVE", message);
|
|
295
|
-
if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data
|
|
318
|
+
if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
296
319
|
try {
|
|
297
|
-
if (!this.resolver) this.resolver = this.router?.resolve(this);
|
|
298
|
-
else this.resolver.match(this.resolver
|
|
320
|
+
if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
|
|
321
|
+
else this.resolver.match(this.resolver.route, this);
|
|
299
322
|
await this.fireAsync("onMessage", message, this, "RECEIVE");
|
|
300
|
-
if (this.resolver
|
|
323
|
+
if (this.resolver?.resolve) {
|
|
301
324
|
this.setMetaData({ nodefony: { websocket: {
|
|
302
325
|
state: this.webSocketState,
|
|
303
326
|
protocol: this.acceptedProtocol
|
|
304
327
|
} } });
|
|
305
|
-
return this.resolver
|
|
306
|
-
throw e;
|
|
307
|
-
});
|
|
328
|
+
return callAsPromise(this.resolver, [message]);
|
|
308
329
|
} else if (!this.rejected) {
|
|
309
330
|
this.reject(4004, "Not Found");
|
|
310
331
|
this.rejected = true;
|
|
311
332
|
}
|
|
333
|
+
return;
|
|
312
334
|
} catch (e) {
|
|
313
335
|
this.reject(1011, "Internal Error");
|
|
314
336
|
throw e;
|
|
@@ -338,7 +360,7 @@ var WebsocketContext = class extends Context {
|
|
|
338
360
|
*/
|
|
339
361
|
onConnectionError(error) {
|
|
340
362
|
this.webSocketState = "error";
|
|
341
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error
|
|
363
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
|
|
342
364
|
this.fire("onError", error, this);
|
|
343
365
|
}
|
|
344
366
|
setScheme() {
|
|
@@ -346,7 +368,7 @@ var WebsocketContext = class extends Context {
|
|
|
346
368
|
}
|
|
347
369
|
getRemoteAddress() {
|
|
348
370
|
if (this.forwarded) return this.forwarded.clientIp;
|
|
349
|
-
return this.request?.socket
|
|
371
|
+
return this.request?.socket.remoteAddress ?? null;
|
|
350
372
|
}
|
|
351
373
|
getHost() {
|
|
352
374
|
return this.request?.headers.host;
|
|
@@ -372,15 +394,26 @@ var WebsocketContext = class extends Context {
|
|
|
372
394
|
super.clean();
|
|
373
395
|
}
|
|
374
396
|
close(reasonCode, description) {
|
|
375
|
-
if (this.response)
|
|
397
|
+
if (this.response) this.response.close(toWsCloseCode(reasonCode), description);
|
|
376
398
|
}
|
|
377
399
|
drop(reasonCode, description) {
|
|
378
|
-
if (this.response)
|
|
400
|
+
if (this.response) this.response.drop(reasonCode, description);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Relit `rejected` sans le rétrécissement de TypeScript : après la garde
|
|
404
|
+
* d'entrée de {@link handle}, le compilateur le croit `false` pour toute la
|
|
405
|
+
* méthode, alors qu'un écouteur `onRequest` (pare-feu) ou le `match` du
|
|
406
|
+
* resolver peut avoir rejeté entre-temps.
|
|
407
|
+
*
|
|
408
|
+
* @returns `true` si la connexion a été rejetée.
|
|
409
|
+
*/
|
|
410
|
+
isRejected() {
|
|
411
|
+
return this.rejected;
|
|
379
412
|
}
|
|
380
413
|
reject(code, message) {
|
|
381
414
|
if (this.connection && this.connection.readyState === Ws.OPEN) {
|
|
382
415
|
const raw = typeof code === "string" ? parseInt(code, 10) : code;
|
|
383
|
-
this.connection.close(toWsCloseCode(raw), message ?? "Rejected");
|
|
416
|
+
this.connection.close(toWsCloseCode(raw), toWsCloseReason(message ?? "Rejected"));
|
|
384
417
|
}
|
|
385
418
|
this.rejected = true;
|
|
386
419
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region nodefony/src/context/websocket/wsCloseReason.ts
|
|
2
|
+
/**
|
|
3
|
+
* Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
|
|
4
|
+
* §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
|
|
5
|
+
*/
|
|
6
|
+
const MAX_WS_CLOSE_REASON_BYTES = 123;
|
|
7
|
+
/** Marqueur de troncature : « … » (U+2026) pèse 3 octets en UTF-8. */
|
|
8
|
+
const ELLIPSIS = "…";
|
|
9
|
+
/**
|
|
10
|
+
* Borne la raison d'une fermeture WebSocket à 123 octets UTF-8, coupée sur une
|
|
11
|
+
* frontière de caractère.
|
|
12
|
+
*
|
|
13
|
+
* `ws.close(code, reason)` LÈVE une `RangeError` au-delà de 123 octets, et la
|
|
14
|
+
* connexion n'est alors PAS fermée : jusqu'en `ws` 8.21 elle restait en
|
|
15
|
+
* `CLOSING` sans trame Close ni minuterie — hors de portée de toute garde
|
|
16
|
+
* `readyState === OPEN` et du heartbeat, donc ouverte jusqu'à la mort TCP ;
|
|
17
|
+
* depuis 8.22 elle reste `OPEN`. Or la raison porte souvent un message
|
|
18
|
+
* d'erreur, dont rien ne borne la longueur.
|
|
19
|
+
*
|
|
20
|
+
* @param reason - raison candidate (message d'erreur, libellé…)
|
|
21
|
+
* @returns la raison telle quelle si elle tient, sinon tronquée et terminée
|
|
22
|
+
* par « … » ; chaîne vide si elle n'est pas une chaîne.
|
|
23
|
+
*/
|
|
24
|
+
function toWsCloseReason(reason) {
|
|
25
|
+
if (typeof reason !== "string") return "";
|
|
26
|
+
if (reason.length <= 41) return reason;
|
|
27
|
+
if (Buffer.byteLength(reason, "utf8") <= 123) return reason;
|
|
28
|
+
const budget = 120;
|
|
29
|
+
let bytes = 0;
|
|
30
|
+
let end = 0;
|
|
31
|
+
for (const char of reason) {
|
|
32
|
+
const cp = char.codePointAt(0);
|
|
33
|
+
const size = cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
34
|
+
if (bytes + size > budget) break;
|
|
35
|
+
bytes += size;
|
|
36
|
+
end += char.length;
|
|
37
|
+
}
|
|
38
|
+
return reason.slice(0, end) + ELLIPSIS;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
|
|
@@ -25,7 +25,7 @@ function cookiesParser(context) {
|
|
|
25
25
|
case "http":
|
|
26
26
|
case "https":
|
|
27
27
|
case "http2":
|
|
28
|
-
if (context.request.request
|
|
28
|
+
if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
|
|
29
29
|
if (cookies) {
|
|
30
30
|
const obj = parser(cookies);
|
|
31
31
|
for (const cookie in obj) {
|
|
@@ -37,7 +37,7 @@ function cookiesParser(context) {
|
|
|
37
37
|
break;
|
|
38
38
|
case "websocket":
|
|
39
39
|
case "websocket-secure": {
|
|
40
|
-
const cookieHeader = context.request?.headers
|
|
40
|
+
const cookieHeader = context.request?.headers.cookie;
|
|
41
41
|
if (cookieHeader) {
|
|
42
42
|
const parsed = parser(cookieHeader);
|
|
43
43
|
for (const name in parsed) {
|
|
@@ -65,7 +65,7 @@ var Cookie = class Cookie {
|
|
|
65
65
|
sameSite;
|
|
66
66
|
priority;
|
|
67
67
|
constructor(cookiesOrName, value, options) {
|
|
68
|
-
this.options = extend({}, cookieDefaultSettings, options
|
|
68
|
+
this.options = extend({}, cookieDefaultSettings, options ?? {});
|
|
69
69
|
if (!cookiesOrName) throw new Error("cookie must have name");
|
|
70
70
|
this.name = cookiesOrName;
|
|
71
71
|
if (typeof cookiesOrName === "string") {
|
|
@@ -32,16 +32,16 @@ var HttpError = class extends nodefonyError {
|
|
|
32
32
|
constructor(message, code, context) {
|
|
33
33
|
super(message, code);
|
|
34
34
|
this.context = context;
|
|
35
|
-
|
|
36
|
-
this.
|
|
35
|
+
const res = context?.response;
|
|
36
|
+
this.response = res ?? void 0;
|
|
37
|
+
this.request = context?.request ?? void 0;
|
|
37
38
|
this.url = this.context?.url;
|
|
38
|
-
if (
|
|
39
|
-
if (!this.message) this.message =
|
|
39
|
+
if (res && code) res.statusCode = code;
|
|
40
|
+
if (!this.message && res) this.message = res.body?.toString() || res.statusMessage || res.getStatusMessage();
|
|
40
41
|
const resolver = context?.resolver;
|
|
41
42
|
this.controller = resolver?.controller?.name ?? void 0;
|
|
42
43
|
this.action = resolver?.actionName ?? void 0;
|
|
43
|
-
|
|
44
|
-
if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage ?? ""}`.trim();
|
|
44
|
+
if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage}`.trim();
|
|
45
45
|
}
|
|
46
46
|
toString() {
|
|
47
47
|
if (this.context) return `${clc.red(this.message)}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region nodefony/src/errors/thrownMessage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
|
|
4
|
+
*
|
|
5
|
+
* Même rendu que `String(value)` pour une `Error` (son `message`) et pour les
|
|
6
|
+
* primitives ; un objet quelconque rend son étiquette (`[object Object]`) sans
|
|
7
|
+
* appeler un `toString` arbitraire ni sérialiser son contenu — ce texte peut
|
|
8
|
+
* finir dans une réponse ou un journal, il ne doit rien divulguer.
|
|
9
|
+
*
|
|
10
|
+
* @param value - la valeur levée.
|
|
11
|
+
* @returns un texte court, jamais vide pour une valeur définie.
|
|
12
|
+
*/
|
|
13
|
+
function thrownMessage(value) {
|
|
14
|
+
if (value instanceof Error) return value.message;
|
|
15
|
+
switch (typeof value) {
|
|
16
|
+
case "string": return value;
|
|
17
|
+
case "number":
|
|
18
|
+
case "boolean":
|
|
19
|
+
case "bigint":
|
|
20
|
+
case "symbol":
|
|
21
|
+
case "undefined": return String(value);
|
|
22
|
+
case "object":
|
|
23
|
+
case "function": return value === null ? "null" : Object.prototype.toString.call(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { thrownMessage };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
1
2
|
//#region nodefony/src/profiler/FrameProfile.ts
|
|
2
3
|
/**
|
|
3
4
|
* Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
|
|
@@ -72,8 +73,8 @@ var FrameProfile = class {
|
|
|
72
73
|
phaseEnd(name) {
|
|
73
74
|
if (!this.timing) return;
|
|
74
75
|
for (let i = this.phases.length - 1; i >= 0; i--) {
|
|
75
|
-
const p = this.phases
|
|
76
|
-
if (p
|
|
76
|
+
const p = this.phases.at(i);
|
|
77
|
+
if (p?.name === name && p.endMs === void 0) {
|
|
77
78
|
p.endMs = performance.now();
|
|
78
79
|
p.durationMs = p.endMs - p.startMs;
|
|
79
80
|
return;
|
|
@@ -88,7 +89,7 @@ var FrameProfile = class {
|
|
|
88
89
|
*/
|
|
89
90
|
finish(status, error) {
|
|
90
91
|
this.response = { statusCode: status };
|
|
91
|
-
if (error !== void 0 && error !== null) this.error = { message:
|
|
92
|
+
if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
//#endregion
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/** Durée totale = (fin de la dernière phase terminée) − (début de la 1ère). */
|
|
3
3
|
function totalDuration(phases) {
|
|
4
4
|
if (phases.length === 0) return null;
|
|
5
|
-
const first = phases
|
|
6
|
-
if (typeof first.startMs !== "number") return null;
|
|
5
|
+
const first = phases.at(0);
|
|
6
|
+
if (first === void 0 || typeof first.startMs !== "number") return null;
|
|
7
7
|
let end = first.startMs;
|
|
8
8
|
for (const p of phases) {
|
|
9
9
|
const e = p.startMs + (p.durationMs ?? 0);
|
|
@@ -71,8 +71,8 @@ var Profiler = class {
|
|
|
71
71
|
recent(limit = 60) {
|
|
72
72
|
const out = [];
|
|
73
73
|
const entries = [...this._buf.values()];
|
|
74
|
-
for (
|
|
75
|
-
|
|
74
|
+
for (const e of entries.reverse()) {
|
|
75
|
+
if (out.length >= limit) break;
|
|
76
76
|
out.push({
|
|
77
77
|
requestId: e.requestId,
|
|
78
78
|
ts: e.ts,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region nodefony/src/servers/kernelServers.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rétrécit la section `servers` de la configuration du kernel (lue dans le sac
|
|
4
|
+
* de config non typé du cœur) en {@link ServersConfig}, sans propager `any`.
|
|
5
|
+
*
|
|
6
|
+
* Seule lecture de `kernel.options.servers` dans `@nodefony/http` : les serveurs
|
|
7
|
+
* et le `HttpKernel` passent tous par ici.
|
|
8
|
+
*
|
|
9
|
+
* @param servers - la valeur brute de `kernel.options.servers`.
|
|
10
|
+
* @returns la section typée, ou `undefined` si elle est absente ou n'est pas un objet.
|
|
11
|
+
*/
|
|
12
|
+
function asServersConfig(servers) {
|
|
13
|
+
return servers !== null && typeof servers === "object" ? servers : void 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
|
|
17
|
+
* serveur est désactivé, absent, ou sans port numérique.
|
|
18
|
+
*
|
|
19
|
+
* @param servers - section `servers` déjà rétrécie ({@link asServersConfig}).
|
|
20
|
+
* @param type - le serveur visé.
|
|
21
|
+
*/
|
|
22
|
+
function configuredServerPort(servers, type) {
|
|
23
|
+
const entry = servers?.[type];
|
|
24
|
+
if (!entry) return void 0;
|
|
25
|
+
const port = entry.port;
|
|
26
|
+
return typeof port === "number" ? port : void 0;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { asServersConfig, configuredServerPort };
|
|
@@ -1,25 +1,53 @@
|
|
|
1
|
-
import { isPortListening } from "nodefony";
|
|
1
|
+
import { isPortListening, resolvePortPolicy } from "nodefony";
|
|
2
|
+
import cluster from "node:cluster";
|
|
2
3
|
//#region nodefony/src/servers/portBinder.ts
|
|
3
|
-
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
4
|
-
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
5
|
+
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
6
|
+
*
|
|
7
|
+
* Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
|
|
8
|
+
* une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
|
|
9
|
+
* en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
|
|
10
|
+
* un CONTRAT en production.
|
|
11
|
+
*
|
|
12
|
+
* ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
|
|
13
|
+
*
|
|
14
|
+
* La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
|
|
15
|
+
* une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
|
|
16
|
+
* prendre le port — et on échoue quand même, après avoir cru le contraire. Le
|
|
17
|
+
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
18
|
+
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
19
|
+
*
|
|
20
|
+
* ## Pourquoi une SONDE malgré cela
|
|
21
|
+
*
|
|
22
|
+
* Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
|
|
23
|
+
* servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
|
|
24
|
+
* processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
|
|
25
|
+
* liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
|
|
26
|
+
* Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
|
|
27
|
+
* l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
|
|
28
|
+
* rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
|
|
29
|
+
*
|
|
30
|
+
* On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
|
|
31
|
+
* liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
|
|
32
|
+
* outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
|
|
33
|
+
* « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
|
|
34
|
+
* atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
|
|
35
|
+
* que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
|
|
36
|
+
* entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
|
|
37
|
+
*
|
|
38
|
+
* ## Pourquoi sauter les ports réservés
|
|
39
|
+
*
|
|
40
|
+
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
41
|
+
* voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
|
|
42
|
+
* AUTRES serveurs convoitent sont donc sautés d'emblée.
|
|
43
|
+
*
|
|
44
|
+
* ## Fail-loud
|
|
45
|
+
*
|
|
46
|
+
* Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
|
|
47
|
+
* là où on l'attend sans le dire est une dégradation silencieuse.
|
|
18
48
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return environment === "development" ? "auto" : "strict";
|
|
22
|
-
}
|
|
49
|
+
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
50
|
+
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
23
51
|
/** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
|
|
24
52
|
function configuredPort(entry) {
|
|
25
53
|
if (!entry) return 0;
|
|
@@ -106,6 +134,8 @@ function portConflictError(port, host) {
|
|
|
106
134
|
error.code = "EADDRINUSE";
|
|
107
135
|
return error;
|
|
108
136
|
}
|
|
137
|
+
/** Plus grand numéro de port TCP (RFC 793) : au-delà, `listen` lève un `RangeError`. */
|
|
138
|
+
const MAX_PORT = 65535;
|
|
109
139
|
/** Prochain candidat : incrémente, en sautant ce que les autres serveurs veulent. */
|
|
110
140
|
function nextCandidate(from, reserved) {
|
|
111
141
|
let port = from + 1;
|
|
@@ -143,12 +173,21 @@ function isPortUnavailable(code, port) {
|
|
|
143
173
|
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
144
174
|
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
145
175
|
*
|
|
176
|
+
* @param server - serveur à mettre en écoute.
|
|
177
|
+
* @param host - hôte passé à `listen`.
|
|
178
|
+
* @param plan - port désiré et nombre de replis permis.
|
|
179
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
180
|
+
* @param sharedListener - vrai dans un worker `node:cluster` : le port y est
|
|
181
|
+
* PARTAGÉ par le maître, et un voisin qui y répond est un frère, pas un
|
|
182
|
+
* conflit. Sans cette exception, `nodefony cluster -w N` ne gardait qu'un
|
|
183
|
+
* worker en vie — les autres mouraient sur « port déjà servi » et
|
|
184
|
+
* bouclaient en relance.
|
|
146
185
|
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
147
186
|
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
148
187
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
149
188
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
150
189
|
*/
|
|
151
|
-
async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
190
|
+
async function bindWithFallback(server, host, plan, probe = isPortListening, sharedListener = cluster.isWorker) {
|
|
152
191
|
let candidate = plan.desired;
|
|
153
192
|
let used = 0;
|
|
154
193
|
/**
|
|
@@ -170,16 +209,19 @@ async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
|
170
209
|
server.listen(port, host);
|
|
171
210
|
});
|
|
172
211
|
for (;;) {
|
|
173
|
-
const conflict = plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
212
|
+
const conflict = sharedListener || plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
174
213
|
const failure = conflict ? portConflictError(candidate, conflict) : await listenOnce(candidate);
|
|
175
214
|
if (failure === null) return {
|
|
176
215
|
address: server.address(),
|
|
177
216
|
shiftedFrom: candidate === plan.desired ? null : plan.desired
|
|
178
217
|
};
|
|
179
|
-
|
|
218
|
+
const code = failure.code;
|
|
219
|
+
if (!conflict && !isPortUnavailable(typeof code === "string" ? code : void 0, candidate)) throw failure;
|
|
180
220
|
if (plan.desired === 0 || used >= plan.attempts) throw failure;
|
|
181
221
|
used += 1;
|
|
182
|
-
|
|
222
|
+
const next = nextCandidate(candidate, plan.reserved);
|
|
223
|
+
if (next > MAX_PORT) throw failure;
|
|
224
|
+
candidate = next;
|
|
183
225
|
}
|
|
184
226
|
}
|
|
185
227
|
//#endregion
|
|
@@ -57,8 +57,9 @@ var Session = class {
|
|
|
57
57
|
constructor(name, options, manager) {
|
|
58
58
|
this.options = extend({}, defaultSessionOptions, options);
|
|
59
59
|
this.manager = manager;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const storage = this.manager.storage;
|
|
61
|
+
this.storage = storage;
|
|
62
|
+
if (!storage) this.status = "disabled";
|
|
62
63
|
this.setName(name);
|
|
63
64
|
this.strategy = this.manager.sessionStrategy;
|
|
64
65
|
}
|
|
@@ -195,7 +196,7 @@ var Session = class {
|
|
|
195
196
|
return true;
|
|
196
197
|
}
|
|
197
198
|
setCookieSession(leftTime, options = {}) {
|
|
198
|
-
if (this.context
|
|
199
|
+
if (this.context?.response) {
|
|
199
200
|
const settings = extend({}, this.options.cookie, options);
|
|
200
201
|
if (leftTime) settings.maxAge = leftTime;
|
|
201
202
|
const cookie = new Cookie(this.context.getSessionCookieName(), this.id, settings);
|
|
@@ -207,7 +208,7 @@ var Session = class {
|
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
209
210
|
deleteCookieSession() {
|
|
210
|
-
if (this.context
|
|
211
|
+
if (this.context?.response) {
|
|
211
212
|
let cookie = this.cookieSession;
|
|
212
213
|
if (cookie) cookie.expires = /* @__PURE__ */ new Date(0);
|
|
213
214
|
else cookie = new Cookie(this.context.getSessionCookieName(), "", { expires: /* @__PURE__ */ new Date(0) });
|
|
@@ -219,11 +220,17 @@ var Session = class {
|
|
|
219
220
|
return this.cookieSession;
|
|
220
221
|
}
|
|
221
222
|
isValidSession(_data, context) {
|
|
222
|
-
if (this.options.refererCheck)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
if (this.options.refererCheck) {
|
|
224
|
+
let sameHost = false;
|
|
225
|
+
try {
|
|
226
|
+
sameHost = this.checkSecureReferer(context);
|
|
227
|
+
} catch {
|
|
228
|
+
sameHost = false;
|
|
229
|
+
}
|
|
230
|
+
if (!sameHost) {
|
|
231
|
+
this.log(`SESSION REFERER MISMATCH ==> ${this.name} : ${this.id}`, "WARNING");
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
227
234
|
}
|
|
228
235
|
const now = Date.now();
|
|
229
236
|
if (this.options.absoluteTimeoutS && this.created) {
|
|
@@ -347,7 +354,8 @@ var Session = class {
|
|
|
347
354
|
user: user ?? this.user ?? ""
|
|
348
355
|
};
|
|
349
356
|
}
|
|
350
|
-
deSerialize(
|
|
357
|
+
deSerialize(serialized) {
|
|
358
|
+
const data = serialized;
|
|
351
359
|
if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
352
360
|
if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
353
361
|
if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
1
2
|
//#region nodefony/src/session/sessionStoreFailure.ts
|
|
2
3
|
/**
|
|
3
4
|
* Reconnaît une table de session absente, quel que soit le dialecte.
|
|
@@ -18,7 +19,7 @@ function looksLikeMissingTable(text) {
|
|
|
18
19
|
* @returns le statut, le corps à servir, et la ligne de journal.
|
|
19
20
|
*/
|
|
20
21
|
function describeSessionStoreFailure(error) {
|
|
21
|
-
const cause = error
|
|
22
|
+
const cause = error === null || error === void 0 ? "cause inconnue" : thrownMessage(error);
|
|
22
23
|
return {
|
|
23
24
|
statusCode: 500,
|
|
24
25
|
body: "Internal Server Error — session store unavailable",
|
|
@@ -82,14 +82,29 @@ var RevocationGuardStorage = class {
|
|
|
82
82
|
constructor(inner) {
|
|
83
83
|
this.inner = inner;
|
|
84
84
|
this.sortableFields = inner.sortableFields;
|
|
85
|
-
if (typeof inner.listAll === "function")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (typeof inner.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
85
|
+
if (typeof inner.listAll === "function") {
|
|
86
|
+
const src = inner;
|
|
87
|
+
this.listAll = (filter) => src.listAll(filter);
|
|
88
|
+
}
|
|
89
|
+
if (typeof inner.listPage === "function") {
|
|
90
|
+
const src = inner;
|
|
91
|
+
this.listPage = (query) => src.listPage(query);
|
|
92
|
+
}
|
|
93
|
+
if (typeof inner.countSessions === "function") {
|
|
94
|
+
const src = inner;
|
|
95
|
+
this.countSessions = (query) => src.countSessions(query);
|
|
96
|
+
}
|
|
97
|
+
if (typeof inner.countDistinctUsers === "function") {
|
|
98
|
+
const src = inner;
|
|
99
|
+
this.countDistinctUsers = (query) => src.countDistinctUsers(query);
|
|
100
|
+
}
|
|
101
|
+
if (typeof inner.touch === "function") {
|
|
102
|
+
const src = inner;
|
|
103
|
+
this.touch = (id, idleSeconds) => {
|
|
104
|
+
if (this.#isRevoked(id)) return Promise.resolve();
|
|
105
|
+
return src.touch(id, idleSeconds);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
93
108
|
}
|
|
94
109
|
read(id) {
|
|
95
110
|
return this.inner.read(id);
|
|
@@ -117,7 +132,7 @@ var RevocationGuardStorage = class {
|
|
|
117
132
|
#revoke(id) {
|
|
118
133
|
const now = Date.now();
|
|
119
134
|
this.#purgeExpired(now);
|
|
120
|
-
|
|
135
|
+
this.#tombstones ??= /* @__PURE__ */ new Map();
|
|
121
136
|
this.#tombstones.set(id, now + TOMBSTONE_TTL_MS);
|
|
122
137
|
}
|
|
123
138
|
#isRevoked(id) {
|