@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/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:502`)
|
|
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:574`) — 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:615`). 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:672`) : 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:1208`) — 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,10 +257,10 @@ 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:502`). `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:615`) 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
266
|
(`context/http/Request.ts:646`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
|
|
@@ -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:479`), 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:1208`.
|
|
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
|
|
@@ -322,16 +322,16 @@ recommandé) et les **getters** de `Controller` (impératif). Les signatures exa
|
|
|
322
322
|
|
|
323
323
|
| Accès | Source lue | Ancrage |
|
|
324
324
|
| -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
|
|
325
|
-
| `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:
|
|
326
|
-
| `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:
|
|
325
|
+
| `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1312`) |
|
|
326
|
+
| `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1311`) |
|
|
327
327
|
| `@Body() body` | tous les champs parsés (`queryPost`) | `resolveParamArg` `"body"` (`routerDecorators.ts:1232`) |
|
|
328
|
-
| `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:
|
|
329
|
-
| `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:
|
|
330
|
-
| `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:
|
|
331
|
-
| `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:
|
|
328
|
+
| `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1279`) |
|
|
329
|
+
| `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1358`) |
|
|
330
|
+
| `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:354`) |
|
|
331
|
+
| `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:364`) |
|
|
332
332
|
|
|
333
333
|
Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
|
|
334
|
-
(`routerDecorators.ts:
|
|
334
|
+
(`routerDecorators.ts:1312`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
|
|
335
335
|
`IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
|
|
336
336
|
|
|
337
337
|
### Un fichier uploadé — `UploadedFile`
|
|
@@ -357,7 +357,7 @@ destination est bâtie avec `filename` — d'où l'avertissement de sécurité c
|
|
|
357
357
|
Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
|
|
358
358
|
par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
|
|
359
359
|
`Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
|
|
360
|
-
(`routerDecorators.ts:
|
|
360
|
+
(`routerDecorators.ts:1509`), mémoïsé sur la route.
|
|
361
361
|
|
|
362
362
|
```ts
|
|
363
363
|
// fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
|
|
@@ -376,7 +376,7 @@ 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:672`). | 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
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`). | — |
|
|
@@ -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:574`) |
|
|
416
|
+
| Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:479`) |
|
|
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:672`) |
|
|
420
|
+
| Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:875`) |
|
|
421
421
|
|
|
422
422
|
## ⚠️ Pièges
|
|
423
423
|
|
|
@@ -430,7 +430,7 @@ 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:
|
|
433
|
+
| Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:875`) |
|
|
434
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
|
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.3",
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"test": "./nodefony/tests"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@fastify/busboy": "3.2.
|
|
54
|
+
"@fastify/busboy": "3.2.3",
|
|
55
55
|
"cookie": "2.0.1",
|
|
56
56
|
"http-terminator": "3.2.0",
|
|
57
57
|
"mime-types": "3.0.2",
|
|
@@ -66,23 +66,23 @@
|
|
|
66
66
|
"@types/chai": "5.2.3",
|
|
67
67
|
"@types/mime-types": "3.0.1",
|
|
68
68
|
"@types/ms": "2.1.0",
|
|
69
|
-
"@types/node": "26.6.
|
|
69
|
+
"@types/node": "26.6.5",
|
|
70
70
|
"@types/qs": "6.15.1",
|
|
71
71
|
"@types/serve-static": "2.2.0",
|
|
72
72
|
"@types/websocket": "1.0.10",
|
|
73
73
|
"@types/ws": "8.18.2",
|
|
74
74
|
"@types/xml2js": "0.4.14",
|
|
75
|
-
"@vitest/coverage-v8": "5.0.
|
|
76
|
-
"nodefony": "^10.0.0-beta.
|
|
75
|
+
"@vitest/coverage-v8": "5.0.4",
|
|
76
|
+
"nodefony": "^10.0.0-beta.3",
|
|
77
77
|
"tsx": "4.23.15",
|
|
78
|
-
"vitest": "5.0.
|
|
78
|
+
"vitest": "5.0.4"
|
|
79
79
|
},
|
|
80
80
|
"license": "Apache-2.0",
|
|
81
81
|
"readmeFilename": "README.md",
|
|
82
82
|
"contributors": [],
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"nodefony": "^10.0.0-beta.
|
|
85
|
-
"zod": "^4.6.
|
|
84
|
+
"nodefony": "^10.0.0-beta.3",
|
|
85
|
+
"zod": "^4.6.7"
|
|
86
86
|
},
|
|
87
87
|
"files": [
|
|
88
88
|
"dist",
|