@nodefony/http 10.0.0-beta.1 → 10.0.0-beta.3
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 +5 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +48 -7
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +214 -158
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +9 -4
- package/dist/nodefony/src/context/http/Response.js +46 -7
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +17 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +126 -50
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- 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/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +9 -3
- package/docs/observabilite.md +18 -18
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +115 -78
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +37 -37
- package/package.json +10 -13
package/docs/servers.md
CHANGED
|
@@ -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. |
|
|
@@ -128,12 +128,12 @@ Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttp
|
|
|
128
128
|
|
|
129
129
|
**Le WebSocket n'est jamais un citoyen de seconde zone.** Il est adossé au serveur HTTP porteur
|
|
130
130
|
(`server-websocket.ts:80`), passe par le **même** rate-limit d'IP que les requêtes HTTP — un upgrade
|
|
131
|
-
_est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:
|
|
131
|
+
_est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`) —, 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:1216`). Elle ne fait rien aujourd'hui.
|
|
137
137
|
|
|
138
138
|
## 🚀 Démarrage rapide
|
|
139
139
|
|
|
@@ -249,22 +249,31 @@ export default PingController;
|
|
|
249
249
|
|
|
250
250
|
### 4. Ce qu'on observe au boot
|
|
251
251
|
|
|
252
|
-
Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1355`). En développement, le
|
|
253
|
+
bilan de démarrage (`renderStartupHuman`, `startupScreen.ts`) dit d'abord **où aller**,
|
|
254
|
+
puis **ce qui est à regarder**, puis les adresses d'écoute :
|
|
255
255
|
|
|
256
256
|
```text
|
|
257
|
-
✓ Prêt en 1.
|
|
257
|
+
✓ Prêt en 6,1 s — Nodefony 10.0.0 · development
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
OUVRIR
|
|
260
|
+
➜ Application https://localhost:5152/
|
|
261
|
+
➜ Studio https://localhost:5152/nodefony
|
|
262
|
+
|
|
263
|
+
ÉTAT
|
|
264
|
+
Mode development · rechargement auto
|
|
265
|
+
Écoute HTTP + WS 127.0.0.1:5151 · HTTP/2 + WSS 127.0.0.1:5152
|
|
264
266
|
```
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
L'adresse à **ouvrir** n'est pas l'adresse de **liaison** : un serveur lié à `127.0.0.1` ou à
|
|
269
|
+
`0.0.0.0` s'ouvre sur `localhost` (`openableHost`, `bootReport.ts:216`) — les passkeys refusent
|
|
270
|
+
une IP comme domaine, et un cookie posé sous un nom ne part pas sous l'autre. Hors d'un
|
|
271
|
+
terminal, le même bilan sort ligne à ligne (`open.app: https://localhost:5152/`,
|
|
272
|
+
`listen.https: 127.0.0.1:5152`) ; pour une machine, `nodefony status --json` le rend en entier.
|
|
273
|
+
|
|
274
|
+
Hors écran animé (production, CI, `--debug`, sortie non-terminal), les bannières par serveur
|
|
275
|
+
sortent aussi (`ServerHttp.showBanner()`, `server-http.ts:239`, appelées par le kernel —
|
|
276
|
+
`Kernel.ts:1403`) :
|
|
268
277
|
|
|
269
278
|
```text
|
|
270
279
|
Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
|
|
@@ -327,8 +336,8 @@ Choisir en cinq secondes :
|
|
|
327
336
|
| `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
|
|
328
337
|
| `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
|
|
329
338
|
|
|
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:
|
|
339
|
+
L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1223`) : chaque serveur est
|
|
340
|
+
consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:1223`).
|
|
332
341
|
Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
|
|
333
342
|
|
|
334
343
|
### `server-http` — HTTP/1.1 en clair
|
|
@@ -375,7 +384,7 @@ automatiquement un éventuel décalage de port.
|
|
|
375
384
|
réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
|
|
376
385
|
le battement de cœur (`server-websocket.ts:80`).
|
|
377
386
|
- **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts:87`) et par connexion
|
|
378
|
-
(`trackPong()`, `server-websocket.ts:
|
|
387
|
+
(`trackPong()`, `server-websocket.ts:113`).
|
|
379
388
|
- **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
|
|
380
389
|
(`prependOnceListener`, `server-websocket.ts:91`) — l'ordre
|
|
381
390
|
compte, voir la section Arrêt gracieux.
|
|
@@ -400,7 +409,7 @@ C'est la distinction la plus utile de cette page, et celle qu'on rate le plus so
|
|
|
400
409
|
| Question | Où ça se règle | Source |
|
|
401
410
|
| ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
|
|
402
411
|
| **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts:149`) |
|
|
403
|
-
| **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:
|
|
412
|
+
| **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:1171`) |
|
|
404
413
|
|
|
405
414
|
Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
|
|
406
415
|
la CI et le cluster) ; le **réglage** est une propriété de l'application.
|
|
@@ -451,16 +460,16 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
|
|
|
451
460
|
|
|
452
461
|
### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
|
|
453
462
|
|
|
454
|
-
Depuis `websocketSchema` (`config.ts:
|
|
455
|
-
lit `websocketSecure` (`config.ts:
|
|
463
|
+
Depuis `websocketSchema` (`config.ts:535`). Les deux sections partagent la forme et les défauts ; le WSS
|
|
464
|
+
lit `websocketSecure` (`config.ts:1240`).
|
|
456
465
|
|
|
457
466
|
| Option | Type | Défaut | Effet |
|
|
458
467
|
| ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------- |
|
|
459
468
|
| `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
|
|
460
469
|
| `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
|
|
461
470
|
| `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:
|
|
471
|
+
| `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts:561`). |
|
|
472
|
+
| `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts:570`). |
|
|
464
473
|
| `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
|
|
465
474
|
| `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
|
|
466
475
|
| `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
|
|
@@ -484,7 +493,7 @@ panne, c'est une nuisance.
|
|
|
484
493
|
|
|
485
494
|
### La règle
|
|
486
495
|
|
|
487
|
-
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:
|
|
496
|
+
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:1845` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
|
|
488
497
|
toujours :
|
|
489
498
|
|
|
490
499
|
| Environnement | Défaut | Pourquoi |
|
|
@@ -504,9 +513,11 @@ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
|
|
|
504
513
|
Le décalage est **toujours annoncé** (`server-http.ts:130`) : jamais de dégradation silencieuse. Trois
|
|
505
514
|
détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
|
|
506
515
|
|
|
507
|
-
1. **
|
|
508
|
-
|
|
509
|
-
`
|
|
516
|
+
1. **Le repli se décide au `listen()`, qui est atomique.** Une seule sonde le précède, sur le port
|
|
517
|
+
désiré : elle détecte un serveur déjà présent sur la boucle locale, que le noyau accorderait quand
|
|
518
|
+
même à un `0.0.0.0` (`detectPortConflict`). Elle est **sautée dans un worker de cluster**
|
|
519
|
+
(`cluster.isWorker`) : le port y est partagé par le maître, et le voisin qui répond est un frère,
|
|
520
|
+
pas un intrus. La course résiduelle retombe sur `EADDRINUSE` (`bindWithFallback()`,
|
|
510
521
|
`portBinder.ts:263`).
|
|
511
522
|
2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
|
|
512
523
|
ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
|
|
@@ -524,9 +535,10 @@ laisser un processus se croire démarré.
|
|
|
524
535
|
|
|
525
536
|
Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
|
|
526
537
|
convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
|
|
527
|
-
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:
|
|
538
|
+
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1279`) écrit donc la
|
|
528
539
|
topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
|
|
529
|
-
environnements** : une application qui déclare son port
|
|
540
|
+
environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
|
|
541
|
+
aussi de la convention,
|
|
530
542
|
même en `strict`. L'écriture est au mieux-effort — une image en lecture seule ne fait jamais tomber un
|
|
531
543
|
serveur qui, lui, écoute très bien.
|
|
532
544
|
|
|
@@ -537,19 +549,19 @@ serveur qui, lui, écoute très bien.
|
|
|
537
549
|
**Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
|
|
538
550
|
n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
|
|
539
551
|
ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
|
|
540
|
-
(`Certificate.resolveStrategy()`, `certificates.ts:
|
|
552
|
+
(`Certificate.resolveStrategy()`, `certificates.ts:577`).
|
|
541
553
|
|
|
542
554
|
### Les quatre stratégies
|
|
543
555
|
|
|
544
|
-
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:
|
|
545
|
-
`Certificate.resolveStrategy()` (`certificates.ts:
|
|
556
|
+
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
|
|
557
|
+
`Certificate.resolveStrategy()` (`certificates.ts:577`).
|
|
546
558
|
|
|
547
559
|
| Stratégie | Quand l'utiliser | Ce qui se passe |
|
|
548
560
|
| --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
|
|
549
561
|
| `auto` (défaut) | On ne veut pas décider | `key`+`cert` fournis → `explicit` ; sinon mkcert en dev ; sinon auto-signé. |
|
|
550
562
|
| `explicit` | **Production** | Charge `key`/`cert`/`ca` depuis la config. Erreur au boot si absents. |
|
|
551
563
|
| `mkcert` | Développement avec HTTPS sans avertissement | CA locale de confiance → HMR cross-origin et WSS sans erreur navigateur. |
|
|
552
|
-
| `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé node
|
|
564
|
+
| `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé (`node:crypto`), non trusté. |
|
|
553
565
|
|
|
554
566
|
```typescript
|
|
555
567
|
// nodefony.config.ts — production : certificat fourni, jamais généré
|
|
@@ -572,13 +584,38 @@ export default defineConfig(() => ({
|
|
|
572
584
|
> [!TIP]
|
|
573
585
|
> Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
|
|
574
586
|
> et au WSS) : `brew install mkcert nss && mkcert -install`. Nodefony le détecte tout seul, sinon il
|
|
575
|
-
> l'annonce et retombe sur l'auto-signé (`certificates.ts:
|
|
587
|
+
> l'annonce et retombe sur l'auto-signé (`certificates.ts:602`).
|
|
576
588
|
|
|
577
589
|
### Ce que le chemin `explicit` évite
|
|
578
590
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
591
|
+
Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330`). La génération de
|
|
592
|
+
l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
|
|
593
|
+
tierce n'est installée avec `@nodefony/http`.
|
|
594
|
+
|
|
595
|
+
### Autorités sortantes — `extraCaFiles`
|
|
596
|
+
|
|
597
|
+
Les certificats ci-dessus sont ceux que l'application **présente**. L'inverse compte autant :
|
|
598
|
+
quand l'application **appelle** un autre service en `https` (Keycloak, une API partenaire, l'amont
|
|
599
|
+
du proxy inverse), elle n'accepte que les certificats signés par une autorité qu'elle connaît. Une
|
|
600
|
+
autorité publique ne demande rien, et celle de développement de Nodefony est reconnue d'office ;
|
|
601
|
+
une autorité **d'entreprise** se déclare :
|
|
602
|
+
|
|
603
|
+
```typescript ignore
|
|
604
|
+
use("@nodefony/http", {
|
|
605
|
+
certificates: {
|
|
606
|
+
// Fichiers PEM, absolus ou relatifs à la racine de l'application.
|
|
607
|
+
// Un fichier peut porter une chaîne entière (racine + intermédiaires).
|
|
608
|
+
extraCaFiles: ["certs/autorite-entreprise.pem"],
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Le gabarit d'application la lit dans `NF_TLS_CA_FILE`. Les autorités sont ajoutées à celles du
|
|
614
|
+
processus à l'enregistrement du module, avant tout service — donc aussi pour une commande en ligne
|
|
615
|
+
(`Http.onKernelRegister()`, `index.ts:104` ; `trustCaFile()`, `certificates.ts:131`). Un fichier
|
|
616
|
+
introuvable ou sans certificat **interrompt le démarrage** en nommant le chemin (code 78), plutôt
|
|
617
|
+
qu'un `fetch failed` loin de sa cause. C'est l'équivalent de `NODE_EXTRA_CA_CERTS`, porté par la
|
|
618
|
+
configuration : rien à poser au lancement, et la même écriture sous Windows.
|
|
582
619
|
|
|
583
620
|
### Conformité de l'auto-signé
|
|
584
621
|
|
|
@@ -588,15 +625,15 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
|
|
|
588
625
|
| Exigence | Norme | Mise en œuvre |
|
|
589
626
|
| --------------------------------------- | -------------------- | --------------------------------------------------------- |
|
|
590
627
|
| Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
|
|
591
|
-
| Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:
|
|
592
|
-
| Le SAN fait foi, pas le CN | RFC
|
|
628
|
+
| Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:443`) |
|
|
629
|
+
| Le SAN fait foi, pas le CN | RFC 9525 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
|
|
593
630
|
| `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
|
|
594
631
|
| Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
|
|
595
632
|
|
|
596
633
|
### Régénération automatique
|
|
597
634
|
|
|
598
635
|
Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
|
|
599
|
-
(`certificates.ts:
|
|
636
|
+
(`certificates.ts:743`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
|
|
600
637
|
plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
|
|
601
638
|
contrôle laisserait un certificat obsolète en place indéfiniment.
|
|
602
639
|
|
|
@@ -618,7 +655,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
|
|
|
618
655
|
| `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
|
|
619
656
|
|
|
620
657
|
La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
|
|
621
|
-
`http-kernel.ts:
|
|
658
|
+
`http-kernel.ts:627`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
|
|
622
659
|
requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
|
|
623
660
|
(`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
|
|
624
661
|
|
|
@@ -626,7 +663,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
|
|
|
626
663
|
|
|
627
664
|
Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
|
|
628
665
|
kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
|
|
629
|
-
`http-kernel.ts:
|
|
666
|
+
`http-kernel.ts:1082`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
|
|
630
667
|
d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
|
|
631
668
|
filtre déjà le `Host` (`config.ts:974`).
|
|
632
669
|
|
|
@@ -635,13 +672,13 @@ filtre déjà le `Host` (`config.ts:974`).
|
|
|
635
672
|
Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
|
|
636
673
|
WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
|
|
637
674
|
par défaut que l'`Origin` du handshake corresponde au `Host` servi
|
|
638
|
-
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:
|
|
675
|
+
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:724`), avec tolérance loopback en développement
|
|
639
676
|
et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
|
|
640
677
|
Violation)**, jamais par un code HTTP.
|
|
641
678
|
|
|
642
679
|
> [!NOTE]
|
|
643
680
|
> Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
|
|
644
|
-
> (`http-kernel.ts:
|
|
681
|
+
> (`http-kernel.ts:642`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
|
|
645
682
|
> il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
|
|
646
683
|
|
|
647
684
|
## Probes de santé — `/livez` et `/readyz`
|
|
@@ -658,13 +695,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
|
|
|
658
695
|
protéger.
|
|
659
696
|
|
|
660
697
|
Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
|
|
661
|
-
(`http-kernel.ts:
|
|
662
|
-
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:
|
|
698
|
+
(`http-kernel.ts:1093`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
|
|
699
|
+
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:561`). Et surtout : **avant le
|
|
663
700
|
rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
|
|
664
701
|
|
|
665
702
|
| Option | Type | Défaut | Effet |
|
|
666
703
|
| --------------- | ------ | --------- | ---------------------------------------------------------------------- |
|
|
667
|
-
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:
|
|
704
|
+
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:961`). |
|
|
668
705
|
| `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
|
|
669
706
|
| `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
|
|
670
707
|
| `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
|
|
@@ -728,11 +765,11 @@ sequenceDiagram
|
|
|
728
765
|
```
|
|
729
766
|
|
|
730
767
|
**Étape 1 — la disponibilité bascule en premier.** L'écouteur est posé à `onPostReady` pour être
|
|
731
|
-
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:
|
|
768
|
+
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:587`). `readyz` répond `503`, le
|
|
732
769
|
load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
|
|
733
770
|
propager.
|
|
734
771
|
|
|
735
|
-
**Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:
|
|
772
|
+
**Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:121`)
|
|
736
773
|
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
|
|
737
774
|
Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
|
|
738
775
|
indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
|
|
@@ -770,27 +807,27 @@ ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les
|
|
|
770
807
|
|
|
771
808
|
Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
|
|
772
809
|
descripteur retenus pour personne. Le battement de cœur les réclame
|
|
773
|
-
(`startHeartbeat()`, `wsHeartbeat.ts:
|
|
774
|
-
les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts:
|
|
810
|
+
(`startHeartbeat()`, `wsHeartbeat.ts:124`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
|
|
811
|
+
les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts:37`).
|
|
775
812
|
|
|
776
813
|
L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
|
|
777
814
|
serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
|
|
778
|
-
(`trackPong()`, `wsHeartbeat.ts:
|
|
815
|
+
(`trackPong()`, `wsHeartbeat.ts:54`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
|
|
779
816
|
horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
|
|
780
|
-
configuration pathologique (`tick`, `wsHeartbeat.ts:
|
|
817
|
+
configuration pathologique (`tick`, `wsHeartbeat.ts:136`) ; et un timer `unref` qui ne retient jamais le
|
|
781
818
|
processus à l'arrêt.
|
|
782
819
|
|
|
783
820
|
### Floods de connexions
|
|
784
821
|
|
|
785
822
|
L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
|
|
786
823
|
IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
|
|
787
|
-
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:
|
|
824
|
+
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`). Le `101` étant déjà émis par `ws`, un `429`
|
|
788
825
|
est impossible → la connexion est fermée en **1013 « Try Again Later »**
|
|
789
|
-
(`
|
|
826
|
+
(`websocketQuotaRefusal()`, `http-kernel.ts:2230`), sans
|
|
790
827
|
journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
|
|
791
828
|
|
|
792
829
|
Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
|
|
793
|
-
`wsMaxConnectionsPerIp` (`config.ts:
|
|
830
|
+
`wsMaxConnectionsPerIp` (`config.ts:1250`). En cloud-native, laisser `null` et déléguer à l'edge —
|
|
794
831
|
nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
|
|
795
832
|
descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
|
|
796
833
|
|
|
@@ -807,17 +844,17 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
|
|
|
807
844
|
par seconde. Les choix visibles dans le code :
|
|
808
845
|
|
|
809
846
|
- **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
|
|
810
|
-
socket (`wsHeartbeat.ts:
|
|
847
|
+
socket (`wsHeartbeat.ts:137`).
|
|
811
848
|
- **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
|
|
812
|
-
`trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:
|
|
849
|
+
`trustedHosts` sont compilés une fois et mémoïsés (`getTrustProxyChecker()`, `http-kernel.ts:627` ; `getWsOriginPolicy()`, `http-kernel.ts:634` ; `compileAlias()`, `http-kernel.ts:1082`).
|
|
813
850
|
- **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
|
|
814
851
|
DI et l'ALS : un flood coûte une recherche dans une table de hachage.
|
|
815
852
|
- **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
|
|
816
|
-
(`http-kernel.ts:
|
|
853
|
+
(`http-kernel.ts:388`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
|
|
817
854
|
- **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
|
|
818
855
|
route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
|
|
819
856
|
production mono-processus).
|
|
820
|
-
-
|
|
857
|
+
- **Aucune génération en production** avec un certificat fourni (`certificates.ts:330`).
|
|
821
858
|
|
|
822
859
|
Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
|
|
823
860
|
boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
|
|
@@ -832,21 +869,21 @@ demande le backplane realtime.
|
|
|
832
869
|
|
|
833
870
|
## 📜 Normes appliquées
|
|
834
871
|
|
|
835
|
-
| Domaine | Norme | Ancrage
|
|
836
|
-
| ------------------------------------- | ------------------ |
|
|
837
|
-
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:
|
|
838
|
-
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`)
|
|
839
|
-
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`)
|
|
840
|
-
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`)
|
|
841
|
-
| WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`)
|
|
842
|
-
| WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:
|
|
843
|
-
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:
|
|
844
|
-
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:
|
|
845
|
-
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:
|
|
846
|
-
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:
|
|
847
|
-
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`)
|
|
848
|
-
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:
|
|
849
|
-
| Certificat — identité par le SAN | RFC
|
|
872
|
+
| Domaine | Norme | Ancrage |
|
|
873
|
+
| ------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
|
|
874
|
+
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1093`) |
|
|
875
|
+
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
|
|
876
|
+
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
|
|
877
|
+
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
|
|
878
|
+
| WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
|
|
879
|
+
| WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:121`) |
|
|
880
|
+
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:561`) |
|
|
881
|
+
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:641`) |
|
|
882
|
+
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:585`) |
|
|
883
|
+
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:724`) |
|
|
884
|
+
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
|
|
885
|
+
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:443`) |
|
|
886
|
+
| Certificat — identité par le SAN | RFC 9525 | `sanSchema` (`config.ts:446`) |
|
|
850
887
|
|
|
851
888
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
852
889
|
|
|
@@ -854,16 +891,16 @@ demande le backplane realtime.
|
|
|
854
891
|
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
855
892
|
| 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 |
|
|
856
893
|
| 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é |
|
|
857
|
-
| `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:
|
|
894
|
+
| `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:220`) |
|
|
858
895
|
| Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
|
|
859
896
|
| 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 |
|
|
860
897
|
| 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:134`) |
|
|
861
898
|
| 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 |
|
|
862
899
|
| 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 |
|
|
863
|
-
| 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:
|
|
900
|
+
| 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:860`) |
|
|
864
901
|
| `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 |
|
|
865
902
|
| Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
|
|
866
|
-
| 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:
|
|
903
|
+
| 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:543`) |
|
|
867
904
|
| `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux |
|
|
868
905
|
| Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
|
|
869
906
|
| Handshake WebSocket refusé en `1008` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
|
|
@@ -886,7 +923,7 @@ l'origine du transport.
|
|
|
886
923
|
|
|
887
924
|
`proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
|
|
888
925
|
confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
|
|
889
|
-
résumé de certificat vient de `Certificate.describe()` (`certificates.ts:
|
|
926
|
+
résumé de certificat vient de `Certificate.describe()` (`certificates.ts:973`), source unique partagée
|
|
890
927
|
par la commande, le boot et un futur écran d'administration.
|
|
891
928
|
|
|
892
929
|
**Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
|