@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.1
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 +7 -0
- package/dist/index.js +4 -2
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -1
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +22 -1
- 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 +25 -13
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +340 -163
- 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 +20 -20
- package/dist/nodefony/service/servers/server-https.js +38 -39
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
- package/dist/nodefony/service/servers/server-websocket.js +8 -7
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +19 -12
- package/dist/nodefony/service/trace.js +1 -1
- package/dist/nodefony/service/upload/upload-service.js +8 -7
- 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 +93 -44
- package/dist/nodefony/src/context/http/Response.js +107 -51
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- 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 +8 -21
- 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 +22 -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 +10 -10
- 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/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 +6 -19
- 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 +20 -20
- package/docs/observabilite.md +25 -25
- package/docs/rate-limit.md +18 -18
- package/docs/servers.md +52 -52
- package/docs/session.md +65 -60
- package/docs/upload.md +36 -30
- package/package.json +7 -7
|
@@ -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() {
|
|
@@ -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 };
|