@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/servers.md
CHANGED
|
@@ -51,7 +51,7 @@ flowchart TD
|
|
|
51
51
|
INIT -->|"servers.https ≠ false"| SS["server-https<br/>node:https ou node:http2 — 5152"]
|
|
52
52
|
SH --> WS["server-websocket<br/>ws:// sur 5151"]
|
|
53
53
|
SS --> WSS["server-websocket-secure<br/>wss:// sur 5152"]
|
|
54
|
-
SH --> HK["HttpKernel<br/>onHttpRequest /
|
|
54
|
+
SH --> HK["HttpKernel<br/>onHttpRequest / onWebsocketUpgrade"]
|
|
55
55
|
SS --> HK
|
|
56
56
|
WS --> HK
|
|
57
57
|
WSS --> HK
|
|
@@ -61,13 +61,13 @@ flowchart TD
|
|
|
61
61
|
Trois idées à retenir, et tout le reste en découle :
|
|
62
62
|
|
|
63
63
|
1. **Un serveur = un service DI.** Ils sont déclarés dans `@services([…])` du module
|
|
64
|
-
(`src/packages/@nodefony/http/index.ts
|
|
64
|
+
(`src/packages/@nodefony/http/index.ts#nodefony`) et instanciés par le conteneur, comme n'importe quel
|
|
65
65
|
service — donc introspectables, testables, remplaçables.
|
|
66
66
|
2. **Le WS hérite de son porteur.** `server-websocket` reçoit le `http.Server` déjà en écoute
|
|
67
|
-
(`Websocket.createServer()`, `server-websocket.ts
|
|
67
|
+
(`Websocket.createServer()`, `server-websocket.ts#Websocket.createServer`) ; `server-websocket-secure` reçoit le
|
|
68
68
|
`https.Server`. Couper HTTPS coupe donc le WSS, sans autre réglage.
|
|
69
69
|
3. **Les quatre convergent vers `HttpKernel`.** Les serveurs ne connaissent ni les routes, ni les
|
|
70
|
-
sessions, ni le firewall : ils branchent `onHttpRequest` / `
|
|
70
|
+
sessions, ni le firewall : ils branchent `onHttpRequest` / `onWebsocketUpgrade` et se taisent.
|
|
71
71
|
|
|
72
72
|
## 📖 Lexique
|
|
73
73
|
|
|
@@ -85,7 +85,7 @@ Trois idées à retenir, et tout le reste en découle :
|
|
|
85
85
|
| Drain | Vidange : laisser finir les requêtes en cours avant de fermer, plutôt que couper les sockets. |
|
|
86
86
|
| Liveness / Readiness | Sondes cloud : « le processus est-il vivant ? » / « peut-il recevoir du trafic ? ». |
|
|
87
87
|
| SIGTERM | Signal d'arrêt poli envoyé par l'orchestrateur (k8s, `docker stop`) avant le `SIGKILL`. |
|
|
88
|
-
| SAN | _Subject Alternative Name_ : la liste des noms/IP qu'un certificat couvre réellement (RFC
|
|
88
|
+
| SAN | _Subject Alternative Name_ : la liste des noms/IP qu'un certificat couvre réellement (RFC 9525). |
|
|
89
89
|
| mkcert | Outil de développement qui installe une autorité de certification locale **de confiance** sur la machine. |
|
|
90
90
|
| Reverse-proxy / edge | Le nginx / HAProxy / ingress placé devant l'application, souvent porteur du TLS. |
|
|
91
91
|
| `X-Forwarded-*` | En-têtes ajoutés par un proxy pour dire l'IP, l'hôte et le protocole d'origine du client. |
|
|
@@ -122,18 +122,18 @@ les probes.
|
|
|
122
122
|
|
|
123
123
|
**Un port TLS qui parle deux protocoles.** Quand `servers.https.protocol` vaut `"2.0"` (le défaut),
|
|
124
124
|
Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttps.createServerH2()`,
|
|
125
|
-
`server-https.ts
|
|
125
|
+
`server-https.ts#ServerHttps.createServerH2`) : les clients modernes négocient `h2` par ALPN, les autres restent en HTTP/1.1
|
|
126
126
|
**sur le même port**. Le protocole effectif est relu socket par socket pour taguer le contexte
|
|
127
|
-
(`server-https.ts
|
|
127
|
+
(`server-https.ts#ServerHttps.createServerH2`).
|
|
128
128
|
|
|
129
129
|
**Le WebSocket n'est jamais un citoyen de seconde zone.** Il est adossé au serveur HTTP porteur
|
|
130
|
-
(`server-websocket.ts
|
|
131
|
-
_est_ une requête HTTP (`HttpKernel.
|
|
130
|
+
(`server-websocket.ts#Websocket.createServer`), passe par le **même** rate-limit d'IP que les requêtes HTTP — un upgrade
|
|
131
|
+
_est_ une requête HTTP (`HttpKernel.onWebsocketUpgrade()`, `http-kernel.ts#HttpKernel.onWebsocketUpgrade`) —, hérite de la même
|
|
132
132
|
session et du même firewall, et se ferme avec le même soin qu'une réponse HTTP.
|
|
133
133
|
|
|
134
134
|
> [!NOTE]
|
|
135
135
|
> Le serveur HTTP/3 (QUIC) est **réservé, pas implémenté** : la clé `http3` existe dans le schéma,
|
|
136
|
-
> marquée `reserved` (`config.ts
|
|
136
|
+
> marquée `reserved` (`config.ts#httpConfigSchema`). Elle ne fait rien aujourd'hui.
|
|
137
137
|
|
|
138
138
|
## 🚀 Démarrage rapide
|
|
139
139
|
|
|
@@ -164,45 +164,45 @@ export default defineConfig((ctx) => ({
|
|
|
164
164
|
|
|
165
165
|
### 2. Le réglage — le comportement de chaque serveur
|
|
166
166
|
|
|
167
|
-
Le réglage appartient au **module
|
|
167
|
+
Le réglage appartient au **module** : il vit dans `nodefony/config/http.ts`, que le manifeste passe
|
|
168
|
+
à `use("@nodefony/http", …)`. Toutes les clés
|
|
168
169
|
ci-dessous sont facultatives : ce sont les défauts du schéma Zod, écrits ici pour les montrer.
|
|
169
170
|
|
|
170
171
|
```typescript
|
|
171
|
-
// nodefony
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
"
|
|
204
|
-
|
|
205
|
-
}));
|
|
172
|
+
// nodefony/config/http.ts — réglage transport, le fragment que
|
|
173
|
+
// `nodefony.config.ts` passe à `use("@nodefony/http", httpConfig())`
|
|
174
|
+
import type { IHttpConfigInput } from "@nodefony/http";
|
|
175
|
+
|
|
176
|
+
export const httpConfig = () =>
|
|
177
|
+
({
|
|
178
|
+
// Ne pas exposer l'identité du serveur (anti-empreinte, recommandé en prod).
|
|
179
|
+
headerServer: null,
|
|
180
|
+
// Barrière Host : le domaine canonique est toujours accepté, + ceux-ci.
|
|
181
|
+
trustedHosts: ["app.example.com"],
|
|
182
|
+
// Un seul reverse-proxy en amont ? alors et alors seulement, lui faire confiance.
|
|
183
|
+
trustProxy: false,
|
|
184
|
+
http: {
|
|
185
|
+
requestTimeout: 30_000, // anti slow-loris (réception complète)
|
|
186
|
+
keepAliveTimeout: 5_000, // réutilisation de la socket TCP
|
|
187
|
+
shutdownTimeout: 5_000, // drain avant destruction forcée
|
|
188
|
+
},
|
|
189
|
+
http2: {
|
|
190
|
+
maxConcurrentStreams: 100, // défense CVE-2023-44487 (Rapid Reset)
|
|
191
|
+
maxSessionMemory: 10, // Mo par session h2
|
|
192
|
+
},
|
|
193
|
+
websocket: {
|
|
194
|
+
maxPayload: 1024 * 1024, // 1 MiB → au-delà, close 1009
|
|
195
|
+
keepaliveInterval: 20_000, // ping toutes les 20 s
|
|
196
|
+
keepaliveGracePeriod: 10_000, // pong attendu sous 10 s, sinon zombie
|
|
197
|
+
},
|
|
198
|
+
// Probes cloud-native — actives par défaut.
|
|
199
|
+
health: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
livenessPath: "/livez",
|
|
202
|
+
readinessPath: "/readyz",
|
|
203
|
+
},
|
|
204
|
+
certificates: { strategy: "auto" }, // mkcert en dev, sinon auto-signé
|
|
205
|
+
}) satisfies IHttpConfigInput;
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
### 3. Le contrôleur qui répond sur les deux transports
|
|
@@ -225,8 +225,9 @@ class PingController extends Controller {
|
|
|
225
225
|
@Get("/")
|
|
226
226
|
async ping() {
|
|
227
227
|
return this.renderJson({
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
type: this.context?.type, // "http" | "websocket" — la famille
|
|
229
|
+
scheme: this.context?.scheme, // "http" | "https" — vu par le client
|
|
230
|
+
alpnProtocol: this.context?.alpnProtocol, // "http/1.1" | "h2" | "h2c"
|
|
230
231
|
});
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -249,22 +250,31 @@ export default PingController;
|
|
|
249
250
|
|
|
250
251
|
### 4. Ce qu'on observe au boot
|
|
251
252
|
|
|
252
|
-
Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts#Kernel.onReady`). En développement, le
|
|
254
|
+
bilan de démarrage (`renderStartupHuman`, `startupScreen.ts`) dit d'abord **où aller**,
|
|
255
|
+
puis **ce qui est à regarder**, puis les adresses d'écoute :
|
|
255
256
|
|
|
256
257
|
```text
|
|
257
|
-
✓ Prêt en 1.
|
|
258
|
+
✓ Prêt en 6,1 s — Nodefony 10.0.0 · development
|
|
259
|
+
|
|
260
|
+
OUVRIR
|
|
261
|
+
➜ Application https://localhost:5152/
|
|
262
|
+
➜ Studio https://localhost:5152/nodefony
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
➜ WS ws://127.0.0.1:5151
|
|
263
|
-
➜ WSS wss://127.0.0.1:5152
|
|
264
|
+
ÉTAT
|
|
265
|
+
Mode development · rechargement auto
|
|
266
|
+
Écoute HTTP + WS 127.0.0.1:5151 · HTTP/2 + WSS 127.0.0.1:5152
|
|
264
267
|
```
|
|
265
268
|
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
L'adresse à **ouvrir** n'est pas l'adresse de **liaison** : un serveur lié à `127.0.0.1` ou à
|
|
270
|
+
`0.0.0.0` s'ouvre sur `localhost` (`openableHost`, `bootReport.ts#openableHost`) — les passkeys refusent
|
|
271
|
+
une IP comme domaine, et un cookie posé sous un nom ne part pas sous l'autre. Hors d'un
|
|
272
|
+
terminal, le même bilan sort ligne à ligne (`open.app: https://localhost:5152/`,
|
|
273
|
+
`listen.https: 127.0.0.1:5152`) ; pour une machine, `nodefony status --json` le rend en entier.
|
|
274
|
+
|
|
275
|
+
Hors écran animé (production, CI, `--debug`, sortie non-terminal), les bannières par serveur
|
|
276
|
+
sortent aussi (`ServerHttp.showBanner()`, `server-http.ts#ServerHttp.showBanner`, appelées par le kernel —
|
|
277
|
+
`Kernel.ts#Kernel.onReady`) :
|
|
268
278
|
|
|
269
279
|
```text
|
|
270
280
|
Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
|
|
@@ -275,15 +285,15 @@ Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
|
|
|
275
285
|
```bash
|
|
276
286
|
# HTTP/1.1 en clair
|
|
277
287
|
curl -s http://127.0.0.1:5151/ping
|
|
278
|
-
# {"scheme":"http","
|
|
288
|
+
# {"type":"http","scheme":"http","alpnProtocol":"http/1.1"}
|
|
279
289
|
|
|
280
290
|
# HTTP/2 sur le port TLS (-k : le certificat de dev n'est pas dans le trust store)
|
|
281
291
|
curl -sk --http2 https://127.0.0.1:5152/ping
|
|
282
|
-
# {"scheme":"https","
|
|
292
|
+
# {"type":"http","scheme":"https","alpnProtocol":"h2"}
|
|
283
293
|
|
|
284
294
|
# Le même port TLS sert encore HTTP/1.1 (allowHTTP1) — repli négocié par ALPN
|
|
285
295
|
curl -sk --http1.1 https://127.0.0.1:5152/ping
|
|
286
|
-
# {"scheme":"https","
|
|
296
|
+
# {"type":"http","scheme":"https","alpnProtocol":"http/1.1"}
|
|
287
297
|
|
|
288
298
|
# Probes cloud-native, servies par les DEUX serveurs
|
|
289
299
|
curl -si http://127.0.0.1:5151/livez | head -1 # HTTP/1.1 200 OK
|
|
@@ -296,26 +306,36 @@ C'est le déploiement **nominal** en Kubernetes : l'ingress porte le certificat,
|
|
|
296
306
|
`https: false` désactive HTTPS **et** le WSS qui en hérite, et supprime au passage toute génération de
|
|
297
307
|
certificat au boot.
|
|
298
308
|
|
|
299
|
-
```typescript
|
|
309
|
+
```typescript ignore
|
|
300
310
|
// nodefony.config.ts — pod k8s derrière un ingress qui termine le TLS
|
|
311
|
+
import { defineConfig, use } from "nodefony";
|
|
312
|
+
import { httpConfig } from "./nodefony/config/http";
|
|
313
|
+
|
|
301
314
|
export default defineConfig((ctx) => ({
|
|
302
315
|
domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1",
|
|
303
316
|
servers: {
|
|
304
317
|
https: false, // un seul port exposé : 5151
|
|
305
318
|
},
|
|
306
|
-
modules: [
|
|
307
|
-
use("@nodefony/http", {
|
|
308
|
-
// L'ingress est l'unique point d'entrée → ses X-Forwarded-* sont fiables.
|
|
309
|
-
trustProxy: ctx.isProd ? "uniquelocal" : false,
|
|
310
|
-
// L'ingress filtre déjà le Host ; sinon lister les vhosts servis.
|
|
311
|
-
trustedHosts: ["app.example.com"],
|
|
312
|
-
}),
|
|
313
|
-
"@nodefony/framework",
|
|
314
|
-
],
|
|
319
|
+
modules: [use("@nodefony/http", httpConfig(ctx)), "@nodefony/framework"],
|
|
315
320
|
}));
|
|
316
321
|
```
|
|
317
322
|
|
|
318
|
-
|
|
323
|
+
```typescript ignore
|
|
324
|
+
// nodefony/config/http.ts — le fragment que le manifeste ci-dessus appelle
|
|
325
|
+
import type { ConfigContext } from "nodefony";
|
|
326
|
+
import type { IHttpConfigInput } from "@nodefony/http";
|
|
327
|
+
import type { env } from "../../env";
|
|
328
|
+
|
|
329
|
+
export const httpConfig = (ctx: ConfigContext<typeof env>) =>
|
|
330
|
+
({
|
|
331
|
+
// L'ingress est l'unique point d'entrée → ses X-Forwarded-* sont fiables.
|
|
332
|
+
trustProxy: ctx.isProd ? "uniquelocal" : false,
|
|
333
|
+
// L'ingress filtre déjà le Host ; sinon lister les vhosts servis.
|
|
334
|
+
trustedHosts: ["app.example.com"],
|
|
335
|
+
}) satisfies IHttpConfigInput;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## 🔌 Les serveurs — quatre par défaut, h2c en option
|
|
319
339
|
|
|
320
340
|
Choisir en cinq secondes :
|
|
321
341
|
|
|
@@ -325,65 +345,142 @@ Choisir en cinq secondes :
|
|
|
325
345
|
| `server-https` | `https://` — 5152 | `servers.https !== false` | TLS : HTTP/2 (défaut) ou HTTP/1.1. |
|
|
326
346
|
| `server-websocket` | `ws://` — 5151 | `server-http` actif | WebSocket adossé au serveur HTTP. |
|
|
327
347
|
| `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
|
|
348
|
+
| `server-h2c` | h2c — 5153 | `servers.h2c.enabled` | HTTP/2 en clair, pour un proxy inverse (option). |
|
|
328
349
|
| `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
|
|
329
350
|
|
|
330
|
-
L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts
|
|
331
|
-
consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts
|
|
332
|
-
Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
|
|
351
|
+
L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts#HttpKernel.initServers`) : chaque serveur est
|
|
352
|
+
consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts#HttpKernel.initServers`).
|
|
353
|
+
Les serveurs WebSocket ne sont montés que si leur porteur l'a été. `server-h2c` n'a pas de WebSocket
|
|
354
|
+
adossé (voir sa section).
|
|
333
355
|
|
|
334
356
|
### `server-http` — HTTP/1.1 en clair
|
|
335
357
|
|
|
336
358
|
Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.createServer()`
|
|
337
|
-
(`server-http.ts
|
|
359
|
+
(`server-http.ts#ServerHttp.createServer`) crée le `http.Server`, applique les réglages de transport, branche
|
|
338
360
|
`onHttpRequest`, puis écoute selon la politique de port.
|
|
339
361
|
|
|
340
362
|
- **Actif** si `servers.http` n'est pas `false` — la décision est prise dès le constructeur
|
|
341
|
-
(`server-http.ts
|
|
342
|
-
`server-http.ts
|
|
363
|
+
(`server-http.ts#ServerHttp.constructor`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
|
|
364
|
+
`server-http.ts#ServerHttp.setPort`).
|
|
343
365
|
- **Réglages appliqués** : `requestTimeout` (anti slow-loris), `maxHeadersCount`, `timeout` de socket
|
|
344
366
|
(qui émet un événement `onTimeout`), `keepAliveTimeout`.
|
|
345
367
|
- **Erreurs de protocole** : l'événement `clientError` est traité explicitement
|
|
346
|
-
(`server-http.ts
|
|
368
|
+
(`server-http.ts#ServerHttp.createServer`) — voir la section Résilience, c'est un piège Node à part entière.
|
|
347
369
|
|
|
348
370
|
### `server-https` — TLS, et HTTP/2 par défaut
|
|
349
371
|
|
|
350
|
-
Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts
|
|
372
|
+
Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts#ServerHttps.protocol`) :
|
|
351
373
|
|
|
352
374
|
- **`"2.0"` (défaut)** → `http2.createSecureServer` avec `allowHTTP1: true`
|
|
353
|
-
(`ServerHttps.createServerH2()`, `server-https.ts
|
|
375
|
+
(`ServerHttps.createServerH2()`, `server-https.ts#ServerHttps.createServerH2`). Les bornes anti-DoS HTTP/2 ne sont posées
|
|
354
376
|
**que si elles sont configurées**, pour ne pas écraser les défauts de Node
|
|
355
|
-
(`maxSessionMemory`, `server-https.ts
|
|
377
|
+
(`maxSessionMemory`, `server-https.ts#ServerHttps.createServerH2`).
|
|
356
378
|
Les erreurs de session et de flux sont journalisées sans tuer le serveur
|
|
357
|
-
(`sessionError`, `server-https.ts
|
|
379
|
+
(`sessionError`, `server-https.ts#ServerHttps.listenWithPolicy`).
|
|
358
380
|
- **`"1.1"`** → `https.createServer` classique.
|
|
359
381
|
|
|
360
382
|
Le certificat vient du service `certificates`, lu au moment de la création via
|
|
361
|
-
`serviceCerticats` (`server-https.ts
|
|
383
|
+
`serviceCerticats` (`server-https.ts#ServerHttps.createServer`) — voir la section dédiée.
|
|
362
384
|
|
|
363
385
|
> [!IMPORTANT]
|
|
364
|
-
> Sur la branche HTTP/2, le protocole **effectif**
|
|
365
|
-
>
|
|
366
|
-
>
|
|
386
|
+
> Sur la branche HTTP/2, le protocole **effectif** se constate par requête (`transport.ts#alpnProtocolOf`) :
|
|
387
|
+
> un client `h2` produit un contexte `alpnProtocol: "h2"`, un client HTTP/1.1 sur le même port
|
|
388
|
+
> `alpnProtocol: "http/1.1"` — les deux avec `type: "http"` et `serverType: "https"`. C'est ce qui rend
|
|
389
|
+
> le port TLS universel.
|
|
390
|
+
|
|
391
|
+
### `server-h2c` — HTTP/2 en clair, pour un proxy inverse
|
|
392
|
+
|
|
393
|
+
Un proxy inverse qui termine le TLS (Envoy et Istio, HAProxy, Traefik) peut parler HTTP/2 **en clair** à
|
|
394
|
+
ses amonts : c'est h2c, en connaissance préalable (RFC 9113 §3.3) — le client envoie directement la préface
|
|
395
|
+
HTTP/2, sans négociation. `ServerH2c.createServer()` (`server-h2c.ts#ServerH2c.createServer`) le sert sur un
|
|
396
|
+
port **dédié**, désactivé par défaut :
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// nodefony.config.ts
|
|
400
|
+
servers: { h2c: { enabled: true } }, // port 5153 par défaut — ou NF__APP__SERVERS__H2C__ENABLED=true
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Il emprunte le chemin HTTP/2 du port TLS — même pipeline, mêmes limites anti-DoS
|
|
404
|
+
(`http2Limits.ts#http2ServerLimits`), même arrêt par GOAWAY — sans TLS : le contexte porte
|
|
405
|
+
`alpnProtocol: "h2c"`, `serverType: "http"`, et son `scheme` vient du transport ou d'un proxy de confiance
|
|
406
|
+
(`trustProxy`), **jamais** du pseudo-en-tête `:scheme`, que le client écrit lui-même.
|
|
407
|
+
|
|
408
|
+
**Ce qu'il ne fait pas, délibérément :**
|
|
409
|
+
|
|
410
|
+
- **HTTP/1.1** : un client HTTP/1.1 échoue sur la préface et la session est fermée. Les sondes de santé et
|
|
411
|
+
les clients HTTP/1.1 restent sur le port HTTP — d'où l'avertissement au boot si `servers.http` vaut
|
|
412
|
+
`false` alors que h2c est actif.
|
|
413
|
+
- **WebSocket** : WebSocket sur HTTP/2 passe par le CONNECT étendu (RFC 8441), que ce serveur ne sert pas.
|
|
414
|
+
Le proxy route `Upgrade: websocket` vers le port HTTP.
|
|
415
|
+
- **La bascule `Upgrade: h2c`** depuis HTTP/1.1 : retirée par RFC 9113 §3.1 (contrebande h2c). Le port
|
|
416
|
+
HTTP la refuse — 404 sur un chemin sans route WebSocket, 400 sinon — et ne répond jamais `101`.
|
|
417
|
+
- **Partager un port** : `servers.h2c.port` égal à `servers.http.port` ou `servers.https.port` est refusé
|
|
418
|
+
au boot (`portBinder.ts#h2cPortConflict`).
|
|
419
|
+
|
|
420
|
+
#### Faut-il l'activer ? — h2c n'est pas « plus rapide » par principe
|
|
421
|
+
|
|
422
|
+
Le gain classique de HTTP/2 est le multiplexage : plusieurs requêtes sur une connexion, sans blocage en tête
|
|
423
|
+
de file. Il sert surtout un **navigateur**, limité à quelques connexions sur un réseau lent. Entre un proxy et
|
|
424
|
+
un pod, sur un réseau local, le proxy garde déjà un **pool** de connexions HTTP/1.1 ouvertes qui masque ce
|
|
425
|
+
blocage ; et côté Node, HTTP/2 ajoute du travail par requête (trames, compression HPACK des en-têtes,
|
|
426
|
+
contrôle de flux) que le parseur HTTP/1.1 n'a pas. Sur de petites requêtes, un serveur Node en HTTP/2 sert
|
|
427
|
+
souvent **moins** de requêtes par seconde qu'en HTTP/1.1. Le choix se fait sur la **forme du trafic** :
|
|
428
|
+
|
|
429
|
+
| Forme du trafic | h2c ? | Pourquoi |
|
|
430
|
+
| -------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
|
+
| gRPC | **requis** | gRPC n'existe qu'en HTTP/2. |
|
|
432
|
+
| Beaucoup de flux longs (SSE) par pod | **oui** | En HTTP/1.1, chaque flux occupe une connexion du proxy ; en h2c, ils partagent quelques connexions. |
|
|
433
|
+
| En-têtes lourds et répétés (cookies, jetons) | probable | HPACK les compresse d'une requête à l'autre. |
|
|
434
|
+
| REST classique, petites réponses | **mesurer** | Souvent neutre, parfois perdant. |
|
|
435
|
+
| WebSocket | **non** | Reste sur le port HTTP, en HTTP/1.1. |
|
|
436
|
+
| Plusieurs workers (`nodefony cluster -w N`) | **attention** | Le cluster Node répartit des **connexions**, pas des requêtes : avec les rares connexions h2c d'un proxy, un seul worker peut tout recevoir. Un process par pod n'a pas ce biais. |
|
|
437
|
+
|
|
438
|
+
**Ce que coûte h2c, mesuré** — production en un seul process, même route triviale, même instrument
|
|
439
|
+
(`h2load`), médiane de paires alternées ; débit de h2c rapporté à HTTP/1.1 sur la même machine, et le même
|
|
440
|
+
écart pour Node seul, sans framework :
|
|
441
|
+
|
|
442
|
+
| Forme (`h2load`) | Node seul | Nodefony |
|
|
443
|
+
| ----------------------------------------------- | --------- | -------- |
|
|
444
|
+
| 64 connexions × 1 flux — forme égale à HTTP/1.1 | −60 % | −48 % |
|
|
445
|
+
| 4 connexions × 16 flux — la forme d'un proxy | −14 % | −18 % |
|
|
446
|
+
|
|
447
|
+
L'écart est d'abord celui de HTTP/2 dans Node : à forme égale, Node seul perd davantage que Nodefony, dont
|
|
448
|
+
le travail par requête dilue le coût du protocole. En forme proxy, le multiplexage regagne presque tout, et
|
|
449
|
+
Nodefony reste dans la même zone. Sur une route triviale, h2c ne fait donc pas gagner de débit ; il se choisit
|
|
450
|
+
pour ce que le tableau ci-dessus nomme — gRPC, flux longs, en-têtes lourds.
|
|
451
|
+
|
|
452
|
+
**Mesurer avant de décider** : même pod, même route, en ne changeant que le protocole amont du proxy.
|
|
453
|
+
`wrk` ne parle que HTTP/1.1 ; `h2load` (nghttp2) parle les deux, ce qui garde le même instrument des deux
|
|
454
|
+
côtés. Comparer le débit, le p99 et le **CPU par requête** — côté pod **et** côté proxy, qui paie aussi
|
|
455
|
+
HPACK — en paires alternées, sur la médiane de plusieurs runs.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
# vérifier le port : la préface HTTP/2 directe, sans TLS
|
|
459
|
+
curl --http2-prior-knowledge http://127.0.0.1:5153/readyz
|
|
460
|
+
# même route, même instrument, deux protocoles
|
|
461
|
+
h2load --h1 -n 100000 -c 64 http://127.0.0.1:5151/readyz # HTTP/1.1
|
|
462
|
+
h2load -n 100000 -c 64 http://127.0.0.1:5153/readyz # h2c
|
|
463
|
+
```
|
|
367
464
|
|
|
368
465
|
### `server-websocket` — WebSocket en clair
|
|
369
466
|
|
|
370
467
|
Il ne crée **aucun** socket : il reçoit le `http.Server` déjà en écoute et s'y greffe
|
|
371
|
-
(`server-websocket.ts
|
|
468
|
+
(`server-websocket.ts#Websocket.server`), puis relit `address()` pour connaître son port — il suit donc
|
|
372
469
|
automatiquement un éventuel décalage de port.
|
|
373
470
|
|
|
374
471
|
- **Options transmises à `ws`** telles quelles (compression, validation UTF-8, `maxPayload`…), avec deux
|
|
375
472
|
réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
|
|
376
|
-
le battement de cœur (`server-websocket.ts
|
|
377
|
-
- **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts
|
|
378
|
-
(`trackPong()`, `
|
|
473
|
+
le battement de cœur (`server-websocket.ts#Websocket.createServer`).
|
|
474
|
+
- **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts#Websocket.createServer`) et par connexion
|
|
475
|
+
(`trackPong()`, `http-kernel.ts#HttpKernel.handleWebsocket`, juste après l'acceptation du handshake).
|
|
379
476
|
- **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
|
|
380
|
-
(`prependOnceListener`, `server-websocket.ts
|
|
477
|
+
(`prependOnceListener`, `server-websocket.ts#Websocket.createServer`) — l'ordre
|
|
381
478
|
compte, voir la section Arrêt gracieux.
|
|
382
479
|
|
|
383
480
|
### `server-websocket-secure` — WebSocket sur TLS
|
|
384
481
|
|
|
385
482
|
Même code, une différence qui a son importance : il lit sa **propre** section de configuration,
|
|
386
|
-
`websocketSecure`, et non `websocket` (`server-websocket-secure.ts
|
|
483
|
+
`websocketSecure`, et non `websocket` (`server-websocket-secure.ts#WebsocketSecure.constructor`). Régler `websocket.maxPayload`
|
|
387
484
|
ne change donc rien au WSS ; les deux sections ont la même forme et les mêmes défauts.
|
|
388
485
|
|
|
389
486
|
### `server-static` — les fichiers, en repli
|
|
@@ -397,10 +494,10 @@ pour autant supprimer les montages programmatiques.
|
|
|
397
494
|
|
|
398
495
|
C'est la distinction la plus utile de cette page, et celle qu'on rate le plus souvent.
|
|
399
496
|
|
|
400
|
-
| Question | Où ça se règle | Source
|
|
401
|
-
| ----------------------------------------- | ------------------------------ |
|
|
402
|
-
| **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts
|
|
403
|
-
| **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts
|
|
497
|
+
| Question | Où ça se règle | Source |
|
|
498
|
+
| ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------- |
|
|
499
|
+
| **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts#serversSchema`) |
|
|
500
|
+
| **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts#httpConfigSchema`) |
|
|
404
501
|
|
|
405
502
|
Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
|
|
406
503
|
la CI et le cluster) ; le **réglage** est une propriété de l'application.
|
|
@@ -413,16 +510,17 @@ la CI et le cluster) ; le **réglage** est une propriété de l'application.
|
|
|
413
510
|
| `servers.http` | `{ port }` \| `false` | `{ port: 5151 }` | Serveur en clair. `false` = TLS-only (le WS tombe avec). |
|
|
414
511
|
| `servers.https` | `{ port, protocol }` \| `false` | `{ port: 5152, protocol: "2.0" }` | Serveur TLS. `false` = nominal cloud-native (le WSS tombe). |
|
|
415
512
|
| `servers.https.protocol` | `"1.1"` \| `"2.0"` | `"2.0"` | HTTP/2 (h2) avec repli HTTP/1.1 par ALPN, ou HTTP/1.1 seul. |
|
|
513
|
+
| `servers.h2c` | `{ enabled, port }` | `{ enabled: false, port: 5153 }` | HTTP/2 en clair pour un proxy inverse — section `server-h2c`. |
|
|
416
514
|
| `servers.statics` | bool | `true` | Monte le service de fichiers statiques. |
|
|
417
515
|
| `servers.portPolicy` | `"auto"` \| `"strict"` | `auto` en dev, `strict` en prod **et** test | Que faire si le port est occupé (section suivante). |
|
|
418
516
|
| `servers.portRetryAttempts` | int ≥ 0 | `20` | En `auto`, nombre de ports essayés après le port désiré. |
|
|
419
517
|
|
|
420
|
-
Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaults.ts
|
|
518
|
+
Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaults.ts#defaultAppConfig`).
|
|
421
519
|
|
|
422
520
|
### Niveau 2 — le transport HTTP / HTTPS
|
|
423
521
|
|
|
424
|
-
Table dérivée de `httpServerSchema` (`config.ts
|
|
425
|
-
ajoute une (`httpsServerSchema`, `config.ts
|
|
522
|
+
Table dérivée de `httpServerSchema` (`config.ts#httpServerSchema`) ; la section `https` reprend les mêmes clés et en
|
|
523
|
+
ajoute une (`httpsServerSchema`, `config.ts#httpsServerSchema`).
|
|
426
524
|
|
|
427
525
|
| Option | Type | Défaut | Effet |
|
|
428
526
|
| ---------------------------- | ----- | ------- | -------------------------------------------------------------------------------- |
|
|
@@ -441,8 +539,8 @@ quelle à Node. C'est délibéré — un schéma strict effacerait silencieuseme
|
|
|
441
539
|
|
|
442
540
|
### Niveau 2 — HTTP/2
|
|
443
541
|
|
|
444
|
-
Depuis `http2Schema` (`config.ts
|
|
445
|
-
(`maxSessionMemory`, `server-https.ts
|
|
542
|
+
Depuis `http2Schema` (`config.ts#http2Schema`), appliqué seulement si défini
|
|
543
|
+
(`maxSessionMemory`, `server-https.ts#ServerHttps.createServerH2`).
|
|
446
544
|
|
|
447
545
|
| Option | Type | Défaut | Effet |
|
|
448
546
|
| ---------------------- | ---- | ------ | ----------------------------------------------------------------------------- |
|
|
@@ -451,22 +549,22 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
|
|
|
451
549
|
|
|
452
550
|
### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
|
|
453
551
|
|
|
454
|
-
Depuis `websocketSchema` (`config.ts
|
|
455
|
-
lit `websocketSecure` (`config.ts
|
|
456
|
-
|
|
457
|
-
| Option | Type | Défaut | Effet
|
|
458
|
-
| ------------------------ | ------------------- | ------- |
|
|
459
|
-
| `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies.
|
|
460
|
-
| `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture.
|
|
461
|
-
| `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket.
|
|
462
|
-
| `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts
|
|
463
|
-
| `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts
|
|
464
|
-
| `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_.
|
|
465
|
-
| `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`.
|
|
466
|
-
| `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`.
|
|
467
|
-
| `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité).
|
|
468
|
-
| `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM.
|
|
469
|
-
| `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013).
|
|
552
|
+
Depuis `websocketSchema` (`config.ts#websocketSchema`). Les deux sections partagent la forme et les défauts ; le WSS
|
|
553
|
+
lit `websocketSecure` (`config.ts#httpConfigSchema`).
|
|
554
|
+
|
|
555
|
+
| Option | Type | Défaut | Effet |
|
|
556
|
+
| ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------------- |
|
|
557
|
+
| `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
|
|
558
|
+
| `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
|
|
559
|
+
| `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket. |
|
|
560
|
+
| `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts#websocketSchema`). |
|
|
561
|
+
| `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts#websocketSchema`). |
|
|
562
|
+
| `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
|
|
563
|
+
| `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
|
|
564
|
+
| `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
|
|
565
|
+
| `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité). |
|
|
566
|
+
| `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM. |
|
|
567
|
+
| `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013). |
|
|
470
568
|
|
|
471
569
|
> [!WARNING]
|
|
472
570
|
> `keepalive*`, `closeTimeout`, `maxBackpressure` et `backpressurePolicy` sont des **réglages
|
|
@@ -484,7 +582,7 @@ panne, c'est une nuisance.
|
|
|
484
582
|
|
|
485
583
|
### La règle
|
|
486
584
|
|
|
487
|
-
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts
|
|
585
|
+
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts#resolvePortPolicy` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
|
|
488
586
|
toujours :
|
|
489
587
|
|
|
490
588
|
| Environnement | Défaut | Pourquoi |
|
|
@@ -501,7 +599,7 @@ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
|
|
|
501
599
|
servers.portPolicy = "strict".
|
|
502
600
|
```
|
|
503
601
|
|
|
504
|
-
Le décalage est **toujours annoncé** (`server-http.ts
|
|
602
|
+
Le décalage est **toujours annoncé** (`server-http.ts#ServerHttp.createServer`) : jamais de dégradation silencieuse. Trois
|
|
505
603
|
détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
|
|
506
604
|
|
|
507
605
|
1. **Le repli se décide au `listen()`, qui est atomique.** Une seule sonde le précède, sur le port
|
|
@@ -509,24 +607,24 @@ détails d'implémentation valent d'être connus, parce qu'ils expliquent des co
|
|
|
509
607
|
même à un `0.0.0.0` (`detectPortConflict`). Elle est **sautée dans un worker de cluster**
|
|
510
608
|
(`cluster.isWorker`) : le port y est partagé par le maître, et le voisin qui répond est un frère,
|
|
511
609
|
pas un intrus. La course résiduelle retombe sur `EADDRINUSE` (`bindWithFallback()`,
|
|
512
|
-
`portBinder.ts
|
|
610
|
+
`portBinder.ts#bindWithFallback`).
|
|
513
611
|
2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
|
|
514
612
|
ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
|
|
515
|
-
sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts
|
|
613
|
+
sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts#buildBindPlan`, réservation `portBinder.ts#buildBindPlan`).
|
|
516
614
|
3. **Le gestionnaire d'erreur durable est posé APRÈS le bind.** Attaché avant, il verrait passer les
|
|
517
615
|
`EADDRINUSE` de repli et terminerait le kernel en croyant à une panne
|
|
518
|
-
(`ServerHttp.attachErrorHandler()`, `server-http.ts
|
|
616
|
+
(`ServerHttp.attachErrorHandler()`, `server-http.ts#ServerHttp.attachErrorHandler`).
|
|
519
617
|
|
|
520
618
|
Quand le bind échoue pour de bon — `strict`, ou tous les replis épuisés, ou une erreur qui n'est pas un
|
|
521
619
|
conflit de port — c'est **fatal** : message explicite puis terminaison du processus
|
|
522
|
-
(`ServerHttp.reportBindError()`, `server-http.ts
|
|
620
|
+
(`ServerHttp.reportBindError()`, `server-http.ts#ServerHttp.reportBindError`). Un serveur qui n'écoute pas ne doit jamais
|
|
523
621
|
laisser un processus se croire démarré.
|
|
524
622
|
|
|
525
623
|
### Le corollaire : les ports effectifs sont publiés
|
|
526
624
|
|
|
527
625
|
Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
|
|
528
626
|
convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
|
|
529
|
-
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts
|
|
627
|
+
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts#HttpKernel.publishRuntimePorts`) écrit donc la
|
|
530
628
|
topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
|
|
531
629
|
environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
|
|
532
630
|
aussi de la convention,
|
|
@@ -540,12 +638,12 @@ serveur qui, lui, écoute très bien.
|
|
|
540
638
|
**Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
|
|
541
639
|
n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
|
|
542
640
|
ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
|
|
543
|
-
(`Certificate.resolveStrategy()`, `certificates.ts
|
|
641
|
+
(`Certificate.resolveStrategy()`, `certificates.ts#Certificate.resolveStrategy`).
|
|
544
642
|
|
|
545
643
|
### Les quatre stratégies
|
|
546
644
|
|
|
547
|
-
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts
|
|
548
|
-
`Certificate.resolveStrategy()` (`certificates.ts
|
|
645
|
+
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts#certificatesSchema`), résolues par
|
|
646
|
+
`Certificate.resolveStrategy()` (`certificates.ts#Certificate.resolveStrategy`).
|
|
549
647
|
|
|
550
648
|
| Stratégie | Quand l'utiliser | Ce qui se passe |
|
|
551
649
|
| --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
|
|
@@ -555,51 +653,94 @@ Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), r
|
|
|
555
653
|
| `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé (`node:crypto`), non trusté. |
|
|
556
654
|
|
|
557
655
|
```typescript
|
|
558
|
-
// nodefony
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
],
|
|
572
|
-
}));
|
|
656
|
+
// nodefony/config/http.ts — production : certificat fourni, jamais généré
|
|
657
|
+
import type { IHttpConfigInput } from "@nodefony/http";
|
|
658
|
+
|
|
659
|
+
export const httpConfig = () =>
|
|
660
|
+
({
|
|
661
|
+
certificates: {
|
|
662
|
+
strategy: "explicit",
|
|
663
|
+
key: "/etc/tls/privkey.pem",
|
|
664
|
+
cert: "/etc/tls/fullchain.pem",
|
|
665
|
+
ca: "/etc/tls/chain.pem",
|
|
666
|
+
},
|
|
667
|
+
https: { rejectUnauthorized: true },
|
|
668
|
+
}) satisfies IHttpConfigInput;
|
|
573
669
|
```
|
|
574
670
|
|
|
575
671
|
> [!TIP]
|
|
576
672
|
> Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
|
|
577
673
|
> et au WSS) : `brew install mkcert nss && mkcert -install`. Nodefony le détecte tout seul, sinon il
|
|
578
|
-
> l'annonce et retombe sur l'auto-signé (`certificates.ts
|
|
674
|
+
> l'annonce et retombe sur l'auto-signé (`certificates.ts#Certificate.resolveStrategy`).
|
|
579
675
|
|
|
580
676
|
### Ce que le chemin `explicit` évite
|
|
581
677
|
|
|
582
|
-
Aucune génération : le certificat fourni est lu tel quel (`certificates.ts
|
|
678
|
+
Aucune génération : le certificat fourni est lu tel quel (`certificates.ts#trustOutboundAuthorities`). La génération de
|
|
583
679
|
l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
|
|
584
680
|
tierce n'est installée avec `@nodefony/http`.
|
|
585
681
|
|
|
682
|
+
### Autorités sortantes — `extraCaFiles`
|
|
683
|
+
|
|
684
|
+
Les certificats ci-dessus sont ceux que l'application **présente**. L'inverse compte autant :
|
|
685
|
+
quand l'application **appelle** un autre service en `https` (Keycloak, une API partenaire, l'amont
|
|
686
|
+
du proxy inverse), elle n'accepte que les certificats signés par une autorité qu'elle connaît. Une
|
|
687
|
+
autorité publique ne demande rien, et celle de développement de Nodefony est reconnue d'office ;
|
|
688
|
+
une autorité **d'entreprise** se déclare :
|
|
689
|
+
|
|
690
|
+
```typescript ignore
|
|
691
|
+
use("@nodefony/http", {
|
|
692
|
+
certificates: {
|
|
693
|
+
// Fichiers PEM, absolus ou relatifs à la racine de l'application.
|
|
694
|
+
// Un fichier peut porter une chaîne entière (racine + intermédiaires).
|
|
695
|
+
extraCaFiles: ["certs/autorite-entreprise.pem"],
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Le gabarit d'application la lit dans `NF_TLS_CA_FILE`. Les autorités sont ajoutées à celles du
|
|
701
|
+
processus à l'enregistrement du module, avant tout service — donc aussi pour une commande en ligne
|
|
702
|
+
(`Http.onKernelRegister()`, `index.ts#Http.onKernelRegister` ; `trustCaFile()`, `certificates.ts#trustCaFile`). Un fichier
|
|
703
|
+
introuvable ou sans certificat **interrompt le démarrage** en nommant le chemin (code 78), plutôt
|
|
704
|
+
qu'un `fetch failed` loin de sa cause — et, quand le chemin vient de `NF_TLS_CA_FILE`, le refus le
|
|
705
|
+
dit. Une seule exception : l'autorité de **développement** (`nodefony/config/certificates/ca/…`)
|
|
706
|
+
déclarée avant d'avoir été engendrée est **ignorée en développement**, une ligne du journal le dit ;
|
|
707
|
+
elle n'a encore rien signé, et le serveur de développement lui fait confiance dès qu'il la fabrique.
|
|
708
|
+
Rien n'est fabriqué à l'enregistrement : une commande console n'écrit aucun certificat. C'est l'équivalent de `NODE_EXTRA_CA_CERTS`, porté par la
|
|
709
|
+
configuration : rien à poser au lancement, et la même écriture sous Windows.
|
|
710
|
+
|
|
711
|
+
Ce que chaque décor donne — le message du refus porte la même consigne
|
|
712
|
+
(`caFileRemedy()`, `certificates.ts`), et chaque ligne est éprouvée par
|
|
713
|
+
`tests/unit/certificates.test.ts` :
|
|
714
|
+
|
|
715
|
+
| Décor | Résultat |
|
|
716
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
717
|
+
| développement, autorité de dev pas encore engendrée | démarre ; une ligne INFO dit qu'elle naîtra au premier `npx nodefony development` |
|
|
718
|
+
| développement ou production, autorité présente | démarre ; l'autorité est reconnue pour les appels sortants |
|
|
719
|
+
| production, autorité de dev absente | refus 78 : l'engendrer une fois (`npx nodefony http:certificates`), ou retirer la déclaration |
|
|
720
|
+
| autorité d'entreprise absente (tout décor) | refus 78 : poser le fichier au chemin déclaré, ou retirer la déclaration |
|
|
721
|
+
| droit de lecture refusé | refus 78 : donner le droit au compte qui lance l'application (Kubernetes : ConfigMap/Secret lisible) |
|
|
722
|
+
| un dossier déclaré au lieu du fichier | refus 78 : désigner le fichier PEM qu'il contient |
|
|
723
|
+
| une clé privée, ou un certificat DER | refus 78 : fournir le CERTIFICAT en PEM (`openssl x509 -inform der -in a.der -out a.pem`) |
|
|
724
|
+
|
|
725
|
+
Quand le chemin vient de `NF_TLS_CA_FILE`, le refus le dit : c'est la ligne du `.env` à corriger.
|
|
726
|
+
|
|
586
727
|
### Conformité de l'auto-signé
|
|
587
728
|
|
|
588
729
|
Un certificat de développement bâclé fait perdre des heures (« not yet valid », « common name
|
|
589
730
|
invalid »). Celui de Nodefony respecte les règles qui comptent :
|
|
590
731
|
|
|
591
|
-
| Exigence | Norme | Mise en œuvre
|
|
592
|
-
| --------------------------------------- | -------------------- |
|
|
593
|
-
| Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts
|
|
594
|
-
| Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts
|
|
595
|
-
| Le SAN fait foi, pas le CN | RFC
|
|
596
|
-
| `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts
|
|
597
|
-
| Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts
|
|
732
|
+
| Exigence | Norme | Mise en œuvre |
|
|
733
|
+
| --------------------------------------- | -------------------- | ----------------------------------------------------------------------------------- |
|
|
734
|
+
| Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts#selfSignedSchema`) |
|
|
735
|
+
| Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts#Certificate.generateSerialHex`) |
|
|
736
|
+
| Le SAN fait foi, pas le CN | RFC 9525 | SAN dérivé du kernel si non fourni (`config.ts#selfSignedSchema`) |
|
|
737
|
+
| `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts#selfSignedSchema`) |
|
|
738
|
+
| Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts#certificatesSchema`) |
|
|
598
739
|
|
|
599
740
|
### Régénération automatique
|
|
600
741
|
|
|
601
742
|
Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
|
|
602
|
-
(`certificates.ts
|
|
743
|
+
(`certificates.ts#Certificate.isCertAdequate`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
|
|
603
744
|
plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
|
|
604
745
|
contrôle laisserait un certificat obsolète en place indéfiniment.
|
|
605
746
|
|
|
@@ -611,7 +752,7 @@ Dès qu'un proxy est devant l'application, trois questions se posent — et troi
|
|
|
611
752
|
|
|
612
753
|
Sans barrière, n'importe quel client peut envoyer `X-Forwarded-For: 1.2.3.4` et usurper son IP :
|
|
613
754
|
contournement de rate-limit, journaux d'audit falsifiés. Le défaut est donc **`false` — ces en-têtes
|
|
614
|
-
sont ignorés** (`config.ts
|
|
755
|
+
sont ignorés** (`config.ts#healthSchema`), et l'IP retenue est celle de la socket réelle, non falsifiable.
|
|
615
756
|
|
|
616
757
|
| Valeur | Sens |
|
|
617
758
|
| -------------------------------------------- | ------------------------------------------------------------------ |
|
|
@@ -621,30 +762,30 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
|
|
|
621
762
|
| `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
|
|
622
763
|
|
|
623
764
|
La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
|
|
624
|
-
`http-kernel.ts
|
|
765
|
+
`http-kernel.ts#HttpKernel.getTrustProxyChecker`, via `buildTrustProxy()`, `trustProxy.ts#buildTrustProxy`) : aucune structure allouée par
|
|
625
766
|
requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
|
|
626
|
-
(`resolveForwarded()`, `forwarded.ts
|
|
767
|
+
(`resolveForwarded()`, `forwarded.ts#resolveForwarded`) — conforme RFC 7239 et à la recommandation OWASP.
|
|
627
768
|
|
|
628
769
|
### « Ce `Host` est-il le mien ? » → `trustedHosts`
|
|
629
770
|
|
|
630
771
|
Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
|
|
631
772
|
kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
|
|
632
|
-
`http-kernel.ts
|
|
773
|
+
`http-kernel.ts#HttpKernel.compileAlias`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
|
|
633
774
|
d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
|
|
634
|
-
filtre déjà le `Host` (`config.ts
|
|
775
|
+
filtre déjà le `Host` (`config.ts#healthSchema`).
|
|
635
776
|
|
|
636
777
|
### « Cette page a-t-elle le droit d'ouvrir un WebSocket ? » → `allowedOrigins`
|
|
637
778
|
|
|
638
779
|
Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
|
|
639
780
|
WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
|
|
640
781
|
par défaut que l'`Origin` du handshake corresponde au `Host` servi
|
|
641
|
-
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts
|
|
642
|
-
et allowlist explicite pour les SPA cross-origine. Un refus se solde par
|
|
643
|
-
|
|
782
|
+
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts#HttpKernel.checkWebsocketOrigin`), avec tolérance loopback en développement
|
|
783
|
+
et allowlist explicite pour les SPA cross-origine. Un refus se solde par un **`403` au handshake**, avant le
|
|
784
|
+
`101` : la socket ne s'ouvre jamais (RFC 6455 §4.2.2).
|
|
644
785
|
|
|
645
786
|
> [!NOTE]
|
|
646
787
|
> Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
|
|
647
|
-
> (`http-kernel.ts
|
|
788
|
+
> (`http-kernel.ts#HttpKernel.getWsOriginPolicy`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
|
|
648
789
|
> il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
|
|
649
790
|
|
|
650
791
|
## Probes de santé — `/livez` et `/readyz`
|
|
@@ -661,13 +802,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
|
|
|
661
802
|
protéger.
|
|
662
803
|
|
|
663
804
|
Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
|
|
664
|
-
(`http-kernel.ts
|
|
665
|
-
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts
|
|
805
|
+
(`http-kernel.ts#HttpKernel.onHttpRequest`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
|
|
806
|
+
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts#HttpKernel.#respondHealth`). Et surtout : **avant le
|
|
666
807
|
rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
|
|
667
808
|
|
|
668
809
|
| Option | Type | Défaut | Effet |
|
|
669
810
|
| --------------- | ------ | --------- | ---------------------------------------------------------------------- |
|
|
670
|
-
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts
|
|
811
|
+
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts#healthSchema`). |
|
|
671
812
|
| `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
|
|
672
813
|
| `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
|
|
673
814
|
| `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
|
|
@@ -731,28 +872,35 @@ sequenceDiagram
|
|
|
731
872
|
```
|
|
732
873
|
|
|
733
874
|
**Étape 1 — la disponibilité bascule en premier.** L'écouteur est posé à `onPostReady` pour être
|
|
734
|
-
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts
|
|
875
|
+
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts#HttpKernel.init`). `readyz` répond `503`, le
|
|
735
876
|
load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
|
|
736
877
|
propager.
|
|
737
878
|
|
|
738
|
-
**Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts
|
|
739
|
-
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts
|
|
879
|
+
**Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts#Websocket.terminate`)
|
|
880
|
+
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts#Websocket.terminate`).
|
|
740
881
|
Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
|
|
741
882
|
indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
|
|
742
883
|
|
|
743
|
-
**Étape 3 — les requêtes HTTP se terminent.** Le drain
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
884
|
+
**Étape 3 — les requêtes HTTP se terminent.** Le drain (`createDrainTerminator()`,
|
|
885
|
+
`serverShutdown.ts#createDrainTerminator`) cesse d'accepter, puis traite chaque connexion selon son protocole :
|
|
886
|
+
|
|
887
|
+
- **HTTP/2** (5152, et h2c) : chaque session reçoit un **GOAWAY** — les flux en vol se terminent, les
|
|
888
|
+
nouveaux sont refusés, et le client (navigateur, proxy) rouvre ailleurs au lieu de constater une
|
|
889
|
+
coupure ;
|
|
890
|
+
- **HTTP/1.1** : en-tête `connection: close` injecté sur les réponses en cours, sockets inactives
|
|
891
|
+
fermées.
|
|
892
|
+
|
|
893
|
+
Ce qui reste au-delà de `shutdownTimeout` (défaut `5000` ms, `serverShutdown.ts#DEFAULT_SHUTDOWN_TIMEOUT`)
|
|
894
|
+
est détruit.
|
|
747
895
|
|
|
748
896
|
> [!IMPORTANT]
|
|
749
897
|
> L'ordre WS-avant-HTTP n'est pas cosmétique. Les serveurs WebSocket s'inscrivent en
|
|
750
|
-
> `prependOnceListener` (`server-websocket.ts
|
|
751
|
-
> (`server-http.ts
|
|
898
|
+
> `prependOnceListener` (`server-websocket.ts#Websocket.createServer`), les serveurs HTTP en `once`
|
|
899
|
+
> (`server-http.ts#ServerHttp.createServer`). Inversé, le drain détruirait les sockets déjà upgradées **sans** frame
|
|
752
900
|
> Close : retour du 1006 pour tous les clients temps réel.
|
|
753
901
|
|
|
754
902
|
Deux garde-fous encadrent la séquence : `shutdownTimeout` **par serveur** (le drain nominal) et
|
|
755
|
-
`shutdownDeadline` **global** au kernel (défaut 15 s, `src/nodefony/src/config/defaults.ts
|
|
903
|
+
`shutdownDeadline` **global** au kernel (défaut 15 s, `src/nodefony/src/config/defaults.ts#defaultAppConfig`), filet
|
|
756
904
|
anti-écouteur bloqué. Les deux doivent rester sous la période de grâce de l'orchestrateur — 30 s en
|
|
757
905
|
Kubernetes, 10 s avec Docker.
|
|
758
906
|
|
|
@@ -766,34 +914,34 @@ transport.
|
|
|
766
914
|
Dès qu'un écouteur `clientError` est attaché, **Node cesse de fermer la socket automatiquement**. Un
|
|
767
915
|
écouteur naïf (« je journalise et je passe ») transforme donc une requête malformée en **fuite de
|
|
768
916
|
socket et de descripteur de fichier** — c'est-à-dire un vecteur de déni de service. Nodefony répond et
|
|
769
|
-
ferme explicitement (`handleClientError()`, `clientError.ts
|
|
770
|
-
(RFC 6585 §5), `400` sinon (`clientError.ts
|
|
917
|
+
ferme explicitement (`handleClientError()`, `clientError.ts#handleClientError`) : `431` si les en-têtes débordent
|
|
918
|
+
(RFC 6585 §5), `400` sinon (`clientError.ts#handleClientError`), et rien du tout si la socket est déjà morte.
|
|
771
919
|
|
|
772
920
|
### Connexions WebSocket zombies
|
|
773
921
|
|
|
774
922
|
Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
|
|
775
923
|
descripteur retenus pour personne. Le battement de cœur les réclame
|
|
776
|
-
(`startHeartbeat()`, `wsHeartbeat.ts
|
|
777
|
-
les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts
|
|
924
|
+
(`startHeartbeat()`, `wsHeartbeat.ts#startHeartbeat`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
|
|
925
|
+
les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts#IHeartbeatClient.terminate`).
|
|
778
926
|
|
|
779
927
|
L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
|
|
780
928
|
serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
|
|
781
|
-
(`trackPong()`, `wsHeartbeat.ts
|
|
929
|
+
(`trackPong()`, `wsHeartbeat.ts#trackPong`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
|
|
782
930
|
horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
|
|
783
|
-
configuration pathologique (`tick`, `wsHeartbeat.ts
|
|
931
|
+
configuration pathologique (`tick`, `wsHeartbeat.ts#startHeartbeat`) ; et un timer `unref` qui ne retient jamais le
|
|
784
932
|
processus à l'arrêt.
|
|
785
933
|
|
|
786
934
|
### Floods de connexions
|
|
787
935
|
|
|
788
936
|
L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
|
|
789
937
|
IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
|
|
790
|
-
(`HttpKernel.
|
|
791
|
-
|
|
792
|
-
(`
|
|
938
|
+
(`HttpKernel.onWebsocketUpgrade()`, `http-kernel.ts#HttpKernel.onWebsocketUpgrade`), et AVANT le `101` : le handshake reçoit un
|
|
939
|
+
**`429`** HTTP, jamais une socket ouverte puis fermée
|
|
940
|
+
(`websocketQuotaRefusal()`, `http-kernel.ts#HttpKernel.websocketQuotaRefusal`), sans
|
|
793
941
|
journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
|
|
794
942
|
|
|
795
943
|
Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
|
|
796
|
-
`wsMaxConnectionsPerIp` (`config.ts
|
|
944
|
+
`wsMaxConnectionsPerIp` (`config.ts#httpConfigSchema`). En cloud-native, laisser `null` et déléguer à l'edge —
|
|
797
945
|
nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
|
|
798
946
|
descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
|
|
799
947
|
|
|
@@ -810,17 +958,17 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
|
|
|
810
958
|
par seconde. Les choix visibles dans le code :
|
|
811
959
|
|
|
812
960
|
- **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
|
|
813
|
-
socket (`wsHeartbeat.ts
|
|
961
|
+
socket (`wsHeartbeat.ts#startHeartbeat`).
|
|
814
962
|
- **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
|
|
815
|
-
`trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts
|
|
963
|
+
`trustedHosts` sont compilés une fois et mémoïsés (`getTrustProxyChecker()`, `http-kernel.ts#HttpKernel.getTrustProxyChecker` ; `getWsOriginPolicy()`, `http-kernel.ts#HttpKernel.getWsOriginPolicy` ; `compileAlias()`, `http-kernel.ts#HttpKernel.compileAlias`).
|
|
816
964
|
- **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
|
|
817
965
|
DI et l'ALS : un flood coûte une recherche dans une table de hachage.
|
|
818
966
|
- **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
|
|
819
|
-
(`http-kernel.ts
|
|
967
|
+
(`http-kernel.ts#HttpKernel.constructor`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
|
|
820
968
|
- **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
|
|
821
969
|
route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
|
|
822
970
|
production mono-processus).
|
|
823
|
-
- **Aucune génération en production** avec un certificat fourni (`certificates.ts
|
|
971
|
+
- **Aucune génération en production** avec un certificat fourni (`certificates.ts#trustOutboundAuthorities`).
|
|
824
972
|
|
|
825
973
|
Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
|
|
826
974
|
boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
|
|
@@ -835,61 +983,61 @@ demande le backplane realtime.
|
|
|
835
983
|
|
|
836
984
|
## 📜 Normes appliquées
|
|
837
985
|
|
|
838
|
-
| Domaine | Norme | Ancrage
|
|
839
|
-
| ------------------------------------- | ------------------ |
|
|
840
|
-
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts
|
|
841
|
-
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts
|
|
842
|
-
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts
|
|
843
|
-
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts
|
|
844
|
-
| WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts
|
|
845
|
-
| WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts
|
|
846
|
-
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts
|
|
847
|
-
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts
|
|
848
|
-
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts
|
|
849
|
-
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts
|
|
850
|
-
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts
|
|
851
|
-
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts
|
|
852
|
-
| Certificat — identité par le SAN | RFC
|
|
986
|
+
| Domaine | Norme | Ancrage |
|
|
987
|
+
| ------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
|
|
988
|
+
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts#HttpKernel.onHttpRequest`) |
|
|
989
|
+
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts#ServerHttps.createServerH2`) |
|
|
990
|
+
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts#http2Schema`) |
|
|
991
|
+
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts#handleClientError`) |
|
|
992
|
+
| WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts#certificatesSchema`) |
|
|
993
|
+
| WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts#Websocket.terminate`) |
|
|
994
|
+
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts#websocketSchema`) |
|
|
995
|
+
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts#websocketSchema`) |
|
|
996
|
+
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts#websocketSchema`) |
|
|
997
|
+
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts#HttpKernel.checkWebsocketOrigin`) |
|
|
998
|
+
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts#resolveForwarded`) |
|
|
999
|
+
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts#Certificate.generateSerialHex`) |
|
|
1000
|
+
| Certificat — identité par le SAN | RFC 9525 | `sanSchema` (`config.ts#sanSchema`) |
|
|
853
1001
|
|
|
854
1002
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
855
1003
|
|
|
856
|
-
| Symptôme | Cause | Correction
|
|
857
|
-
| --------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
858
|
-
| L'app écoute sur 5153 au lieu de 5151 | `portPolicy: "auto"` (défaut dev) : le port était pris — c'est **annoncé** | Libérer le port, ou `servers.portPolicy: "strict"` pour échouer franchement
|
|
859
|
-
| En production, le pod est « sain » mais injoignable | Un port glissé en silence | Rien à faire : `strict` est le défaut hors dev — vérifier qu'on ne l'a pas forcé
|
|
860
|
-
| `nodefony status` ne voit pas le serveur | Ports sondés par convention alors qu'ils ont glissé | Déjà géré : les ports effectifs sont publiés (`http-kernel.ts
|
|
861
|
-
| Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts
|
|
862
|
-
| Plus de WebSocket après avoir mis `servers.https: false` | Le WSS est adossé au serveur HTTPS et tombe avec lui | Attendu — utiliser `ws://` sur le port clair, ou réactiver HTTPS
|
|
863
|
-
| Le client WebSocket voit `1006` à chaque redéploiement | Frame Close jamais reçue (socket coupée avant) | Déjà géré : close `1001` avant le drain (`server-websocket.ts
|
|
864
|
-
| Requêtes coupées à chaque mise à jour progressive | Destruction des connexions au lieu d'un drain | Déjà géré (`createDrainTerminator()`) — vérifier `shutdownTimeout` < grâce k8s
|
|
865
|
-
| Le pod redémarre en boucle pendant l'arrêt | `livez` répondrait `503` pendant le drain | Déjà géré : `livez` reste `200`, seul `readyz` bascule
|
|
866
|
-
| Cascade de redémarrages sous charge | Sonde de santé soumise au rate-limit | Déjà géré : les probes court-circuitent avant le rate-limit (`http-kernel.ts
|
|
867
|
-
| `curl --http2` renvoie du HTTP/1.1 | `servers.https.protocol: "1.1"`, ou client sans ALPN | Passer `protocol: "2.0"` (défaut) et vérifier le client
|
|
868
|
-
| Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer
|
|
869
|
-
| Le certificat n'est pas régénéré après un changement de domaine | On croit qu'un fichier présent suffit | Déjà géré : le SAN est vérifié (`certificates.ts
|
|
870
|
-
| `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux
|
|
871
|
-
| Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false`
|
|
872
|
-
| Handshake WebSocket refusé en `
|
|
873
|
-
|
|
|
1004
|
+
| Symptôme | Cause | Correction |
|
|
1005
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
1006
|
+
| L'app écoute sur 5153 au lieu de 5151 | `portPolicy: "auto"` (défaut dev) : le port était pris — c'est **annoncé** | Libérer le port, ou `servers.portPolicy: "strict"` pour échouer franchement |
|
|
1007
|
+
| En production, le pod est « sain » mais injoignable | Un port glissé en silence | Rien à faire : `strict` est le défaut hors dev — vérifier qu'on ne l'a pas forcé |
|
|
1008
|
+
| `nodefony status` ne voit pas le serveur | Ports sondés par convention alors qu'ils ont glissé | Déjà géré : les ports effectifs sont publiés (`http-kernel.ts#RouteMetaData`) |
|
|
1009
|
+
| Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts#WebsocketSecure.constructor`) |
|
|
1010
|
+
| Plus de WebSocket après avoir mis `servers.https: false` | Le WSS est adossé au serveur HTTPS et tombe avec lui | Attendu — utiliser `ws://` sur le port clair, ou réactiver HTTPS |
|
|
1011
|
+
| Le client WebSocket voit `1006` à chaque redéploiement | Frame Close jamais reçue (socket coupée avant) | Déjà géré : close `1001` avant le drain (`server-websocket.ts#Websocket.terminate`) |
|
|
1012
|
+
| Requêtes coupées à chaque mise à jour progressive | Destruction des connexions au lieu d'un drain | Déjà géré (`createDrainTerminator()`) — vérifier `shutdownTimeout` < grâce k8s |
|
|
1013
|
+
| Le pod redémarre en boucle pendant l'arrêt | `livez` répondrait `503` pendant le drain | Déjà géré : `livez` reste `200`, seul `readyz` bascule |
|
|
1014
|
+
| Cascade de redémarrages sous charge | Sonde de santé soumise au rate-limit | Déjà géré : les probes court-circuitent avant le rate-limit (`http-kernel.ts#HttpKernel.handle`) |
|
|
1015
|
+
| `curl --http2` renvoie du HTTP/1.1 | `servers.https.protocol: "1.1"`, ou client sans ALPN | Passer `protocol: "2.0"` (défaut) et vérifier le client |
|
|
1016
|
+
| Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
|
|
1017
|
+
| Le certificat n'est pas régénéré après un changement de domaine | On croit qu'un fichier présent suffit | Déjà géré : le SAN est vérifié (`certificates.ts#Certificate.isDev`) |
|
|
1018
|
+
| `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux |
|
|
1019
|
+
| Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
|
|
1020
|
+
| Handshake WebSocket refusé en `403` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
|
|
1021
|
+
| Handshakes WebSocket refusés en `429` | Rate-limit d'IP, ou plafond de connexions concurrentes | Vérifier `rateLimit` et `wsMaxConnectionsPerIp` |
|
|
874
1022
|
|
|
875
1023
|
## 📡 Observabilité — Studio et CLI
|
|
876
1024
|
|
|
877
1025
|
**Studio.** L'onglet **Configuration** rend la config du module en réglages documentés (type, défaut,
|
|
878
1026
|
état, valeur effective) à partir du JSON Schema dérivé du schéma Zod
|
|
879
|
-
(`src/packages/@nodefony/http/index.ts
|
|
1027
|
+
(`src/packages/@nodefony/http/index.ts#Http.constructor`). Le profiler expose les phases par requête, y compris
|
|
880
1028
|
l'origine du transport.
|
|
881
1029
|
|
|
882
1030
|
**CLI.** Trois commandes touchent directement aux serveurs :
|
|
883
1031
|
|
|
884
|
-
| Commande | Rôle
|
|
885
|
-
| ---------------------------------------------------- |
|
|
886
|
-
| `nodefony http:network [interface] [--json]` | Interfaces réseau vues par le kernel (`networkCommand.ts
|
|
887
|
-
| `nodefony http:certificates [--force] [--json]` | (Re)génère et décrit le certificat de dev (`certificatesCommand.ts
|
|
888
|
-
| `nodefony proxy:generate <nginx\|haproxy> [-o file]` | Dérive une configuration reverse-proxy de l'introspection réelle.
|
|
1032
|
+
| Commande | Rôle |
|
|
1033
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
1034
|
+
| `nodefony http:network [interface] [--json]` | Interfaces réseau vues par le kernel (`networkCommand.ts#Network.constructor`). |
|
|
1035
|
+
| `nodefony http:certificates [--force] [--json]` | (Re)génère et décrit le certificat de dev (`certificatesCommand.ts#Certificates.constructor`). |
|
|
1036
|
+
| `nodefony proxy:generate <nginx\|haproxy> [-o file]` | Dérive une configuration reverse-proxy de l'introspection réelle. |
|
|
889
1037
|
|
|
890
1038
|
`proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
|
|
891
1039
|
confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
|
|
892
|
-
résumé de certificat vient de `Certificate.describe()` (`certificates.ts
|
|
1040
|
+
résumé de certificat vient de `Certificate.describe()` (`certificates.ts#Certificate.describe`), source unique partagée
|
|
893
1041
|
par la commande, le boot et un futur écran d'administration.
|
|
894
1042
|
|
|
895
1043
|
**Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
|