@nodefony/http 10.0.0-beta.6 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -2
- package/dist/nodefony/config/config.js +25 -2
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +41 -9
- package/dist/nodefony/service/http-kernel.js +198 -19
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +1 -1
- package/dist/nodefony/service/servers/clientError.js +26 -1
- package/dist/nodefony/service/servers/server-h2c.js +3 -3
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +4 -4
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/servers/serverShutdown.js +14 -36
- package/dist/nodefony/service/sessions/sessions-service.js +6 -6
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +9 -4
- package/dist/nodefony/src/context/http/HttpContext.js +86 -8
- package/dist/nodefony/src/context/http/Request.js +96 -16
- package/dist/nodefony/src/context/http/Response.js +120 -17
- package/dist/nodefony/src/context/http/SseStream.js +4 -4
- package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Response.js +47 -8
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/websocket/Response.js +11 -1
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
- package/dist/nodefony/src/cookies/cookie.js +3 -1
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +1 -1
- package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/nodefony/config/config.d.ts +10 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
- package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/docs/byte-stream.md +52 -17
- package/docs/diagnostics-channel.md +45 -1
- package/docs/servers.md +57 -44
- package/docs/upload.md +43 -17
- package/package.json +26 -6
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// nodefony/src/grpc/refusal.ts
|
|
2
|
+
/**
|
|
3
|
+
* Refus d'un appel gRPC au format de son protocole (ADR-0026 D5 point 2, D13
|
|
4
|
+
* point 2). Sans Connect : une application sans service gRPC charge ce module
|
|
5
|
+
* avec le noyau, et n'y paie rien tant qu'aucune erreur n'est rendue.
|
|
6
|
+
*
|
|
7
|
+
* Le protocole se reconnaît aux en-têtes de la requête et se code en petit
|
|
8
|
+
* entier (`GRPC_WIRE_*`, 0 = aucun) : une valeur toujours du même type, lue
|
|
9
|
+
* sur le chemin d'erreur seulement.
|
|
10
|
+
*/
|
|
11
|
+
/** Requête qui ne parle aucun protocole gRPC. */
|
|
12
|
+
const GRPC_WIRE_NONE = 0;
|
|
13
|
+
/** gRPC natif (`application/grpc`, `application/grpc+proto`…). */
|
|
14
|
+
const GRPC_WIRE_GRPC = 1;
|
|
15
|
+
/** gRPC-Web (`application/grpc-web`, `application/grpc-web-text`…). */
|
|
16
|
+
const GRPC_WIRE_WEB = 2;
|
|
17
|
+
/** Connect unaire (`connect-protocol-version`, ou GET `?connect=v1`). */
|
|
18
|
+
const GRPC_WIRE_CONNECT = 3;
|
|
19
|
+
/** Connect en flux (`application/connect+proto`, `application/connect+json`). */
|
|
20
|
+
const GRPC_WIRE_CONNECT_STREAM = 4;
|
|
21
|
+
/** `"application/".length` : le sous-type commence là. */
|
|
22
|
+
const SUBTYPE = 12;
|
|
23
|
+
const PLUS = 43;
|
|
24
|
+
const SEMICOLON = 59;
|
|
25
|
+
const SPACE = 32;
|
|
26
|
+
const HYPHEN = 45;
|
|
27
|
+
/** Le sous-type s'arrête ici : fin, suffixe `+codec`, paramètre, espace. */
|
|
28
|
+
function endsSubtype(code) {
|
|
29
|
+
return Number.isNaN(code) || code === PLUS || code === SEMICOLON || code === SPACE;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reconnaît le protocole gRPC d'une requête à ses en-têtes : `content-type`
|
|
33
|
+
* d'abord, puis `connect-protocol-version`. Une API JSON ordinaire rend
|
|
34
|
+
* `GRPC_WIRE_NONE`.
|
|
35
|
+
*
|
|
36
|
+
* @param headers - les en-têtes de la requête.
|
|
37
|
+
* @param connectGet - `true` pour un GET qui porte `connect=v1` (Connect
|
|
38
|
+
* unaire en GET, sans en-tête propre).
|
|
39
|
+
* @returns le protocole reconnu.
|
|
40
|
+
*/
|
|
41
|
+
function grpcWireOf(headers, connectGet = false) {
|
|
42
|
+
const type = headers["content-type"];
|
|
43
|
+
if (type?.startsWith("application/") === true) {
|
|
44
|
+
if (type.startsWith("grpc", SUBTYPE)) {
|
|
45
|
+
const next = type.charCodeAt(16);
|
|
46
|
+
if (endsSubtype(next)) return 1;
|
|
47
|
+
if (next === HYPHEN && type.startsWith("-web", 16)) {
|
|
48
|
+
const after = type.charCodeAt(20);
|
|
49
|
+
if (endsSubtype(after) || after === HYPHEN) return 2;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (type.startsWith("connect+", SUBTYPE)) return 4;
|
|
54
|
+
}
|
|
55
|
+
if (headers["connect-protocol-version"] !== void 0 || connectGet) return 3;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Noms des codes gRPC (`google-rpc-code.proto`), en minuscules comme Connect
|
|
60
|
+
* les écrit, indexés par le code : un accès direct, aucune table de hachage.
|
|
61
|
+
*/
|
|
62
|
+
const CODE_NAMES = Object.freeze([
|
|
63
|
+
"ok",
|
|
64
|
+
"cancelled",
|
|
65
|
+
"unknown",
|
|
66
|
+
"invalid_argument",
|
|
67
|
+
"deadline_exceeded",
|
|
68
|
+
"not_found",
|
|
69
|
+
"already_exists",
|
|
70
|
+
"permission_denied",
|
|
71
|
+
"resource_exhausted",
|
|
72
|
+
"failed_precondition",
|
|
73
|
+
"aborted",
|
|
74
|
+
"out_of_range",
|
|
75
|
+
"unimplemented",
|
|
76
|
+
"internal",
|
|
77
|
+
"unavailable",
|
|
78
|
+
"data_loss",
|
|
79
|
+
"unauthenticated"
|
|
80
|
+
]);
|
|
81
|
+
/** Les codes en texte décimal, pour l'en-tête `grpc-status` : aucune conversion par refus. */
|
|
82
|
+
const CODE_TEXTS = Object.freeze(CODE_NAMES.map((_name, code) => String(code)));
|
|
83
|
+
/** 16 `UNAUTHENTICATED`. */
|
|
84
|
+
const GRPC_UNAUTHENTICATED = 16;
|
|
85
|
+
/** 7 `PERMISSION_DENIED`. */
|
|
86
|
+
const GRPC_PERMISSION_DENIED = 7;
|
|
87
|
+
/**
|
|
88
|
+
* Le nom d'un code gRPC (`unimplemented`, `unauthenticated`…) — le libellé
|
|
89
|
+
* générique d'un refus (ADR-0026 D5 point 4).
|
|
90
|
+
*
|
|
91
|
+
* @param code - le code gRPC, de 0 à 16.
|
|
92
|
+
* @returns son nom ; `unknown` hors de la plage.
|
|
93
|
+
*/
|
|
94
|
+
function grpcCodeName(code) {
|
|
95
|
+
return CODE_NAMES[code] ?? "unknown";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Les mêmes noms tels que les écrivent `google/rpc/code.proto` et les
|
|
99
|
+
* conventions OpenTelemetry (`rpc.status_code`) : `OK`, `UNAVAILABLE`… Tirés
|
|
100
|
+
* une fois, au chargement, de {@link CODE_NAMES} — une seule liste.
|
|
101
|
+
*/
|
|
102
|
+
const STATUS_NAMES = Object.freeze(CODE_NAMES.map((name) => name.toUpperCase()));
|
|
103
|
+
/**
|
|
104
|
+
* Le nom d'un code gRPC tel que le publient la ligne d'accès et le canal de
|
|
105
|
+
* traçage (`UNAVAILABLE`) — ADR-0026 D10 point 1.
|
|
106
|
+
*
|
|
107
|
+
* @param code - le code gRPC, de 0 à 16.
|
|
108
|
+
* @returns son nom ; `UNKNOWN` hors de la plage.
|
|
109
|
+
*/
|
|
110
|
+
function grpcStatusName(code) {
|
|
111
|
+
return STATUS_NAMES[code] ?? "UNKNOWN";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Le code gRPC d'un refus de transport — rendu avant l'implémentation (chemin
|
|
115
|
+
* inconnu, méthode refusée, limite de débit, pare-feu) — en gRPC natif et en
|
|
116
|
+
* gRPC-Web (ADR-0026 D13 point 2). Distincte de la table des erreurs de
|
|
117
|
+
* l'application (D7) ; seule la règle d'identité de 401/403 est commune (D5).
|
|
118
|
+
*
|
|
119
|
+
* @param status - le statut HTTP que le noyau aurait rendu.
|
|
120
|
+
* @param identified - un appelant authentifié est connu.
|
|
121
|
+
* @returns le code gRPC.
|
|
122
|
+
*/
|
|
123
|
+
function transportRefusalCode(status, identified) {
|
|
124
|
+
switch (status) {
|
|
125
|
+
case 400: return 13;
|
|
126
|
+
case 401:
|
|
127
|
+
case 403: return identified ? 7 : 16;
|
|
128
|
+
case 404:
|
|
129
|
+
case 405: return 12;
|
|
130
|
+
case 421:
|
|
131
|
+
case 429:
|
|
132
|
+
case 502:
|
|
133
|
+
case 503:
|
|
134
|
+
case 504: return 14;
|
|
135
|
+
default: return 2;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Le code gRPC d'une erreur levée par l'APPLICATION pendant un appel — une
|
|
140
|
+
* `HttpError` d'un service partagé avec des routes HTTP, une validation, un
|
|
141
|
+
* conflit (ADR-0026 D7, table « HTTP Mapping » de `google/rpc/code.proto`).
|
|
142
|
+
* Distincte de la table des refus de transport ({@link transportRefusalCode}) :
|
|
143
|
+
* un 404 levé par un service dit « entité introuvable » (5), un chemin inconnu
|
|
144
|
+
* dit « méthode absente » (12). Seule la règle d'identité de 401/403 est
|
|
145
|
+
* commune (D5).
|
|
146
|
+
*
|
|
147
|
+
* `code.proto` associe 409 à `ABORTED` et à `ALREADY_EXISTS` : `ABORTED`, le
|
|
148
|
+
* sens le plus large, est retenu ; `ALREADY_EXISTS` se lève en `ConnectError`.
|
|
149
|
+
*
|
|
150
|
+
* @param status - le statut HTTP que rendrait une route HTTP.
|
|
151
|
+
* @param identified - un appelant authentifié est connu.
|
|
152
|
+
* @returns le code gRPC.
|
|
153
|
+
*/
|
|
154
|
+
function applicationErrorCode(status, identified) {
|
|
155
|
+
switch (status) {
|
|
156
|
+
case 400:
|
|
157
|
+
case 422: return 3;
|
|
158
|
+
case 401:
|
|
159
|
+
case 403: return identified ? 7 : 16;
|
|
160
|
+
case 404: return 5;
|
|
161
|
+
case 409: return 10;
|
|
162
|
+
case 412: return 9;
|
|
163
|
+
case 413:
|
|
164
|
+
case 429: return 8;
|
|
165
|
+
case 499: return 1;
|
|
166
|
+
case 501: return 12;
|
|
167
|
+
case 502:
|
|
168
|
+
case 503: return 14;
|
|
169
|
+
case 408:
|
|
170
|
+
case 504: return 4;
|
|
171
|
+
default: return status >= 500 && status <= 599 ? 13 : 2;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Le code d'un refus en protocole Connect : celui qu'un client DÉDUIT du
|
|
176
|
+
* statut HTTP (`connect-protocol.md`, « HTTP to Error Code »), puisque le
|
|
177
|
+
* statut voyage avec la réponse et doit s'accorder au code. 405 et 421 y
|
|
178
|
+
* valent donc `unknown` (ADR-0026 D13, précision du lot 3) ; 401/403 suivent
|
|
179
|
+
* la règle d'identité (D5).
|
|
180
|
+
*
|
|
181
|
+
* @param status - le statut HTTP du refus.
|
|
182
|
+
* @param identified - un appelant authentifié est connu.
|
|
183
|
+
* @returns le code gRPC.
|
|
184
|
+
*/
|
|
185
|
+
function connectRefusalCode(status, identified) {
|
|
186
|
+
switch (status) {
|
|
187
|
+
case 400: return 13;
|
|
188
|
+
case 401:
|
|
189
|
+
case 403: return identified ? 7 : 16;
|
|
190
|
+
case 404: return 12;
|
|
191
|
+
case 429:
|
|
192
|
+
case 502:
|
|
193
|
+
case 503:
|
|
194
|
+
case 504: return 14;
|
|
195
|
+
default: return 2;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Drapeau d'un message de fin de flux Connect (`connect-protocol.md`). */
|
|
199
|
+
const CONNECT_END_STREAM = 2;
|
|
200
|
+
/**
|
|
201
|
+
* Pose le statut et les en-têtes d'un refus au format du protocole, et rend
|
|
202
|
+
* le corps à envoyer — vide en gRPC et gRPC-Web, dont le statut voyage dans
|
|
203
|
+
* les en-têtes (_Trailers-Only_ ; en HTTP/2, `Http2Response.writeHead` termine
|
|
204
|
+
* alors le flux sur la trame d'en-têtes).
|
|
205
|
+
*
|
|
206
|
+
* | Protocole | Statut HTTP | Corps |
|
|
207
|
+
* | --------------- | ------------------------------ | -------------------------------------- |
|
|
208
|
+
* | gRPC, gRPC-Web | 200 | aucun |
|
|
209
|
+
* | Connect unaire | celui du refus (401/403 : D5) | `{"code":"<nom>"}` |
|
|
210
|
+
* | Connect en flux | 200 | message de fin `{"error":{…}}` |
|
|
211
|
+
*
|
|
212
|
+
* Le message n'est jamais celui de l'erreur, qui nomme parfois la zone
|
|
213
|
+
* refusée : seul le nom du code part (D5 point 4).
|
|
214
|
+
*
|
|
215
|
+
* @param response - la réponse du contexte, pas encore envoyée.
|
|
216
|
+
* @param wire - le protocole de la requête, autre que `GRPC_WIRE_NONE`.
|
|
217
|
+
* @param status - le statut HTTP que le noyau aurait rendu.
|
|
218
|
+
* @param identified - un appelant authentifié est connu.
|
|
219
|
+
* @param contentType - le `content-type` de la requête, recopié en gRPC,
|
|
220
|
+
* gRPC-Web et Connect en flux (déjà reconnu par {@link grpcWireOf}).
|
|
221
|
+
* @returns le corps : texte vide, JSON, ou octets d'un message de fin.
|
|
222
|
+
*/
|
|
223
|
+
function prepareGrpcRefusal(response, wire, status, identified, contentType) {
|
|
224
|
+
if (wire === 3) {
|
|
225
|
+
const code = connectRefusalCode(status, identified);
|
|
226
|
+
const connectStatus = code === 16 ? 401 : code === 7 ? 403 : status;
|
|
227
|
+
response.setStatusCode(connectStatus);
|
|
228
|
+
response.setHeader("content-type", "application/json");
|
|
229
|
+
return `{"code":"${grpcCodeName(code)}"}`;
|
|
230
|
+
}
|
|
231
|
+
response.setStatusCode(200, "OK");
|
|
232
|
+
if (contentType !== void 0) response.setHeader("content-type", contentType);
|
|
233
|
+
if (wire === 4) return connectEndStream(`{"error":{"code":"${grpcCodeName(connectRefusalCode(status, identified))}"}}`);
|
|
234
|
+
const code = transportRefusalCode(status, identified);
|
|
235
|
+
response.setHeader("grpc-status", CODE_TEXTS[code] ?? "2");
|
|
236
|
+
response.setHeader("grpc-message", grpcCodeName(code));
|
|
237
|
+
return "";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Un message de fin de flux Connect : drapeau, longueur (4 octets, gros
|
|
241
|
+
* boutiste), puis le JSON. Le JSON n'est fait que de noms de codes ASCII :
|
|
242
|
+
* un octet par caractère, aucun encodeur.
|
|
243
|
+
*/
|
|
244
|
+
function connectEndStream(json) {
|
|
245
|
+
const length = json.length;
|
|
246
|
+
const frame = new Uint8Array(5 + length);
|
|
247
|
+
frame[0] = CONNECT_END_STREAM;
|
|
248
|
+
new DataView(frame.buffer).setUint32(1, length);
|
|
249
|
+
for (let i = 0; i < length; i++) frame[5 + i] = json.charCodeAt(i);
|
|
250
|
+
return frame;
|
|
251
|
+
}
|
|
252
|
+
/** Au moins une route gRPC est déclarée dans le processus. */
|
|
253
|
+
let grpcRoutesDeclared = false;
|
|
254
|
+
/**
|
|
255
|
+
* Marque une route comme servie par un point d'entrée gRPC — au démarrage,
|
|
256
|
+
* par `@nodefony/framework/grpc`. Allume la garde qui refuse en 12 une requête
|
|
257
|
+
* gRPC native aboutie à une route qui n'est pas gRPC (ADR-0026 D13 point 3) :
|
|
258
|
+
* tant qu'aucune route n'est marquée, la garde coûte une lecture de booléen.
|
|
259
|
+
*
|
|
260
|
+
* @param route - la route montée pour une méthode du contrat.
|
|
261
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
262
|
+
*/
|
|
263
|
+
function markGrpcRoute(route) {
|
|
264
|
+
route.grpc = true;
|
|
265
|
+
grpcRoutesDeclared = true;
|
|
266
|
+
}
|
|
267
|
+
/** `true` dès qu'une route gRPC est déclarée dans le processus. */
|
|
268
|
+
function hasGrpcRoutes() {
|
|
269
|
+
return grpcRoutesDeclared;
|
|
270
|
+
}
|
|
271
|
+
export { GRPC_PERMISSION_DENIED, GRPC_UNAUTHENTICATED, GRPC_WIRE_CONNECT, GRPC_WIRE_CONNECT_STREAM, GRPC_WIRE_GRPC, GRPC_WIRE_NONE, GRPC_WIRE_WEB, applicationErrorCode, connectRefusalCode, grpcCodeName, grpcStatusName, grpcWireOf, hasGrpcRoutes, markGrpcRoute, prepareGrpcRefusal, transportRefusalCode };
|
|
@@ -121,8 +121,8 @@ function actionSelfMs(entry) {
|
|
|
121
121
|
}
|
|
122
122
|
inner.sort((a, b) => a[0] - b[0]);
|
|
123
123
|
let covered = 0;
|
|
124
|
-
let curStart = -
|
|
125
|
-
let curEnd = -
|
|
124
|
+
let curStart = -1e400;
|
|
125
|
+
let curEnd = -1e400;
|
|
126
126
|
for (const [s, e] of inner) if (s > curEnd) {
|
|
127
127
|
if (curEnd > curStart) covered += curEnd - curStart;
|
|
128
128
|
curStart = s;
|
|
@@ -597,4 +597,4 @@ function forwardUpgrade(req, socket, head, mount, target, from, relay) {
|
|
|
597
597
|
});
|
|
598
598
|
upstream.end();
|
|
599
599
|
}
|
|
600
|
-
export { MAX_VIA_HOPS, bodyFramingOf, forwardRequest, forwardUpgrade, rawHead, receivedMaxForwards, refuseMalformedHandshake, refuseUpgrade, requestHeadersFor, responseHeadersFor, rewriteLocation, upstreamPath, viaLoops };
|
|
600
|
+
export { HOP_BY_HOP, MAX_VIA_HOPS, bodyFramingOf, forwardRequest, forwardUpgrade, rawHead, receivedMaxForwards, refuseMalformedHandshake, refuseUpgrade, requestHeadersFor, responseHeadersFor, rewriteLocation, upstreamPath, viaLoops };
|
|
@@ -30,6 +30,30 @@ function idleTimeoutSeconds(intro) {
|
|
|
30
30
|
if (intro.keepaliveIntervalMs <= 0) return 3600;
|
|
31
31
|
return Math.max(300, Math.ceil(intro.keepaliveIntervalMs * 4 / 1e3));
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Port h2c que le proxy doit joindre, ou `null` — règle UNIQUE des deux
|
|
35
|
+
* générateurs.
|
|
36
|
+
*
|
|
37
|
+
* Seulement vers un backend en clair : le re-chiffrement vise le port TLS, qui
|
|
38
|
+
* négocie déjà HTTP/2 par ALPN, et mélanger les deux liens ferait voyager une
|
|
39
|
+
* partie du trafic en clair alors que l'utilisateur a demandé le chiffrement.
|
|
40
|
+
*
|
|
41
|
+
* @param intro - modèle d'introspection Nodefony.
|
|
42
|
+
* @returns le port h2c à joindre, ou `null` s'il n'y a pas de lien h2c.
|
|
43
|
+
*/
|
|
44
|
+
function upstreamH2cPort(intro) {
|
|
45
|
+
return intro.reencrypt ? null : intro.h2cPort ?? null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Le `content-type` d'un appel gRPC natif, en expression régulière nginx :
|
|
49
|
+
* `application/grpc`, suivi de rien, d'un sous-type (`+proto`, `+json`) ou de
|
|
50
|
+
* paramètres (`;`).
|
|
51
|
+
*
|
|
52
|
+
* gRPC-Web (`application/grpc-web…`) n'y entre PAS, et c'est voulu : il est
|
|
53
|
+
* conçu pour HTTP/1.1, le chemin ordinaire le sert déjà, et l'envoyer sur le
|
|
54
|
+
* lien h2c retirerait la traduction que le serveur fait pour le navigateur.
|
|
55
|
+
*/
|
|
56
|
+
const NGINX_GRPC_CONTENT_TYPE = "^application/grpc($|[+;])";
|
|
33
57
|
/** `server_name` nginx / hôte de comparaison — IP et `0.0.0.0` exclus. */
|
|
34
58
|
function serverNames(domains) {
|
|
35
59
|
const names = domains.filter((d) => d && d !== "0.0.0.0" && !isIpLiteral(d));
|
|
@@ -39,15 +63,23 @@ function serverNames(domains) {
|
|
|
39
63
|
function isIpLiteral(host) {
|
|
40
64
|
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
|
41
65
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
66
|
+
/**
|
|
67
|
+
* En-têtes d'identité du client (pattern EDGE : on ÉCRASE X-Forwarded-For),
|
|
68
|
+
* communs au relais HTTP (`proxy_set_header`) et au relais gRPC
|
|
69
|
+
* (`grpc_set_header`) — `__SET__` reçoit la directive. Un seul texte : les deux
|
|
70
|
+
* chemins annoncent au serveur le même client, sinon `trustProxy` lirait deux
|
|
71
|
+
* vérités selon le protocole.
|
|
72
|
+
*/
|
|
73
|
+
const NGINX_CLIENT_HEADERS_TPL = ` # EDGE (face client) : on ÉCRASE X-Forwarded-For avec la SEULE IP vue par
|
|
44
74
|
# nginx → toute valeur forgée par le client est jetée (RFC 7239 §8.1).
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
__SET__ Host $host;
|
|
76
|
+
__SET__ X-Real-IP $remote_addr;
|
|
77
|
+
__SET__ X-Forwarded-For $remote_addr;
|
|
78
|
+
__SET__ X-Forwarded-Proto $scheme;
|
|
79
|
+
__SET__ X-Forwarded-Host $host;
|
|
80
|
+
__SET__ X-Forwarded-Port $server_port;`;
|
|
81
|
+
/** En-têtes forwarded nginx (pattern EDGE : on ÉCRASE X-Forwarded-For). */
|
|
82
|
+
const NGINX_FORWARD_HEADERS_TPL = `${NGINX_CLIENT_HEADERS_TPL.replaceAll("__SET__ ", "proxy_set_header ")}
|
|
51
83
|
# WebSocket (Nodefony co-héberge HTTP + WS sur le même port).
|
|
52
84
|
proxy_set_header Upgrade $http_upgrade;
|
|
53
85
|
proxy_set_header Connection $connection_upgrade;
|
|
@@ -61,6 +93,39 @@ function nginxForwardHeaders(idleSeconds) {
|
|
|
61
93
|
return NGINX_FORWARD_HEADERS_TPL.replaceAll("__IDLE__", String(idleSeconds));
|
|
62
94
|
}
|
|
63
95
|
/**
|
|
96
|
+
* Le relais gRPC d'un bloc `server` : aiguillage par `content-type`, puis la
|
|
97
|
+
* location nommée qui parle HTTP/2 au port h2c.
|
|
98
|
+
*
|
|
99
|
+
* nginx ne parle HTTP/2 à un amont que par `grpc_pass` (`proxy_pass` s'arrête à
|
|
100
|
+
* HTTP/1.1) ; or gRPC n'existe qu'en HTTP/2, avec son statut dans les trailers.
|
|
101
|
+
* Une `location` ne choisit que sur l'URI, et l'URI d'un appel gRPC
|
|
102
|
+
* (`/paquet.Service/Méthode`) ne se distingue d'aucune autre : c'est donc le
|
|
103
|
+
* `content-type` qui aiguille, au niveau du `server`, par un `if` qui ne fait
|
|
104
|
+
* que `return` — la seule forme qui ne se mêle pas aux `try_files` des
|
|
105
|
+
* locations. Le `418` est un code INTERNE (jamais rendu au client) que
|
|
106
|
+
* `error_page` convertit en saut vers `@grpc`.
|
|
107
|
+
*
|
|
108
|
+
* @param idleSeconds - inactivité tolérée, dérivée du heartbeat WebSocket.
|
|
109
|
+
* @returns les lignes à poser dans le bloc `server`.
|
|
110
|
+
*/
|
|
111
|
+
function nginxGrpcRelay(idleSeconds) {
|
|
112
|
+
return [
|
|
113
|
+
"",
|
|
114
|
+
" # gRPC (servers.h2c) : HTTP/2 jusqu'au backend, seul chemin où ses trailers",
|
|
115
|
+
" # (grpc-status) survivent. gRPC-Web reste sur le chemin HTTP/1.1 ordinaire.",
|
|
116
|
+
` if ($content_type ~* "${NGINX_GRPC_CONTENT_TYPE}") { return 418; }`,
|
|
117
|
+
" error_page 418 = @grpc;",
|
|
118
|
+
"",
|
|
119
|
+
" location @grpc {",
|
|
120
|
+
" grpc_pass grpc://nodefony_h2c;",
|
|
121
|
+
NGINX_CLIENT_HEADERS_TPL.replaceAll("__SET__ ", "grpc_set_header "),
|
|
122
|
+
` grpc_read_timeout ${idleSeconds}s;`,
|
|
123
|
+
` grpc_send_timeout ${idleSeconds}s;`,
|
|
124
|
+
" client_max_body_size 0;",
|
|
125
|
+
" }"
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
64
129
|
* Génère une configuration nginx complète (reverse-proxy + offload statiques).
|
|
65
130
|
*
|
|
66
131
|
* @param intro - modèle d'introspection Nodefony.
|
|
@@ -71,10 +136,15 @@ function generateNginxConfig(intro) {
|
|
|
71
136
|
const backendPort = intro.reencrypt ? intro.httpsPort : intro.httpPort;
|
|
72
137
|
const idleSeconds = idleTimeoutSeconds(intro);
|
|
73
138
|
const lines = [];
|
|
74
|
-
lines.push("# Généré par `nodefony proxy:generate nginx` — NE PAS éditer à la main.", "# Reverse-proxy dérivé de l'introspection Nodefony (domaines, statiques, ports).", "worker_processes auto;", "events { worker_connections 1024; }", "", "http {", " include /etc/nginx/mime.types;", " default_type application/octet-stream;", "", " server_tokens off;", "", " sendfile on;", " tcp_nopush on;", " tcp_nodelay on;", "", " gzip on;", " gzip_vary on;", " gzip_min_length 1024;", " gzip_proxied any;", " gzip_types text/plain text/css text/xml application/javascript application/json application/xml image/svg+xml application/manifest+json;", "", " # Upgrade WebSocket — HTTP et WS co-habitent sur le même port Nodefony.", " map $http_upgrade $connection_upgrade { default upgrade; '' close; }", "", ` upstream nodefony { server ${intro.backendHost}:${backendPort}; keepalive 32; }
|
|
139
|
+
lines.push("# Généré par `nodefony proxy:generate nginx` — NE PAS éditer à la main.", "# Reverse-proxy dérivé de l'introspection Nodefony (domaines, statiques, ports).", "worker_processes auto;", "events { worker_connections 1024; }", "", "http {", " include /etc/nginx/mime.types;", " default_type application/octet-stream;", "", " server_tokens off;", "", " sendfile on;", " tcp_nopush on;", " tcp_nodelay on;", "", " gzip on;", " gzip_vary on;", " gzip_min_length 1024;", " gzip_proxied any;", " gzip_types text/plain text/css text/xml application/javascript application/json application/xml image/svg+xml application/manifest+json;", "", " # Upgrade WebSocket — HTTP et WS co-habitent sur le même port Nodefony.", " map $http_upgrade $connection_upgrade { default upgrade; '' close; }", "", ` upstream nodefony { server ${intro.backendHost}:${backendPort}; keepalive 32; }`);
|
|
140
|
+
const h2cPort = upstreamH2cPort(intro);
|
|
141
|
+
if (h2cPort === null) lines.push(intro.reencrypt && intro.h2cPort ? " # gRPC : non relayé — le re-chiffrement vise le port TLS ; h2c reste un lien en clair." : " # gRPC : non relayé — activer `servers.h2c` côté Nodefony puis régénérer.");
|
|
142
|
+
else lines.push(` upstream nodefony_h2c { server ${intro.backendHost}:${h2cPort}; keepalive 32; }`);
|
|
143
|
+
lines.push("");
|
|
75
144
|
if (intro.maxBodyBytes > 0) lines.push(` # Aligné sur \`http.maxBodySize\` (${intro.maxBodyBytes} octets) — sans quoi`, " # nginx rendrait 413 à 1 Mo, son défaut, sans que le serveur le sache.", ` client_max_body_size ${intro.maxBodyBytes};`, "");
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
const grpc = h2cPort !== null;
|
|
146
|
+
lines.push(...nginxServerBlock(intro, scheme, idleSeconds, null, grpc));
|
|
147
|
+
if (intro.tls) lines.push("", ...nginxServerBlock(intro, scheme, idleSeconds, intro.tls, grpc));
|
|
78
148
|
lines.push("}", "");
|
|
79
149
|
return lines.join("\n");
|
|
80
150
|
}
|
|
@@ -90,12 +160,18 @@ function generateNginxConfig(intro) {
|
|
|
90
160
|
* @param scheme - `http` ou `https` vers le BACKEND (re-chiffrement).
|
|
91
161
|
* @param idleSeconds - inactivité tolérée, dérivée du heartbeat WebSocket.
|
|
92
162
|
* @param tls - terminaison TLS de CE bloc, ou `null` pour une écoute en clair.
|
|
163
|
+
* @param grpc - relayer gRPC vers l'amont `nodefony_h2c` (h2c actif).
|
|
93
164
|
* @returns les lignes du bloc `server`.
|
|
94
165
|
*/
|
|
95
|
-
function nginxServerBlock(intro, scheme, idleSeconds, tls) {
|
|
166
|
+
function nginxServerBlock(intro, scheme, idleSeconds, tls, grpc) {
|
|
96
167
|
const lines = [" server {"];
|
|
97
168
|
if (tls) lines.push(` listen ${tls.listen} ssl;`, " http2 on;", ` server_name ${serverNames(intro.domains)};`, "", " # Terminaison TLS au frontal. Ces chemins sont ceux d'un MONTAGE, à", " # pourvoir au déploiement (volume compose, secret k8s) : une clé privée", " # n'entre pas dans une image, où la couche reste lisible même effacée.", ` ssl_certificate ${tls.certPath};`, ` ssl_certificate_key ${tls.keyPath};`, " ssl_protocols TLSv1.2 TLSv1.3;", " ssl_session_cache shared:SSL:10m;", " ssl_session_timeout 1h;", "", " error_page 497 =301 https://$http_host$request_uri;");
|
|
98
|
-
else
|
|
169
|
+
else {
|
|
170
|
+
lines.push(` listen ${intro.listen};`);
|
|
171
|
+
if (grpc) lines.push(" http2 on;");
|
|
172
|
+
lines.push(` server_name ${serverNames(intro.domains)};`);
|
|
173
|
+
}
|
|
174
|
+
if (grpc) lines.push(...nginxGrpcRelay(idleSeconds));
|
|
99
175
|
if (intro.reencrypt) lines.push(" # Re-encrypt : valider le cert backend (cf docker/certs).", " # proxy_ssl_trusted_certificate /etc/nginx/certs/ca.pem;", " # proxy_ssl_verify on; proxy_ssl_name nodefony.com;");
|
|
100
176
|
for (const m of intro.mounts) {
|
|
101
177
|
const prefix = ensureTrailingSlash(m.prefix);
|
|
@@ -125,7 +201,7 @@ function nginxServerBlock(intro, scheme, idleSeconds, tls) {
|
|
|
125
201
|
*/
|
|
126
202
|
function generateHaproxyConfig(intro) {
|
|
127
203
|
const backendPort = intro.reencrypt ? intro.httpsPort : intro.httpPort;
|
|
128
|
-
const h2cPort = intro
|
|
204
|
+
const h2cPort = upstreamH2cPort(intro);
|
|
129
205
|
const idleSeconds = idleTimeoutSeconds(intro);
|
|
130
206
|
const serverSsl = intro.reencrypt ? ` ssl ca-file /etc/haproxy/certs/ca.pem verify required verifyhost ${firstDomain(intro)} sni str(${firstDomain(intro)})` : "";
|
|
131
207
|
const readiness = intro.readinessPath ?? null;
|
|
@@ -193,4 +269,4 @@ function firstDomain(intro) {
|
|
|
193
269
|
function ensureTrailingSlash(dir) {
|
|
194
270
|
return dir.endsWith("/") ? dir : `${dir}/`;
|
|
195
271
|
}
|
|
196
|
-
export { defaultIntrospection, generateHaproxyConfig, generateNginxConfig };
|
|
272
|
+
export { defaultIntrospection, generateHaproxyConfig, generateNginxConfig, upstreamH2cPort };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { constants } from "node:http2";
|
|
2
|
+
// nodefony/src/servers/http2Goaway.ts
|
|
3
|
+
/** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas la fermeture. */
|
|
4
|
+
const ANNOUNCE_CAP_MS = 1e3;
|
|
5
|
+
/** Dernier identifiant de flux possible (RFC 9113 §6.8) : « aucun flux n'est encore écarté ». */
|
|
6
|
+
const LAST_POSSIBLE_STREAM = 2 ** 31 - 1;
|
|
7
|
+
/**
|
|
8
|
+
* Annonce à UNE session HTTP/2 qu'elle va fermer, SANS écarter un flux déjà en route.
|
|
9
|
+
*
|
|
10
|
+
* Un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
|
|
11
|
+
* aller-retour : après elle, tout flux ouvert par le client avant l'annonce est
|
|
12
|
+
* arrivé, et le GOAWAY définitif (`session.close()`, dernier flux traité) peut
|
|
13
|
+
* suivre. RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last
|
|
14
|
+
* stream identifier set to 2^31-1 […] After allowing time for any in-flight
|
|
15
|
+
* stream creation (at least one round-trip time) ». Sans annonce, un flux croisé
|
|
16
|
+
* avec le GOAWAY est refusé alors qu'il aurait pu être servi.
|
|
17
|
+
*
|
|
18
|
+
* L'aller-retour se MESURE avant l'annonce : après un GOAWAY, un client Node ne
|
|
19
|
+
* répond plus aux PING (constaté sur Node 26.13).
|
|
20
|
+
*
|
|
21
|
+
* @param session - la session à annoncer, ni fermée ni détruite
|
|
22
|
+
* @param settle - appelé UNE fois : après l'attente, à la fermeture de la
|
|
23
|
+
* session, ou aussitôt si le PING ne part pas. Un pair muet ne l'appelle
|
|
24
|
+
* jamais — le délai maximal appartient à l'appelant.
|
|
25
|
+
*/
|
|
26
|
+
function announceSessionClose(session, settle) {
|
|
27
|
+
let settled = false;
|
|
28
|
+
let wait = null;
|
|
29
|
+
const done = () => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
if (wait !== null) clearTimeout(wait);
|
|
33
|
+
session.removeListener("close", done);
|
|
34
|
+
settle();
|
|
35
|
+
};
|
|
36
|
+
session.once("close", done);
|
|
37
|
+
if (!session.ping((error, rttMs) => {
|
|
38
|
+
if (error !== null || settled || session.closed) return done();
|
|
39
|
+
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
40
|
+
wait = setTimeout(done, Math.max(1, Math.ceil(rttMs)));
|
|
41
|
+
})) {
|
|
42
|
+
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
43
|
+
done();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ferme EN DOUCEUR une session HTTP/2 restée inactive au-delà du délai du serveur.
|
|
48
|
+
*
|
|
49
|
+
* RFC 9113 §9.1 : un serveur est « permitted to terminate idle connections »,
|
|
50
|
+
* et « SHOULD first send a GOAWAY ». La fermeture suit l'annonce en deux temps
|
|
51
|
+
* ({@link announceSessionClose}), puis `session.close()` : aucun nouveau flux,
|
|
52
|
+
* mais les flux en cours vont à leur terme — un flux long et muet (abonnement
|
|
53
|
+
* gRPC) n'est pas coupé, seuls les NOUVEAUX flux repartent sur une autre
|
|
54
|
+
* connexion, ce que tout client HTTP/2 fait sur GOAWAY.
|
|
55
|
+
*
|
|
56
|
+
* Le délai de la session est désarmé d'abord : Node (26.13) marque une session
|
|
57
|
+
* détruite AVANT de désarmer son minuteur, qui la retiendrait alors jusqu'à son
|
|
58
|
+
* échéance (nodejs/node#66885).
|
|
59
|
+
*
|
|
60
|
+
* @param session - la session inactive
|
|
61
|
+
* @param capMs - délai maximal d'attente de l'écho du PING (ms)
|
|
62
|
+
*/
|
|
63
|
+
function closeIdleSession(session, capMs = ANNOUNCE_CAP_MS) {
|
|
64
|
+
if (session.closed || session.destroyed) return;
|
|
65
|
+
session.setTimeout(0);
|
|
66
|
+
let finished = false;
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (finished) return;
|
|
69
|
+
finished = true;
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (!session.closed && !session.destroyed) session.close();
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(finish, capMs);
|
|
74
|
+
announceSessionClose(session, finish);
|
|
75
|
+
}
|
|
76
|
+
export { ANNOUNCE_CAP_MS, announceSessionClose, closeIdleSession };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeIdleSession } from "./http2Goaway.js";
|
|
1
2
|
// nodefony/src/servers/http2Limits.ts
|
|
2
3
|
/**
|
|
3
4
|
* Limites anti-DoS d'un serveur HTTP/2, lues dans la section `http2` du module :
|
|
@@ -18,29 +19,54 @@ function http2ServerLimits(limits) {
|
|
|
18
19
|
return options;
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Une session HTTP/2, et non une socket HTTP/1.1 : sur 5152 (`allowHTTP1`),
|
|
23
|
+
* l'événement `timeout` du serveur reçoit les deux. `node:http2` n'exporte pas
|
|
24
|
+
* la classe de session — elle se reconnaît à sa capacité (`goaway`).
|
|
25
|
+
*/
|
|
26
|
+
function isHttp2Session(target) {
|
|
27
|
+
return typeof target === "object" && target !== null && "goaway" in target;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ferme les sessions d'un serveur HTTP/2 — TLS (5152) comme h2c — restées
|
|
31
|
+
* inactives au-delà de `timeoutMs`, et contourne un défaut de Node qui garde
|
|
32
|
+
* chaque session fermée en vie jusqu'à l'échéance de ce délai.
|
|
24
33
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* **La fermeture est À NOUS.** Node arme chaque session par
|
|
35
|
+
* `session.setTimeout(server.timeout, sessionOnTimeout)` — un écouteur `once` —
|
|
36
|
+
* et `sessionOnTimeout` ne détruit la session que si le serveur n'a AUCUN
|
|
37
|
+
* écouteur `timeout` ; s'il en a un, il lui passe la main, UNE fois par session
|
|
38
|
+
* (`internal/http2/core`, Node 26.13). Le serveur 5152 en a toujours un
|
|
39
|
+
* (`routeSocketTimeout`, pour ses sockets HTTP/1.1), et ce délai doit prévenir
|
|
40
|
+
* l'application (`onTimeout`) : sans fermeture ici, une session inactive vivait
|
|
41
|
+
* jusqu'à ce que le CLIENT la ferme (#763). À l'échéance, la session est fermée
|
|
42
|
+
* en douceur — GOAWAY annoncé puis définitif, les flux en cours vont à leur
|
|
43
|
+
* terme ({@link closeIdleSession}, RFC 9113 §9.1 et §6.8).
|
|
30
44
|
*
|
|
31
|
-
* Le contournement
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
45
|
+
* **Le contournement** (Node 24, 26) : `closeSession` marque la session détruite
|
|
46
|
+
* PUIS appelle `session.setTimeout(0)`, qui sort aussitôt sur un objet détruit ;
|
|
47
|
+
* le minuteur retient alors la session (~2,4 Ko) pendant `timeoutMs`. Il est
|
|
48
|
+
* désarmé à la réception du GOAWAY, AVANT la destruction — c'est ainsi que
|
|
49
|
+
* ferment un proxy et un client Node, même par `destroy()` ; et
|
|
50
|
+
* `closeIdleSession` le désarme avant de fermer. Signalé à Node :
|
|
51
|
+
* https://github.com/nodejs/node/issues/66885 (corrigé par nodejs/node#66886,
|
|
52
|
+
* absent de la 26.13). Une coupure TCP sans GOAWAY garde la rétention, bornée
|
|
53
|
+
* au délai.
|
|
54
|
+
*
|
|
55
|
+
* Coût par requête : nul — un écouteur par serveur, et la fermeture n'agit qu'à
|
|
56
|
+
* l'échéance.
|
|
36
57
|
*
|
|
37
58
|
* @param server - le serveur HTTP/2.
|
|
38
59
|
* @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
|
|
39
|
-
* @param onTimeout - appelé à l'échéance (événement `timeout` du
|
|
60
|
+
* @param onTimeout - appelé à l'échéance d'une session (événement `timeout` du
|
|
61
|
+
* serveur), après le début de sa fermeture.
|
|
40
62
|
*/
|
|
41
63
|
function setHttp2SessionTimeout(server, timeoutMs, onTimeout) {
|
|
42
64
|
if (!timeoutMs) return;
|
|
43
|
-
server.setTimeout(timeoutMs,
|
|
65
|
+
server.setTimeout(timeoutMs, (target) => {
|
|
66
|
+
if (!isHttp2Session(target)) return;
|
|
67
|
+
closeIdleSession(target);
|
|
68
|
+
onTimeout();
|
|
69
|
+
});
|
|
44
70
|
server.on("session", (session) => {
|
|
45
71
|
session.once("goaway", () => {
|
|
46
72
|
session.setTimeout(0);
|