@nodefony/http 10.0.0-beta.1 → 10.0.0-beta.2
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.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +4 -3
- package/dist/nodefony/service/certificates.js +74 -143
- package/dist/nodefony/service/http-kernel.js +2 -2
- 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-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- 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/http/Request.js +17 -4
- package/dist/nodefony/src/context/http/Response.js +30 -6
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/service/certificates.d.ts +32 -47
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/docs/cookies.md +10 -10
- package/docs/index.md +4 -2
- package/docs/observabilite.md +13 -13
- package/docs/rate-limit.md +19 -19
- package/docs/servers.md +50 -47
- package/docs/session.md +17 -17
- package/docs/upload.md +29 -29
- package/package.json +7 -10
package/docs/servers.md
CHANGED
|
@@ -128,7 +128,7 @@ 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:1848`) —, 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]
|
|
@@ -327,8 +327,8 @@ Choisir en cinq secondes :
|
|
|
327
327
|
| `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
|
|
328
328
|
| `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
|
|
329
329
|
|
|
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:
|
|
330
|
+
L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1128`) : chaque serveur est
|
|
331
|
+
consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:1139`).
|
|
332
332
|
Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
|
|
333
333
|
|
|
334
334
|
### `server-http` — HTTP/1.1 en clair
|
|
@@ -400,7 +400,7 @@ C'est la distinction la plus utile de cette page, et celle qu'on rate le plus so
|
|
|
400
400
|
| Question | Où ça se règle | Source |
|
|
401
401
|
| ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
|
|
402
402
|
| **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:
|
|
403
|
+
| **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:993`) |
|
|
404
404
|
|
|
405
405
|
Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
|
|
406
406
|
la CI et le cluster) ; le **réglage** est une propriété de l'application.
|
|
@@ -451,7 +451,7 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
|
|
|
451
451
|
|
|
452
452
|
### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
|
|
453
453
|
|
|
454
|
-
Depuis `websocketSchema` (`config.ts:
|
|
454
|
+
Depuis `websocketSchema` (`config.ts:521`). Les deux sections partagent la forme et les défauts ; le WSS
|
|
455
455
|
lit `websocketSecure` (`config.ts:1058`).
|
|
456
456
|
|
|
457
457
|
| Option | Type | Défaut | Effet |
|
|
@@ -484,7 +484,7 @@ panne, c'est une nuisance.
|
|
|
484
484
|
|
|
485
485
|
### La règle
|
|
486
486
|
|
|
487
|
-
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:
|
|
487
|
+
`resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:1767` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
|
|
488
488
|
toujours :
|
|
489
489
|
|
|
490
490
|
| Environnement | Défaut | Pourquoi |
|
|
@@ -504,9 +504,11 @@ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
|
|
|
504
504
|
Le décalage est **toujours annoncé** (`server-http.ts:130`) : jamais de dégradation silencieuse. Trois
|
|
505
505
|
détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
|
|
506
506
|
|
|
507
|
-
1. **
|
|
508
|
-
|
|
509
|
-
`
|
|
507
|
+
1. **Le repli se décide au `listen()`, qui est atomique.** Une seule sonde le précède, sur le port
|
|
508
|
+
désiré : elle détecte un serveur déjà présent sur la boucle locale, que le noyau accorderait quand
|
|
509
|
+
même à un `0.0.0.0` (`detectPortConflict`). Elle est **sautée dans un worker de cluster**
|
|
510
|
+
(`cluster.isWorker`) : le port y est partagé par le maître, et le voisin qui répond est un frère,
|
|
511
|
+
pas un intrus. La course résiduelle retombe sur `EADDRINUSE` (`bindWithFallback()`,
|
|
510
512
|
`portBinder.ts:263`).
|
|
511
513
|
2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
|
|
512
514
|
ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
|
|
@@ -524,9 +526,10 @@ laisser un processus se croire démarré.
|
|
|
524
526
|
|
|
525
527
|
Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
|
|
526
528
|
convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
|
|
527
|
-
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:
|
|
529
|
+
que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1195`) écrit donc la
|
|
528
530
|
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
|
|
531
|
+
environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
|
|
532
|
+
aussi de la convention,
|
|
530
533
|
même en `strict`. L'écriture est au mieux-effort — une image en lecture seule ne fait jamais tomber un
|
|
531
534
|
serveur qui, lui, écoute très bien.
|
|
532
535
|
|
|
@@ -537,19 +540,19 @@ serveur qui, lui, écoute très bien.
|
|
|
537
540
|
**Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
|
|
538
541
|
n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
|
|
539
542
|
ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
|
|
540
|
-
(`Certificate.resolveStrategy()`, `certificates.ts:
|
|
543
|
+
(`Certificate.resolveStrategy()`, `certificates.ts:369`).
|
|
541
544
|
|
|
542
545
|
### Les quatre stratégies
|
|
543
546
|
|
|
544
|
-
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:
|
|
545
|
-
`Certificate.resolveStrategy()` (`certificates.ts:
|
|
547
|
+
Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
|
|
548
|
+
`Certificate.resolveStrategy()` (`certificates.ts:369`).
|
|
546
549
|
|
|
547
550
|
| Stratégie | Quand l'utiliser | Ce qui se passe |
|
|
548
551
|
| --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
|
|
549
552
|
| `auto` (défaut) | On ne veut pas décider | `key`+`cert` fournis → `explicit` ; sinon mkcert en dev ; sinon auto-signé. |
|
|
550
553
|
| `explicit` | **Production** | Charge `key`/`cert`/`ca` depuis la config. Erreur au boot si absents. |
|
|
551
554
|
| `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
|
|
555
|
+
| `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé (`node:crypto`), non trusté. |
|
|
553
556
|
|
|
554
557
|
```typescript
|
|
555
558
|
// nodefony.config.ts — production : certificat fourni, jamais généré
|
|
@@ -572,13 +575,13 @@ export default defineConfig(() => ({
|
|
|
572
575
|
> [!TIP]
|
|
573
576
|
> Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
|
|
574
577
|
> 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:
|
|
578
|
+
> l'annonce et retombe sur l'auto-signé (`certificates.ts:398`).
|
|
576
579
|
|
|
577
580
|
### Ce que le chemin `explicit` évite
|
|
578
581
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330`). La génération de
|
|
583
|
+
l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
|
|
584
|
+
tierce n'est installée avec `@nodefony/http`.
|
|
582
585
|
|
|
583
586
|
### Conformité de l'auto-signé
|
|
584
587
|
|
|
@@ -588,7 +591,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
|
|
|
588
591
|
| Exigence | Norme | Mise en œuvre |
|
|
589
592
|
| --------------------------------------- | -------------------- | --------------------------------------------------------- |
|
|
590
593
|
| 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:
|
|
594
|
+
| Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:263`) |
|
|
592
595
|
| Le SAN fait foi, pas le CN | RFC 6125 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
|
|
593
596
|
| `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
|
|
594
597
|
| Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
|
|
@@ -596,7 +599,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
|
|
|
596
599
|
### Régénération automatique
|
|
597
600
|
|
|
598
601
|
Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
|
|
599
|
-
(`certificates.ts:
|
|
602
|
+
(`certificates.ts:535`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
|
|
600
603
|
plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
|
|
601
604
|
contrôle laisserait un certificat obsolète en place indéfiniment.
|
|
602
605
|
|
|
@@ -618,7 +621,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
|
|
|
618
621
|
| `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
|
|
619
622
|
|
|
620
623
|
La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
|
|
621
|
-
`http-kernel.ts:
|
|
624
|
+
`http-kernel.ts:573`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
|
|
622
625
|
requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
|
|
623
626
|
(`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
|
|
624
627
|
|
|
@@ -626,7 +629,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
|
|
|
626
629
|
|
|
627
630
|
Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
|
|
628
631
|
kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
|
|
629
|
-
`http-kernel.ts:
|
|
632
|
+
`http-kernel.ts:987`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
|
|
630
633
|
d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
|
|
631
634
|
filtre déjà le `Host` (`config.ts:974`).
|
|
632
635
|
|
|
@@ -658,13 +661,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
|
|
|
658
661
|
protéger.
|
|
659
662
|
|
|
660
663
|
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:
|
|
664
|
+
(`http-kernel.ts:1009`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
|
|
665
|
+
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:508`). Et surtout : **avant le
|
|
663
666
|
rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
|
|
664
667
|
|
|
665
668
|
| Option | Type | Défaut | Effet |
|
|
666
669
|
| --------------- | ------ | --------- | ---------------------------------------------------------------------- |
|
|
667
|
-
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:
|
|
670
|
+
| `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:944`). |
|
|
668
671
|
| `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
|
|
669
672
|
| `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
|
|
670
673
|
| `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
|
|
@@ -728,7 +731,7 @@ sequenceDiagram
|
|
|
728
731
|
```
|
|
729
732
|
|
|
730
733
|
**É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:
|
|
734
|
+
inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:550`). `readyz` répond `503`, le
|
|
732
735
|
load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
|
|
733
736
|
propager.
|
|
734
737
|
|
|
@@ -784,9 +787,9 @@ processus à l'arrêt.
|
|
|
784
787
|
|
|
785
788
|
L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
|
|
786
789
|
IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
|
|
787
|
-
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:
|
|
790
|
+
(`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1848`). Le `101` étant déjà émis par `ws`, un `429`
|
|
788
791
|
est impossible → la connexion est fermée en **1013 « Try Again Later »**
|
|
789
|
-
(`rateLimiter`, `http-kernel.ts:
|
|
792
|
+
(`rateLimiter`, `http-kernel.ts:1863`), sans
|
|
790
793
|
journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
|
|
791
794
|
|
|
792
795
|
Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
|
|
@@ -817,7 +820,7 @@ par seconde. Les choix visibles dans le code :
|
|
|
817
820
|
- **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
|
|
818
821
|
route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
|
|
819
822
|
production mono-processus).
|
|
820
|
-
-
|
|
823
|
+
- **Aucune génération en production** avec un certificat fourni (`certificates.ts:330`).
|
|
821
824
|
|
|
822
825
|
Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
|
|
823
826
|
boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
|
|
@@ -832,21 +835,21 @@ demande le backplane realtime.
|
|
|
832
835
|
|
|
833
836
|
## 📜 Normes appliquées
|
|
834
837
|
|
|
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:113`)
|
|
843
|
-
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`)
|
|
844
|
-
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`)
|
|
845
|
-
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`)
|
|
846
|
-
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:621`)
|
|
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 6125 | `sanSchema` (`config.ts:
|
|
838
|
+
| Domaine | Norme | Ancrage |
|
|
839
|
+
| ------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
|
|
840
|
+
| HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1009`) |
|
|
841
|
+
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
|
|
842
|
+
| HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
|
|
843
|
+
| En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
|
|
844
|
+
| 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:113`) |
|
|
846
|
+
| WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`) |
|
|
847
|
+
| WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`) |
|
|
848
|
+
| WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`) |
|
|
849
|
+
| CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:621`) |
|
|
850
|
+
| En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
|
|
851
|
+
| Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:263`) |
|
|
852
|
+
| Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:446`) |
|
|
850
853
|
|
|
851
854
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
852
855
|
|
|
@@ -863,7 +866,7 @@ demande le backplane realtime.
|
|
|
863
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:848`) |
|
|
864
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 |
|
|
865
868
|
| 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:
|
|
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:543`) |
|
|
867
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 |
|
|
868
871
|
| Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
|
|
869
872
|
| Handshake WebSocket refusé en `1008` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
|
|
@@ -886,7 +889,7 @@ l'origine du transport.
|
|
|
886
889
|
|
|
887
890
|
`proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
|
|
888
891
|
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:
|
|
892
|
+
résumé de certificat vient de `Certificate.describe()` (`certificates.ts:758`), source unique partagée
|
|
890
893
|
par la commande, le boot et un futur écran d'administration.
|
|
891
894
|
|
|
892
895
|
**Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
|
package/docs/session.md
CHANGED
|
@@ -69,8 +69,8 @@ flowchart TD
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Le point d'activation est **unique et commun aux deux transports** : `HttpKernel.startSession()`
|
|
72
|
-
(`http-kernel.ts:
|
|
73
|
-
(`http-kernel.ts:
|
|
72
|
+
(`http-kernel.ts:1246`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
|
|
73
|
+
(`http-kernel.ts:1283`) — sans intent de route ni cookie entrant, **aucune session n'est ouverte**.
|
|
74
74
|
|
|
75
75
|
## 📖 Lexique
|
|
76
76
|
|
|
@@ -114,8 +114,8 @@ défaut** dans Nodefony :
|
|
|
114
114
|
|
|
115
115
|
- **Vol du cookie (hijacking).** Un script injecté (XSS) ou un réseau en clair capte le cookie et
|
|
116
116
|
rejoue la session. → `HttpOnly` et `Secure` sont à `true` par défaut (`sessionCookieSchema`,
|
|
117
|
-
`config.ts:
|
|
118
|
-
(`Context.getSessionCookieName()`, `Context.ts:
|
|
117
|
+
`config.ts:752-755`), et le nom du cookie prend le préfixe `__Host-` dès que le transport est TLS
|
|
118
|
+
(`Context.getSessionCookieName()`, `Context.ts:733`).
|
|
119
119
|
- **Fixation.** L'attaquant pose lui-même un identifiant dans le navigateur de la victime, attend
|
|
120
120
|
qu'elle se connecte, puis réutilise **le même** identifiant. → double défense : `strictMode` rejette
|
|
121
121
|
tout identifiant inconnu du store (`Session.resume()`, `session.ts:177`), et le login **régénère**
|
|
@@ -140,11 +140,11 @@ agents (voir [Firewall](../../security/docs/firewall.md)).
|
|
|
140
140
|
|
|
141
141
|
**2. La session est paresseuse.** Elle n'existe que si une route la demande — `@UseSession`, ou la
|
|
142
142
|
seule présence d'un paramètre `@Session` — ou si un cookie arrive déjà : c'est la garde de
|
|
143
|
-
`HttpKernel.startSession()` (`http-kernel.ts:
|
|
143
|
+
`HttpKernel.startSession()` (`http-kernel.ts:1246`). Une route publique ne paie **ni lecture de store,
|
|
144
144
|
ni `Set-Cookie`**.
|
|
145
145
|
|
|
146
146
|
**3. Un seul modèle d'état pour le web et le temps réel.** Le même `startSession()` sert
|
|
147
|
-
`HttpKernel.onRequestEnd()` (`http-kernel.ts:
|
|
147
|
+
`HttpKernel.onRequestEnd()` (`http-kernel.ts:1697`) et `HttpKernel.onConnect()` (`http-kernel.ts:2008`) ;
|
|
148
148
|
l'activité HTTP **ou** WS prolonge la même session (`Session.touchIfNeeded()`, `session.ts:421`).
|
|
149
149
|
|
|
150
150
|
**4. L'administration ne voit jamais un identifiant.** Un opérateur manipule une `ref`, HMAC tronqué
|
|
@@ -274,12 +274,12 @@ curl -sk -b /tmp/jar https://localhost:5152/panier
|
|
|
274
274
|
Le cookie obtenu porte `__Host-`, `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/` et **aucun** `Domain` :
|
|
275
275
|
c'est exactement ce qu'assert le banc d'intégration `session-runtime` (« Set-Cookie de session sur TLS »).
|
|
276
276
|
Sur un transport en clair (port 5151), le préfixe `__Host-` est omis — le navigateur le rejetterait
|
|
277
|
-
faute de `Secure` (`Context.getSessionCookieName()`, `Context.ts:
|
|
277
|
+
faute de `Secure` (`Context.getSessionCookieName()`, `Context.ts:733`).
|
|
278
278
|
|
|
279
279
|
## ⚙️ Configuration
|
|
280
280
|
|
|
281
|
-
Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:
|
|
282
|
-
`sessionCookieSchema` (`config.ts:
|
|
281
|
+
Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:786`) et son sous-schéma
|
|
282
|
+
`sessionCookieSchema` (`config.ts:752`).
|
|
283
283
|
|
|
284
284
|
| Option | Type | Défaut | Effet |
|
|
285
285
|
| ------------------- | ------- | ------------ | ---------------------------------------------------------------------------------------- |
|
|
@@ -503,14 +503,14 @@ C'est le différenciateur du framework appliqué à l'état de session : un seul
|
|
|
503
503
|
<!-- prettier-ignore -->
|
|
504
504
|
| Aspect | HTTP | WebSocket |
|
|
505
505
|
| --- | --- | --- |
|
|
506
|
-
| Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:
|
|
506
|
+
| Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:1697`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts:2008`) |
|
|
507
507
|
| Lecture du cookie | constructeur du contexte | constructeur, même nom effectif (`WebsocketContext.ts:172`) |
|
|
508
508
|
| Sauvegarde | fin de requête | après **chaque frame** traitée (`WebsocketContext.ts:302`) |
|
|
509
|
-
| Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:
|
|
509
|
+
| Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:1801`) |
|
|
510
510
|
| Portée ALS | une requête | **handshake + toutes les frames** (`http-kernel.ts:1495`) |
|
|
511
511
|
|
|
512
512
|
La conséquence pratique la plus utile : côté WebSocket, la bulle `AsyncLocalStorage` ouverte au
|
|
513
|
-
handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:
|
|
513
|
+
handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:1929`). L'identité résolue une fois est donc
|
|
514
514
|
disponible à chaque frame sans relire la base — c'est ce dont profite
|
|
515
515
|
`FirewallRealtimeAuthenticator.supports()` (`FirewallRealtimeAuthenticator.ts:80`), câblé automatiquement
|
|
516
516
|
par le firewall sur les zones temps réel protégées (`firewall.ts:297`).
|
|
@@ -572,8 +572,8 @@ Trois barrières superposées :
|
|
|
572
572
|
|
|
573
573
|
| Menace | Défense | Ancrage |
|
|
574
574
|
| --------------------------------- | ------------------------------------------------- | -------------------------------------------------- |
|
|
575
|
-
| Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts:
|
|
576
|
-
| Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts:
|
|
575
|
+
| Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts:752`) |
|
|
576
|
+
| Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts:733`) |
|
|
577
577
|
| Requête inter-sites | `SameSite=Lax` par défaut | `cookieDefaultSettings` (`cookie.ts:39`) |
|
|
578
578
|
| Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts:177`) |
|
|
579
579
|
| Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts:231`) |
|
|
@@ -643,7 +643,7 @@ Trois règles de conception se dégagent du contrat, et méritent d'être respec
|
|
|
643
643
|
| Domaine | Norme | Comment le code s'y conforme |
|
|
644
644
|
| ----------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
|
|
645
645
|
| Attributs et préfixes cookie | RFC 6265bis §4.1.3 | `__Host-` impose `Secure` + `Path=/`, interdit `Domain` (`cookie.ts:386-403`) |
|
|
646
|
-
| Nom du cookie selon transport | RFC 6265bis / OWASP | `getSessionCookieName()` (`Context.ts:
|
|
646
|
+
| Nom du cookie selon transport | RFC 6265bis / OWASP | `getSessionCookieName()` (`Context.ts:733`) |
|
|
647
647
|
| Idle timeout | NIST SP 800-63B-4 / OWASP | défaut 1800 s, glissant par `touch` (`config.ts:831`) |
|
|
648
648
|
| Absolute timeout | NIST SP 800-63B-4 / OWASP | défaut 43200 s, jamais prolongé (`config.ts:808`) |
|
|
649
649
|
| Identifiant de session | OWASP Session Management | 32 octets CSPRNG, opaque (`session.ts:226`) |
|
|
@@ -656,7 +656,7 @@ Trois règles de conception se dégagent du contrat, et méritent d'être respec
|
|
|
656
656
|
Le coût d'une session est **payé seulement quand elle sert** :
|
|
657
657
|
|
|
658
658
|
- **Zéro par défaut** — sans intent ni cookie, `startSession()` sort immédiatement
|
|
659
|
-
(`http-kernel.ts:
|
|
659
|
+
(`http-kernel.ts:1246`) : ni objet `Session`, ni lecture de store.
|
|
660
660
|
- **Objet léger** — trois sacs `{}` à plat, pas de container DI par session (`session.ts:100-104`).
|
|
661
661
|
- **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts:255`) ; le
|
|
662
662
|
`touch` est throttlé à une écriture par demi-vie d'idle (`session.ts:445`).
|
|
@@ -719,7 +719,7 @@ SQLite quand c'est pertinent (`SessionStorage.location`,
|
|
|
719
719
|
|
|
720
720
|
| Symptôme | Cause | Correction |
|
|
721
721
|
| ------------------------------------------------------ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
722
|
-
| Aucun `Set-Cookie`, `session` toujours vide | La route n'a **aucun intent** : ni `@UseSession`, ni paramètre `@Session` | Ajouter l'un des deux — l'activation est paresseuse (`http-kernel.ts:
|
|
722
|
+
| Aucun `Set-Cookie`, `session` toujours vide | La route n'a **aucun intent** : ni `@UseSession`, ni paramètre `@Session` | Ajouter l'un des deux — l'activation est paresseuse (`http-kernel.ts:1283`) |
|
|
723
723
|
| `context.session` est `null` dans un contrôleur WS | Intent posé sur la classe au lieu de la route, ou absent | `@UseSession()` **sur la route** WebSocket ; `Controller.startSession()` n'existe plus |
|
|
724
724
|
| Mutation ignorée, WARNING « READONLY SESSION mutated » | La route est en `@UseSession({ readOnly: true })` | Retirer `readOnly` sur les routes qui écrivent (`session.ts:255-264`) |
|
|
725
725
|
| Session détruite qui « revient » après logout | Une requête en vol réécrit le blob supprimé | Déjà couvert : pierre tombale 5 min (`RevocationGuardStorage.ts:121`) |
|
package/docs/upload.md
CHANGED
|
@@ -62,10 +62,10 @@ flowchart TD
|
|
|
62
62
|
|
|
63
63
|
Trois idées portent tout le reste :
|
|
64
64
|
|
|
65
|
-
1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:
|
|
65
|
+
1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:513`)
|
|
66
66
|
aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / parser brut selon l'en-tête.
|
|
67
67
|
2. **Seul le multipart écrit sur disque.** Les fichiers sont **streamés** au fil de l'eau vers un
|
|
68
|
-
fichier temporaire (`parseMultipart()`, `context/http/Request.ts:
|
|
68
|
+
fichier temporaire (`parseMultipart()`, `context/http/Request.ts:585`) — jamais bufferisés en RAM.
|
|
69
69
|
JSON / urlencoded / XML restent en mémoire (petits corps).
|
|
70
70
|
3. **Deux budgets distincts, jamais confondus.** Le corps **non-multipart** est borné par `maxBodySize`
|
|
71
71
|
(défaut **1 MiB**) ; le multipart a **ses propres** limites busboy (`maxFileSize`, `maxTotalFileSize`,
|
|
@@ -117,16 +117,16 @@ Trois choix structurent l'implémentation, et chacun a une raison de sécurité
|
|
|
117
117
|
|
|
118
118
|
**Le multipart ne touche jamais la RAM.** Là où l'ancien chemin bufferisait le corps entier, busboy lit
|
|
119
119
|
le flux et écrit chaque fichier au fil de l'eau dans le dossier temporaire (`streamMultipart()`,
|
|
120
|
-
`context/http/Request.ts:
|
|
120
|
+
`context/http/Request.ts:626`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
|
|
121
121
|
champs texte restent en mémoire. C'est ce qui rend un endpoint d'upload public tenable.
|
|
122
122
|
|
|
123
123
|
**Le nom du fichier temporaire n'est jamais celui du client.** Chaque fichier reçu est écrit sous un nom
|
|
124
|
-
`randomUUID()` + extension d'origine (`context/http/Request.ts:
|
|
124
|
+
`randomUUID()` + extension d'origine (`context/http/Request.ts:683`) : un nom malveillant
|
|
125
125
|
(`../../etc/passwd`) ne peut pas influencer le **chemin** d'écriture. Le nom d'origine est conservé en
|
|
126
126
|
**métadonnée** (`filename`), pas dans le chemin.
|
|
127
127
|
|
|
128
128
|
**Deux budgets, secure-by-default.** Le corps non-multipart est plafonné à **1 MiB** par défaut
|
|
129
|
-
(`maxBodySize`, `http/nodefony/config/config.ts:
|
|
129
|
+
(`maxBodySize`, `http/nodefony/config/config.ts:1027`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
|
|
130
130
|
multipart, lui, a ses propres bornes busboy (par fichier, cumul, nombre) qui coupent le flux et
|
|
131
131
|
nettoient les temporaires déjà posés au moindre dépassement (`context/http/Request.ts:481`).
|
|
132
132
|
|
|
@@ -257,19 +257,19 @@ sequenceDiagram
|
|
|
257
257
|
Les points d'implémentation qui expliquent des comportements surprenants :
|
|
258
258
|
|
|
259
259
|
1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
|
|
260
|
-
(`context/http/Request.ts:
|
|
260
|
+
(`context/http/Request.ts:513`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
|
|
261
261
|
des méthodes parsées (`context/http/Request.ts:94`) — l'oubli laissait tout `PATCH` avec un corps vide.
|
|
262
262
|
2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
|
|
263
|
-
(`context/http/Request.ts:
|
|
263
|
+
(`context/http/Request.ts:626`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
|
|
264
264
|
qu'une fois tous les fichiers fermés (`bb.on("finish")`, `context/http/Request.ts:707`).
|
|
265
265
|
3. **Une limite dépassée nettoie les temporaires déjà posés** — `abort()`
|
|
266
|
-
(`context/http/Request.ts:
|
|
267
|
-
déjà écrits (`context/http/Request.ts:
|
|
266
|
+
(`context/http/Request.ts:646`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
|
|
267
|
+
déjà écrits (`context/http/Request.ts:64`) avant de rejeter en `413` : pas d'orphelins sur le disque.
|
|
268
268
|
4. **Les autres formats drainent AVANT de concaténer** — la base `Parser.parse()` attend `end`
|
|
269
269
|
(`context/http/parser.ts:111`) avant `Buffer.concat` : sans ce drain, `ParserQs`/`ParserXml`
|
|
270
270
|
lisaient un corps partiel → `queryPost` vide (bug de régression, cf tests).
|
|
271
271
|
5. **Un multipart sans boundary exploitable ne crashe pas** — `new Busboy()` lève **synchroniquement** ;
|
|
272
|
-
c'est rattrapé et on bascule sur le `Parser` brut (`context/http/Request.ts:
|
|
272
|
+
c'est rattrapé et on bascule sur le `Parser` brut (`context/http/Request.ts:417`).
|
|
273
273
|
|
|
274
274
|
## ⚙️ Configuration — deux budgets, jamais confondus
|
|
275
275
|
|
|
@@ -283,10 +283,10 @@ non-multipart n'écoute pas `upload.*`.
|
|
|
283
283
|
| `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
|
|
284
284
|
|
|
285
285
|
Deux rideaux, tous deux `runtimeMutable` (éditable à chaud) : un **pré-check** sur `Content-Length`
|
|
286
|
-
qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:
|
|
286
|
+
qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:490`), puis un **compteur
|
|
287
287
|
en streaming** qui coupe le socket si le corps déborde sans `Content-Length` honnête — chunked ou
|
|
288
288
|
menteur (`Parser.write()`, `context/http/parser.ts:33`, dépassement `context/http/parser.ts:33`).
|
|
289
|
-
Champ `maxBodySize` du schéma : `config/config.ts:
|
|
289
|
+
Champ `maxBodySize` du schéma : `config/config.ts:1027`.
|
|
290
290
|
|
|
291
291
|
**Par requête** : `maxBodySize` et les limites `upload.*` (hors `uploadDir`) sont sur la liste blanche
|
|
292
292
|
du calque de configuration — une application peut les changer pour une requête
|
|
@@ -300,14 +300,14 @@ Table dérivée de `uploadSchema` (`config/config.ts:159`). Toutes les bornes so
|
|
|
300
300
|
|
|
301
301
|
| Option | Type | Défaut | Effet |
|
|
302
302
|
| ------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
|
|
303
|
-
| `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts:
|
|
304
|
-
| `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts:
|
|
305
|
-
| `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts:
|
|
306
|
-
| `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts:
|
|
307
|
-
| `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts:
|
|
308
|
-
| `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts:
|
|
309
|
-
| `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts:
|
|
310
|
-
| `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts:
|
|
303
|
+
| `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts:162`). |
|
|
304
|
+
| `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts:171`). |
|
|
305
|
+
| `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts:183`). |
|
|
306
|
+
| `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts:195`). |
|
|
307
|
+
| `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts:206`). |
|
|
308
|
+
| `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts:217`). |
|
|
309
|
+
| `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts:228`). |
|
|
310
|
+
| `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts:235`). |
|
|
311
311
|
|
|
312
312
|
> [!WARNING]
|
|
313
313
|
> `uploadDir` vide est **résolu au boot** sur le répertoire temporaire du kernel par le builder de config
|
|
@@ -376,9 +376,9 @@ piégé. Les défenses en place, et **ce qui reste à ta charge**.
|
|
|
376
376
|
<!-- prettier-ignore -->
|
|
377
377
|
| Menace | Défense côté framework | À ta charge |
|
|
378
378
|
| --- | --- | --- |
|
|
379
|
-
| **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:
|
|
379
|
+
| **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:683`). | La **destination** de `move()` (voir avertissement). |
|
|
380
380
|
| **Saturation RAM** | Multipart streamé (jamais bufferisé) ; corps non-multipart borné (`maxBodySize`). | Resserrer `maxBodySize` selon l'endpoint. |
|
|
381
|
-
| **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts:
|
|
381
|
+
| **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts:646`). | Purger les temp non déplacés (TTL / cron). |
|
|
382
382
|
| **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts:754`). | — |
|
|
383
383
|
| **Type de fichier hostile** | `mimeType` **déclaré** est exposé tel quel. | Valider le type/contenu réel (le MIME client est déclaratif). |
|
|
384
384
|
|
|
@@ -396,7 +396,7 @@ Le parsing du corps est sur le chemin de chaque requête écrivante — les choi
|
|
|
396
396
|
- **Multipart en flux pur** — plus de double-bufferisation : busboy écrit sur disque au fil de l'eau,
|
|
397
397
|
seuls les champs texte restent en mémoire (`context/http/Request.ts:400`).
|
|
398
398
|
- **Rejet AVANT lecture** — le pré-check `Content-Length` renvoie `413` sans lire un octet
|
|
399
|
-
(`context/http/Request.ts:
|
|
399
|
+
(`context/http/Request.ts:466`) ; le compteur streaming **libère immédiatement** la RAM déjà
|
|
400
400
|
bufferisée au dépassement (`context/http/parser.ts:43`).
|
|
401
401
|
- **`stat` non bloquant** — `UploadedFile.create()` résout les stats du fichier en async
|
|
402
402
|
(`service/upload/upload-service.ts:130`), plus de `lstatSync` par fichier uploadé.
|
|
@@ -412,12 +412,12 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
|
|
|
412
412
|
|
|
413
413
|
| Domaine | Norme | Ancrage |
|
|
414
414
|
| ---------------------------------- | -------------------------------- | ------------------------------------------------------------- |
|
|
415
|
-
| Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:
|
|
416
|
-
| Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:
|
|
415
|
+
| Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:585`) |
|
|
416
|
+
| Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:490`) |
|
|
417
417
|
| 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts:33`) |
|
|
418
418
|
| Bornes multipart → 413 | RFC 9110 §15.5.14 | `stream.on("limit")` (`context/http/Request.ts:481`) |
|
|
419
|
-
| Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts:
|
|
420
|
-
| Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:
|
|
419
|
+
| Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts:683`) |
|
|
420
|
+
| Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:886`) |
|
|
421
421
|
|
|
422
422
|
## ⚠️ Pièges
|
|
423
423
|
|
|
@@ -430,8 +430,8 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
|
|
|
430
430
|
| Un fichier écrit `../../etc/…` après un `move` | `move(dossier)` utilise le nom **client** (`upload-service.ts:197`) | Passer une cible complète, ou `path.basename(file.filename)` |
|
|
431
431
|
| Des fichiers temporaires s'accumulent dans `uploadDir` | Le contrôleur ne déplace jamais le temp | Appeler `moveAsync()` (ou purger l'ancien temp par TTL) |
|
|
432
432
|
| `queryPost` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts:93`) |
|
|
433
|
-
| Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:
|
|
434
|
-
| `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:
|
|
433
|
+
| Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:886`) |
|
|
434
|
+
| `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:521`) |
|
|
435
435
|
|
|
436
436
|
## 🧪 Tests & couverture
|
|
437
437
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/http",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
4
4
|
"description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
|
|
5
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -56,8 +56,7 @@
|
|
|
56
56
|
"http-terminator": "3.2.0",
|
|
57
57
|
"mime-types": "3.0.2",
|
|
58
58
|
"ms": "^2.1.3",
|
|
59
|
-
"
|
|
60
|
-
"qs": "^6.15.3",
|
|
59
|
+
"qs": "^6.16.0",
|
|
61
60
|
"serve-static": "2.2.1",
|
|
62
61
|
"tslib": "2.8.1",
|
|
63
62
|
"ws": "8.22.0",
|
|
@@ -68,23 +67,21 @@
|
|
|
68
67
|
"@types/mime-types": "3.0.1",
|
|
69
68
|
"@types/ms": "2.1.0",
|
|
70
69
|
"@types/node": "26.6.3",
|
|
71
|
-
"@types/node-forge": "1.3.14",
|
|
72
70
|
"@types/qs": "6.15.1",
|
|
73
71
|
"@types/serve-static": "2.2.0",
|
|
74
72
|
"@types/websocket": "1.0.10",
|
|
75
|
-
"@types/ws": "8.18.
|
|
73
|
+
"@types/ws": "8.18.2",
|
|
76
74
|
"@types/xml2js": "0.4.14",
|
|
77
|
-
"@vitest/coverage-v8": "5.0.
|
|
78
|
-
"
|
|
79
|
-
"nodefony": "^10.0.0-beta.1",
|
|
75
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
76
|
+
"nodefony": "^10.0.0-beta.2",
|
|
80
77
|
"tsx": "4.23.15",
|
|
81
|
-
"vitest": "5.0.
|
|
78
|
+
"vitest": "5.0.3"
|
|
82
79
|
},
|
|
83
80
|
"license": "Apache-2.0",
|
|
84
81
|
"readmeFilename": "README.md",
|
|
85
82
|
"contributors": [],
|
|
86
83
|
"peerDependencies": {
|
|
87
|
-
"nodefony": "^10.0.0-beta.
|
|
84
|
+
"nodefony": "^10.0.0-beta.2",
|
|
88
85
|
"zod": "^4.6.5"
|
|
89
86
|
},
|
|
90
87
|
"files": [
|