@nodefony/http 10.0.0-beta.2 → 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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/config/config.js +44 -4
- 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 +140 -15
- 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/upload/upload-service.js +3 -3
- 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 +1 -9
- package/dist/nodefony/src/context/http/Response.js +16 -1
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- 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/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 +16 -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 +96 -5
- 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/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/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 +5 -1
- package/docs/observabilite.md +15 -15
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +94 -60
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +23 -23
- package/package.json +8 -8
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 |
|
|
@@ -526,7 +535,7 @@ laisser un processus se croire démarré.
|
|
|
526
535
|
|
|
527
536
|
Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
|
|
528
537
|
convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
|
|
529
|
-
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:
|
|
538
|
+
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1279`) écrit donc la
|
|
530
539
|
topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
|
|
531
540
|
environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
|
|
532
541
|
aussi de la convention,
|
|
@@ -540,12 +549,12 @@ serveur qui, lui, écoute très bien.
|
|
|
540
549
|
**Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
|
|
541
550
|
n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
|
|
542
551
|
ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
|
|
543
|
-
(`Certificate.resolveStrategy()`, `certificates.ts:
|
|
552
|
+
(`Certificate.resolveStrategy()`, `certificates.ts:577`).
|
|
544
553
|
|
|
545
554
|
### Les quatre stratégies
|
|
546
555
|
|
|
547
556
|
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
|
|
548
|
-
`Certificate.resolveStrategy()` (`certificates.ts:
|
|
557
|
+
`Certificate.resolveStrategy()` (`certificates.ts:577`).
|
|
549
558
|
|
|
550
559
|
| Stratégie | Quand l'utiliser | Ce qui se passe |
|
|
551
560
|
| --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
|
|
@@ -575,7 +584,7 @@ export default defineConfig(() => ({
|
|
|
575
584
|
> [!TIP]
|
|
576
585
|
> Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
|
|
577
586
|
> 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:
|
|
587
|
+
> l'annonce et retombe sur l'auto-signé (`certificates.ts:602`).
|
|
579
588
|
|
|
580
589
|
### Ce que le chemin `explicit` évite
|
|
581
590
|
|
|
@@ -583,6 +592,31 @@ Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330
|
|
|
583
592
|
l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
|
|
584
593
|
tierce n'est installée avec `@nodefony/http`.
|
|
585
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.
|
|
619
|
+
|
|
586
620
|
### Conformité de l'auto-signé
|
|
587
621
|
|
|
588
622
|
Un certificat de développement bâclé fait perdre des heures (« not yet valid », « common name
|
|
@@ -591,15 +625,15 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
|
|
|
591
625
|
| Exigence | Norme | Mise en œuvre |
|
|
592
626
|
| --------------------------------------- | -------------------- | --------------------------------------------------------- |
|
|
593
627
|
| Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
|
|
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
|
|
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`) |
|
|
596
630
|
| `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
|
|
597
631
|
| Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
|
|
598
632
|
|
|
599
633
|
### Régénération automatique
|
|
600
634
|
|
|
601
635
|
Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
|
|
602
|
-
(`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
|
|
603
637
|
plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
|
|
604
638
|
contrôle laisserait un certificat obsolète en place indéfiniment.
|
|
605
639
|
|
|
@@ -621,7 +655,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
|
|
|
621
655
|
| `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
|
|
622
656
|
|
|
623
657
|
La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
|
|
624
|
-
`http-kernel.ts:
|
|
658
|
+
`http-kernel.ts:627`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
|
|
625
659
|
requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
|
|
626
660
|
(`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
|
|
627
661
|
|
|
@@ -629,7 +663,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
|
|
|
629
663
|
|
|
630
664
|
Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
|
|
631
665
|
kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
|
|
632
|
-
`http-kernel.ts:
|
|
666
|
+
`http-kernel.ts:1082`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
|
|
633
667
|
d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
|
|
634
668
|
filtre déjà le `Host` (`config.ts:974`).
|
|
635
669
|
|
|
@@ -638,13 +672,13 @@ filtre déjà le `Host` (`config.ts:974`).
|
|
|
638
672
|
Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
|
|
639
673
|
WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
|
|
640
674
|
par défaut que l'`Origin` du handshake corresponde au `Host` servi
|
|
641
|
-
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:
|
|
675
|
+
(`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:724`), avec tolérance loopback en développement
|
|
642
676
|
et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
|
|
643
677
|
Violation)**, jamais par un code HTTP.
|
|
644
678
|
|
|
645
679
|
> [!NOTE]
|
|
646
680
|
> Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
|
|
647
|
-
> (`http-kernel.ts:
|
|
681
|
+
> (`http-kernel.ts:642`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
|
|
648
682
|
> il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
|
|
649
683
|
|
|
650
684
|
## Probes de santé — `/livez` et `/readyz`
|
|
@@ -661,13 +695,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
|
|
|
661
695
|
protéger.
|
|
662
696
|
|
|
663
697
|
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:
|
|
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
|
|
666
700
|
rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
|
|
667
701
|
|
|
668
702
|
| Option | Type | Défaut | Effet |
|
|
669
703
|
| --------------- | ------ | --------- | ---------------------------------------------------------------------- |
|
|
670
|
-
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:
|
|
704
|
+
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:961`). |
|
|
671
705
|
| `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
|
|
672
706
|
| `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
|
|
673
707
|
| `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
|
|
@@ -731,11 +765,11 @@ sequenceDiagram
|
|
|
731
765
|
```
|
|
732
766
|
|
|
733
767
|
**É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:
|
|
768
|
+
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:587`). `readyz` répond `503`, le
|
|
735
769
|
load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
|
|
736
770
|
propager.
|
|
737
771
|
|
|
738
|
-
**É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`)
|
|
739
773
|
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
|
|
740
774
|
Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
|
|
741
775
|
indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
|
|
@@ -773,27 +807,27 @@ ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les
|
|
|
773
807
|
|
|
774
808
|
Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
|
|
775
809
|
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:
|
|
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`).
|
|
778
812
|
|
|
779
813
|
L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
|
|
780
814
|
serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
|
|
781
|
-
(`trackPong()`, `wsHeartbeat.ts:
|
|
815
|
+
(`trackPong()`, `wsHeartbeat.ts:54`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
|
|
782
816
|
horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
|
|
783
|
-
configuration pathologique (`tick`, `wsHeartbeat.ts:
|
|
817
|
+
configuration pathologique (`tick`, `wsHeartbeat.ts:136`) ; et un timer `unref` qui ne retient jamais le
|
|
784
818
|
processus à l'arrêt.
|
|
785
819
|
|
|
786
820
|
### Floods de connexions
|
|
787
821
|
|
|
788
822
|
L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
|
|
789
823
|
IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
|
|
790
|
-
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:
|
|
824
|
+
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`). Le `101` étant déjà émis par `ws`, un `429`
|
|
791
825
|
est impossible → la connexion est fermée en **1013 « Try Again Later »**
|
|
792
|
-
(`
|
|
826
|
+
(`websocketQuotaRefusal()`, `http-kernel.ts:2230`), sans
|
|
793
827
|
journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
|
|
794
828
|
|
|
795
829
|
Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
|
|
796
|
-
`wsMaxConnectionsPerIp` (`config.ts:
|
|
830
|
+
`wsMaxConnectionsPerIp` (`config.ts:1250`). En cloud-native, laisser `null` et déléguer à l'edge —
|
|
797
831
|
nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
|
|
798
832
|
descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
|
|
799
833
|
|
|
@@ -810,13 +844,13 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
|
|
|
810
844
|
par seconde. Les choix visibles dans le code :
|
|
811
845
|
|
|
812
846
|
- **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
|
|
813
|
-
socket (`wsHeartbeat.ts:
|
|
847
|
+
socket (`wsHeartbeat.ts:137`).
|
|
814
848
|
- **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:
|
|
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`).
|
|
816
850
|
- **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
|
|
817
851
|
DI et l'ALS : un flood coûte une recherche dans une table de hachage.
|
|
818
852
|
- **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
|
|
819
|
-
(`http-kernel.ts:
|
|
853
|
+
(`http-kernel.ts:388`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
|
|
820
854
|
- **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
|
|
821
855
|
route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
|
|
822
856
|
production mono-processus).
|
|
@@ -837,19 +871,19 @@ demande le backplane realtime.
|
|
|
837
871
|
|
|
838
872
|
| Domaine | Norme | Ancrage |
|
|
839
873
|
| ------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
|
|
840
|
-
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:
|
|
874
|
+
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1093`) |
|
|
841
875
|
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
|
|
842
876
|
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
|
|
843
877
|
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
|
|
844
878
|
| WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
|
|
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:
|
|
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`) |
|
|
850
884
|
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
|
|
851
|
-
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:
|
|
852
|
-
| Certificat — identité par le SAN | RFC
|
|
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`) |
|
|
853
887
|
|
|
854
888
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
855
889
|
|
|
@@ -857,13 +891,13 @@ demande le backplane realtime.
|
|
|
857
891
|
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
858
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 |
|
|
859
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é |
|
|
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:
|
|
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`) |
|
|
861
895
|
| Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
|
|
862
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 |
|
|
863
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`) |
|
|
864
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 |
|
|
865
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 |
|
|
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:
|
|
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`) |
|
|
867
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 |
|
|
868
902
|
| Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
|
|
869
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`) |
|
|
@@ -889,7 +923,7 @@ l'origine du transport.
|
|
|
889
923
|
|
|
890
924
|
`proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
|
|
891
925
|
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:
|
|
926
|
+
résumé de certificat vient de `Certificate.describe()` (`certificates.ts:973`), source unique partagée
|
|
893
927
|
par la commande, le boot et un futur écran d'administration.
|
|
894
928
|
|
|
895
929
|
**Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
|