@nodefony/http 10.0.0-beta.5 → 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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- 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/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- 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 +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +57 -14
- 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/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- 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/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -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/ISecurity.d.ts +27 -2
- 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/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- 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/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/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/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- 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/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -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 };
|
|
@@ -31,6 +31,14 @@ var Profiler = class {
|
|
|
31
31
|
/** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
|
|
32
32
|
_seq = 0;
|
|
33
33
|
/**
|
|
34
|
+
* Démarrage du profileur, donc du processus (ms epoch, horloge du serveur) :
|
|
35
|
+
* aucune requête servie avant n'est connue. Rendu avec un profil absent, pour
|
|
36
|
+
* que le lecteur dise « le serveur a redémarré » plutôt que « évincé ».
|
|
37
|
+
*/
|
|
38
|
+
startedAt = Date.now();
|
|
39
|
+
/** Dernier vidage des anneaux (ms epoch), `null` s'il n'y en a jamais eu. */
|
|
40
|
+
_clearedAt = null;
|
|
41
|
+
/**
|
|
34
42
|
* @param cap - capacité de l'anneau de l'application.
|
|
35
43
|
* @param sideCap - capacité de chacun des anneaux de l'outillage et des
|
|
36
44
|
* assets relayés.
|
|
@@ -151,12 +159,17 @@ var Profiler = class {
|
|
|
151
159
|
const { app, tooling, asset } = this._lanes;
|
|
152
160
|
return app.size + tooling.size + asset.size;
|
|
153
161
|
}
|
|
154
|
-
/** Vide les anneaux. */
|
|
162
|
+
/** Vide les anneaux, et retient l'instant du vidage ({@link clearedAt}). */
|
|
155
163
|
clear() {
|
|
156
164
|
const { app, tooling, asset } = this._lanes;
|
|
157
165
|
app.clear();
|
|
158
166
|
tooling.clear();
|
|
159
167
|
asset.clear();
|
|
168
|
+
this._clearedAt = Date.now();
|
|
169
|
+
}
|
|
170
|
+
/** Dernier vidage des anneaux (ms epoch, horloge du serveur), `null` si aucun. */
|
|
171
|
+
get clearedAt() {
|
|
172
|
+
return this._clearedAt;
|
|
160
173
|
}
|
|
161
174
|
/**
|
|
162
175
|
* Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
|
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUnsupportedTransferCoding } from "./rules.js";
|
|
1
2
|
import http from "node:http";
|
|
2
3
|
import https from "node:https";
|
|
3
4
|
// nodefony/src/proxy/forward.ts
|
|
@@ -83,6 +84,71 @@ function viaLoops(via, pseudonym) {
|
|
|
83
84
|
for (const hop of hops) if (hop.trim().split(/\s+/)[1] === pseudonym) return true;
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
87
|
+
/** `1*DIGIT` (RFC 9110 §7.6.2) — compilé une fois. */
|
|
88
|
+
const DIGITS = /^[0-9]+$/;
|
|
89
|
+
/**
|
|
90
|
+
* `Max-Forwards` reçu, tel qu'un intermédiaire doit le lire (RFC 9110 §7.6.2) :
|
|
91
|
+
* sur `OPTIONS` et `TRACE` seulement — la même section permet de l'ignorer
|
|
92
|
+
* pour toute autre méthode, et il part alors tel quel.
|
|
93
|
+
*
|
|
94
|
+
* @param method - méthode de la requête
|
|
95
|
+
* @param value - en-tête `Max-Forwards` reçu
|
|
96
|
+
* @returns `null` s'il n'y a rien à faire (autre méthode, en-tête absent) ;
|
|
97
|
+
* `-1` si la valeur n'est pas `1*DIGIT` — elle ne se décrémente pas, l'en-tête
|
|
98
|
+
* n'est donc pas transmis ; sinon la valeur reçue (`0` : le proxy répond
|
|
99
|
+
* lui-même, sans joindre l'amont).
|
|
100
|
+
*/
|
|
101
|
+
function receivedMaxForwards(method, value) {
|
|
102
|
+
if (value === void 0 || method !== "OPTIONS" && method !== "TRACE") return null;
|
|
103
|
+
const raw = (Array.isArray(value) ? value[0] : value)?.trim() ?? "";
|
|
104
|
+
return DIGITS.test(raw) ? Number(raw) : -1;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* En-têtes qu'une réponse à `TRACE` ne renvoie pas : ils portent des secrets
|
|
108
|
+
* (RFC 9110 §9.3.8 : le destinataire final DEVRAIT exclure les champs
|
|
109
|
+
* susceptibles de contenir des données sensibles).
|
|
110
|
+
*/
|
|
111
|
+
const TRACE_SENSITIVE = /^(?:authorization|proxy-authorization|cookie)$|token|secret|session|api-?key|csrf/i;
|
|
112
|
+
/**
|
|
113
|
+
* Corps `message/http` d'une réponse à `TRACE` : la requête reçue, en-têtes
|
|
114
|
+
* sensibles et pseudo-en-têtes HTTP/2 retirés (RFC 9110 §9.3.8).
|
|
115
|
+
*/
|
|
116
|
+
function traceEcho(req) {
|
|
117
|
+
let echo = `TRACE ${req.url ?? "/"} HTTP/${req.httpVersion}\r\n`;
|
|
118
|
+
const authority = req.headers[":authority"];
|
|
119
|
+
if (req.headers.host === void 0 && typeof authority === "string") echo += `host: ${authority}\r\n`;
|
|
120
|
+
for (const name of Object.keys(req.headers)) {
|
|
121
|
+
if (name.charCodeAt(0) === 58 || TRACE_SENSITIVE.test(name)) continue;
|
|
122
|
+
echo += `${name}: ${flat(req.headers[name])}\r\n`;
|
|
123
|
+
}
|
|
124
|
+
return `${echo}\r\n`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Réponse du proxy en DESTINATAIRE FINAL, quand `Max-Forwards` est épuisé
|
|
128
|
+
* (RFC 9110 §7.6.2) : `TRACE` renvoie la requête reçue (§9.3.8), `OPTIONS`
|
|
129
|
+
* rend 200 avec `Allow` si le montage restreint ses méthodes, et
|
|
130
|
+
* `Content-Length: 0` (§9.3.7). L'amont n'est jamais joint.
|
|
131
|
+
*/
|
|
132
|
+
function answerAsFinalRecipient(req, out, mount) {
|
|
133
|
+
req.resume();
|
|
134
|
+
if (req.method === "TRACE") {
|
|
135
|
+
const body = traceEcho(req);
|
|
136
|
+
out.writeHead(200, {
|
|
137
|
+
"content-type": "message/http",
|
|
138
|
+
"content-length": Buffer.byteLength(body),
|
|
139
|
+
"cache-control": "no-store"
|
|
140
|
+
});
|
|
141
|
+
out.end(body);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const headers = {
|
|
145
|
+
"content-length": 0,
|
|
146
|
+
"cache-control": "no-store"
|
|
147
|
+
};
|
|
148
|
+
if (mount.methods !== void 0) headers.allow = mount.methods.join(", ");
|
|
149
|
+
out.writeHead(200, headers);
|
|
150
|
+
out.end();
|
|
151
|
+
}
|
|
86
152
|
/** Entrée `Via` de ce saut : `<version> <pseudonyme>` (RFC 9110 §7.6.3). */
|
|
87
153
|
function viaEntry(httpVersion, pseudonym) {
|
|
88
154
|
return `${httpVersion.startsWith("2") ? "2" : httpVersion || "1.1"} ${pseudonym}`;
|
|
@@ -98,10 +164,7 @@ function viaEntry(httpVersion, pseudonym) {
|
|
|
98
164
|
*/
|
|
99
165
|
function bodyFramingOf(req) {
|
|
100
166
|
const coding = req.headers["transfer-encoding"];
|
|
101
|
-
if (coding !== void 0)
|
|
102
|
-
if (coding === "chunked") return "chunked";
|
|
103
|
-
return coding.trim().toLowerCase() === "chunked" ? "chunked" : "unsupported";
|
|
104
|
-
}
|
|
167
|
+
if (coding !== void 0) return isUnsupportedTransferCoding(coding) ? "unsupported" : "chunked";
|
|
105
168
|
if (req.headers["content-length"] !== void 0) return "length";
|
|
106
169
|
const stream = req.stream;
|
|
107
170
|
if (stream !== void 0 && !stream.endAfterHeaders) return "chunked";
|
|
@@ -264,8 +327,9 @@ function armConnectTimeout(upstream, ms, onTimeout) {
|
|
|
264
327
|
* Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
|
|
265
328
|
*
|
|
266
329
|
* Le corps est transmis avec un cadrage cohérent ({@link bodyFramingOf}). Un
|
|
267
|
-
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont.
|
|
268
|
-
*
|
|
330
|
+
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Sur
|
|
331
|
+
* `OPTIONS` et `TRACE`, `Max-Forwards` est décrémenté ; à `0`, le proxy répond
|
|
332
|
+
* lui-même en destinataire final (RFC 9110 §7.6.2). Un client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
|
|
269
333
|
* connexion non établie à temps ou un amont muet rendent 504 — à condition
|
|
270
334
|
* que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
|
|
271
335
|
* statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
|
|
@@ -307,10 +371,21 @@ function forwardRequest(req, res, mount, target, from, relay) {
|
|
|
307
371
|
fail(501, NOT_IMPLEMENTED);
|
|
308
372
|
return;
|
|
309
373
|
}
|
|
374
|
+
const method = req.method ?? "GET";
|
|
375
|
+
const hops = receivedMaxForwards(method, req.headers["max-forwards"]);
|
|
376
|
+
if (hops === 0) {
|
|
377
|
+
answerAsFinalRecipient(req, out, mount);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
310
380
|
const options = requestOptions(target, mount, relay);
|
|
311
|
-
options.method =
|
|
381
|
+
options.method = method;
|
|
312
382
|
options.path = upstreamPath(req.url ?? "/", mount);
|
|
313
|
-
|
|
383
|
+
const headers = requestHeadersFor(req.headers, mount, target, from, relay.pseudonym, framing);
|
|
384
|
+
if (hops !== null) {
|
|
385
|
+
if (hops > 0) headers["max-forwards"] = String(Math.min(hops - 1, 10));
|
|
386
|
+
else delete headers["max-forwards"];
|
|
387
|
+
}
|
|
388
|
+
options.headers = headers;
|
|
314
389
|
if (mount.timeoutMs !== void 0) options.timeout = mount.timeoutMs;
|
|
315
390
|
const request = requestFn(target)(options);
|
|
316
391
|
upstream = request;
|
|
@@ -414,6 +489,31 @@ function refuseUpgrade(socket, status, reason, headers) {
|
|
|
414
489
|
}
|
|
415
490
|
socket.end(`HTTP/1.1 ${status} ${reason}\r\n${extra}Connection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
416
491
|
}
|
|
492
|
+
/** En-têtes du refus d'une version WebSocket inconnue — figés, partagés. */
|
|
493
|
+
const WS_VERSION_REFUSAL_HEADERS = Object.freeze({
|
|
494
|
+
Upgrade: "websocket",
|
|
495
|
+
"Sec-WebSocket-Version": "13"
|
|
496
|
+
});
|
|
497
|
+
/**
|
|
498
|
+
* Refuse un handshake WebSocket non conforme — la réponse qui convient à la
|
|
499
|
+
* raison rendue par `websocketHandshakeProblem`, seule implémentation pour le
|
|
500
|
+
* serveur WebSocket ET le proxy inverse.
|
|
501
|
+
*
|
|
502
|
+
* Une version inconnue reçoit `426` avec `Sec-WebSocket-Version: 13` : le
|
|
503
|
+
* client apprend vers quelle version se rabattre (RFC 6455 §4.2.2 point 6,
|
|
504
|
+
* §4.4), et le 426 porte l'`Upgrade` qu'exige RFC 9110 §15.5.22. Tout autre
|
|
505
|
+
* défaut de forme reçoit `400`.
|
|
506
|
+
*
|
|
507
|
+
* @param socket - socket du client, encore en attente de sa réponse
|
|
508
|
+
* @param problem - la raison rendue par `websocketHandshakeProblem`
|
|
509
|
+
*/
|
|
510
|
+
function refuseMalformedHandshake(socket, problem) {
|
|
511
|
+
if (problem === "Sec-WebSocket-Version ≠ 13") {
|
|
512
|
+
refuseUpgrade(socket, 426, "Upgrade Required", WS_VERSION_REFUSAL_HEADERS);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
refuseUpgrade(socket, 400, "Bad Request");
|
|
516
|
+
}
|
|
417
517
|
/** En-têtes d'une réponse d'amont REFUSANT l'upgrade, à ne pas recopier tels quels. */
|
|
418
518
|
const REFUSAL_SKIP = /* @__PURE__ */ new Set([
|
|
419
519
|
"transfer-encoding",
|
|
@@ -497,4 +597,4 @@ function forwardUpgrade(req, socket, head, mount, target, from, relay) {
|
|
|
497
597
|
});
|
|
498
598
|
upstream.end();
|
|
499
599
|
}
|
|
500
|
-
export { MAX_VIA_HOPS, bodyFramingOf, forwardRequest, forwardUpgrade, rawHead, refuseUpgrade, requestHeadersFor, responseHeadersFor, rewriteLocation, upstreamPath, viaLoops };
|
|
600
|
+
export { 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;
|
|
@@ -152,6 +228,12 @@ defaults
|
|
|
152
228
|
# \`timeout server\` ne s'applique plus au tunnel. Sans cette ligne, la valeur
|
|
153
229
|
# implicite coupe des sockets que le heartbeat gardait pourtant vivantes.
|
|
154
230
|
timeout tunnel ${idleSeconds}s
|
|
231
|
+
# Forwarded (RFC 7239) posé par haproxy lui-même (>= 2.8) : il met entre
|
|
232
|
+
# guillemets ce qui n'est pas un jeton — une IPv6 (\`for="[2001:db8::1]"\`),
|
|
233
|
+
# un \`host\` qui porte un port. Un \`set-header\` écrit à la main laissait
|
|
234
|
+
# \`for=2001:db8::1\`, valeur invalide (§6). \`proto\` suit \`ssl_fc\` : le
|
|
235
|
+
# scheme vu par le CLIENT. Vaut pour tous les backends.
|
|
236
|
+
option forwarded proto host for
|
|
155
237
|
|
|
156
238
|
frontend fe_nodefony
|
|
157
239
|
bind *:${intro.listen}
|
|
@@ -173,8 +255,6 @@ frontend fe_nodefony
|
|
|
173
255
|
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
|
|
174
256
|
http-request set-header X-Forwarded-Host %[req.hdr(host)]
|
|
175
257
|
http-request set-header X-Real-IP %[src]
|
|
176
|
-
http-request set-header Forwarded "for=%[src];proto=https;host=%[req.hdr(host)]" if { ssl_fc }
|
|
177
|
-
http-request set-header Forwarded "for=%[src];proto=http;host=%[req.hdr(host)]" unless { ssl_fc }
|
|
178
258
|
|
|
179
259
|
${h2cPort === null ? " default_backend be_nodefony\n" : " # h2c (servers.h2c) : HTTP/2 en clair vers le backend, plusieurs flux par\n # connexion. WebSocket reste en HTTP/1.1 sur le port HTTP : le port h2c\n # n'annonce pas le CONNECT étendu (RFC 8441).\n use_backend be_nodefony if { hdr(upgrade) -i websocket }\n default_backend be_nodefony_h2c\n"}
|
|
180
260
|
backend be_nodefony
|
|
@@ -189,4 +269,4 @@ function firstDomain(intro) {
|
|
|
189
269
|
function ensureTrailingSlash(dir) {
|
|
190
270
|
return dir.endsWith("/") ? dir : `${dir}/`;
|
|
191
271
|
}
|
|
192
|
-
export { defaultIntrospection, generateHaproxyConfig, generateNginxConfig };
|
|
272
|
+
export { defaultIntrospection, generateHaproxyConfig, generateNginxConfig, upstreamH2cPort };
|