@nodefony/http 10.0.0-alpha.1

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.
Files changed (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,543 @@
1
+ import HttpError from "../../errors/httpError.js";
2
+ import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
3
+ import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
4
+ import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
5
+ import { extend } from "nodefony";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { URL } from "node:url";
8
+ import fs from "node:fs";
9
+ import fsp from "node:fs/promises";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import QS from "qs";
13
+ import { Busboy } from "@fastify/busboy";
14
+ //#region nodefony/src/context/http/Request.ts
15
+ const reg = /(.*)[[][\]]$/u;
16
+ /**
17
+ * Supprime un fichier temporaire d'upload sans jamais faire échouer le nettoyage :
18
+ * le fichier peut déjà avoir disparu, et l'erreur d'origine qu'on est en train de
19
+ * remonter doit rester celle que l'appelant voit. Définie au niveau du module —
20
+ * une seule allocation pour tout le processus, jamais une par requête.
21
+ */
22
+ const unlinkQuiet = (p) => fsp.unlink(p).catch(() => void 0);
23
+ var BodyHandled = class {};
24
+ const BODY_DONE = new BodyHandled();
25
+ const CHARSET_ALIASES = {
26
+ "utf-8": "utf8",
27
+ utf8: "utf8",
28
+ "iso-8859-1": "latin1",
29
+ latin1: "latin1",
30
+ "us-ascii": "ascii",
31
+ ascii: "ascii",
32
+ "ucs-2": "ucs2",
33
+ "utf-16le": "utf16le"
34
+ };
35
+ const QUOTE_TRIM = /^["']|["']$/gu;
36
+ const parse = {
37
+ POST: true,
38
+ PUT: true,
39
+ PATCH: true,
40
+ DELETE: true
41
+ };
42
+ const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
43
+ function perfMark(field) {
44
+ const p = globalThis.__nfPerfProbe;
45
+ if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
46
+ }
47
+ var HttpRequest = class {
48
+ context;
49
+ request;
50
+ #url = null;
51
+ headers = {};
52
+ host = "";
53
+ method;
54
+ contentType;
55
+ rawContentType = {};
56
+ domain;
57
+ remoteAddress = "";
58
+ hostname;
59
+ sUrl;
60
+ /**
61
+ * Scheme effectif côté client (`http` | `https`, ou le proto `Forwarded`
62
+ * brut derrière un proxy de confiance) — posé par {@link getFullUrl}, seule
63
+ * source de cette décision. `HttpContext.setScheme` le lit sans toucher à
64
+ * l'URL.
65
+ */
66
+ scheme = "http";
67
+ /**
68
+ * Pathname de la requête, TOUJOURS sous forme normalisée WHATWG : découpe
69
+ * du target quand elle est prouvablement identique au parse (fast-path),
70
+ * `URL.pathname` sinon. C'est la valeur que consomment le router et le
71
+ * matching de zones du firewall — sans jamais construire l'URL.
72
+ */
73
+ pathname = "";
74
+ /** Query string au format `URL.search` (`?…`, ou `""`) — même provenance. */
75
+ search = "";
76
+ /** L'objet {@link url} a-t-il déjà été construit ? (diagnostic/sonde). */
77
+ get hasParsedUrl() {
78
+ return this.#url !== null;
79
+ }
80
+ /**
81
+ * Sérialisation de l'URL de la requête — `URL.href` si l'objet existe,
82
+ * sinon `sUrl` (identique par construction sur le fast-path). Les lecteurs
83
+ * descriptifs (logs, profiler, `context.url`) passent par ici : 0 parse.
84
+ */
85
+ get href() {
86
+ return this.#url === null ? this.sUrl : this.#url.href;
87
+ }
88
+ /**
89
+ * URL WHATWG complète de la requête, construite au premier accès (fast-path)
90
+ * ou dès le ctor (bail-out). Porte `query` (le GET parsé) comme toujours.
91
+ */
92
+ get url() {
93
+ let u = this.#url;
94
+ if (u === null) {
95
+ u = this.getUrl(this.sUrl);
96
+ u.query = this.queryGet;
97
+ this.#url = u;
98
+ }
99
+ return u;
100
+ }
101
+ set url(u) {
102
+ this.#url = u;
103
+ }
104
+ parser = null;
105
+ queryPost = {};
106
+ queryGet = {};
107
+ queryFile = [];
108
+ query = {};
109
+ /**
110
+ * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
111
+ * Fastify, NestJS), alias de {@link queryPost}.
112
+ *
113
+ * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
114
+ * corps : `body` ne rend jamais un paramètre d'URL. Le data plane admin
115
+ * expose déjà cette sémantique (`IAdminRequest.body`) ; ce getter la rend
116
+ * disponible partout.
117
+ *
118
+ * Getter (aucune allocation) : `queryPost` reste la source unique.
119
+ */
120
+ get body() {
121
+ return this.queryPost;
122
+ }
123
+ queryStringOptions;
124
+ charset = "utf8";
125
+ uploadOption = {};
126
+ maxBodySize = 0;
127
+ data = Buffer.alloc(0);
128
+ #accept = null;
129
+ #acceptHtml = null;
130
+ /** Media-ranges de l'en-tête `Accept`, parsés au premier accès (mémoïsé). */
131
+ get accept() {
132
+ if (this.#accept === null) this.#accept = acceptParser(this.headers?.accept);
133
+ return this.#accept;
134
+ }
135
+ /** `true` si le client accepte `text/html` — résolu au premier accès (mémoïsé). */
136
+ get acceptHtml() {
137
+ if (this.#acceptHtml === null) this.#acceptHtml = this.accepts("html");
138
+ return this.#acceptHtml;
139
+ }
140
+ origin;
141
+ trustedProxy = false;
142
+ forwarded = null;
143
+ constructor(request, context) {
144
+ if (PERF_PROBE_SUB) perfMark("reqStartNs");
145
+ this.request = request;
146
+ this.context = context;
147
+ this.request.body = null;
148
+ this.headers = request.headers;
149
+ this.origin = this.headers.origin;
150
+ const checker = this.context?.httpKernel?.getTrustProxyChecker();
151
+ const socketAddress = this.request.socket?.remoteAddress;
152
+ this.trustedProxy = !!checker?.isTrusted(socketAddress);
153
+ if (this.trustedProxy && checker && hasForwardingHeaders(this.headers)) this.forwarded = resolveForwarded(this.headers, socketAddress, checker);
154
+ if (PERF_PROBE_SUB) perfMark("reqProxyNs");
155
+ this.method = this.getMethod();
156
+ this.host = this.getHost();
157
+ this.hostname = this.getHostName(this.host);
158
+ this.sUrl = this.getFullUrl(request);
159
+ const fast = (this.scheme === "http" || this.scheme === "https") && isCanonicalAuthority(this.host, this.scheme) ? splitTarget(this.getRawTarget()) : null;
160
+ if (fast !== null) {
161
+ this.pathname = fast.pathname;
162
+ this.search = fast.search;
163
+ } else {
164
+ const u = this.getUrl(this.sUrl);
165
+ this.#url = u;
166
+ this.pathname = u.pathname;
167
+ this.search = u.search;
168
+ }
169
+ if (PERF_PROBE_SUB) perfMark("reqUrlNs");
170
+ this.queryStringOptions = this.context?.httpKernel?.module.options.queryString || {};
171
+ this.uploadOption = this.context?.httpKernel?.module.options.upload || {};
172
+ this.maxBodySize = this.context?.httpKernel?.module.options.maxBodySize ?? 0;
173
+ let query;
174
+ if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions || {});
175
+ else query = {};
176
+ if (this.#url !== null) this.#url.query = query;
177
+ this.queryGet = query;
178
+ this.query = query;
179
+ if (PERF_PROBE_SUB) perfMark("reqQueryNs");
180
+ this.contentType = this.getContentType(this.request);
181
+ this.charset = this.getCharset();
182
+ this.domain = this.getDomain();
183
+ this.remoteAddress = this.getRemoteAddress();
184
+ if (PERF_PROBE_SUB) perfMark("reqMetaNs");
185
+ if (PERF_PROBE_SUB) perfMark("reqAcceptNs");
186
+ }
187
+ /**
188
+ * Fin de corps — émission unique d'`onRequestEnd`. L'alias `body` est posé
189
+ * ICI sur la requête Node brute (au moment du fire, le corps est parsé) —
190
+ * remplace le `once("onRequestEnd")` du ctor qui allouait 1 onceWrapper +
191
+ * 1 closure à CHAQUE requête.
192
+ *
193
+ * Il porte le CORPS (`queryPost`), pas la fusion `query` : un seul sens de
194
+ * `body` dans tout le framework, celui de l'écosystème et d'`IAdminRequest`.
195
+ */
196
+ fireRequestEnd() {
197
+ this.request.body = this.queryPost;
198
+ if (this.context.listenerCount("onRequestEnd") === 0) return false;
199
+ return this.context.fireAsync("onRequestEnd", this);
200
+ }
201
+ async initialize() {
202
+ return this.parseRequest().then(async (parser) => {
203
+ switch (true) {
204
+ case parser instanceof ParserXml:
205
+ case parser instanceof ParserQs:
206
+ case parser instanceof Parser: try {
207
+ if (this.context.finished) return;
208
+ await parser.parse();
209
+ return this.fireRequestEnd();
210
+ } catch (error) {
211
+ return this.context?.httpKernel?.onError(error, this.context);
212
+ }
213
+ default: if (!parser) try {
214
+ if (this.context.finished) return;
215
+ this.context.requestEnded = true;
216
+ return this.fireRequestEnd();
217
+ } catch (error) {
218
+ return this.context.httpKernel?.onError(error, this.context);
219
+ }
220
+ }
221
+ return parser;
222
+ }).catch((e) => {
223
+ throw e;
224
+ });
225
+ }
226
+ /**
227
+ * B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
228
+ * rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
229
+ * quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
230
+ * tête (client honnête) ; le compteur streaming de {@link Parser.write} couvre
231
+ * le cas sans `Content-Length` (chunked) ou menteur. No-op si limite à 0.
232
+ *
233
+ * @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
234
+ */
235
+ enforceBodyLimit() {
236
+ const max = this.maxBodySize;
237
+ if (max <= 0) return;
238
+ const cl = this.headers["content-length"];
239
+ if (cl === void 0) return;
240
+ const declared = parseInt(Array.isArray(cl) ? cl[0] : cl, 10);
241
+ if (Number.isFinite(declared) && declared > max) throw new HttpError(`Request body too large: ${declared} bytes > maxBodySize ${max}`, 413, this.context);
242
+ }
243
+ async parseRequest() {
244
+ if (!(this.method in parse)) return this.parser;
245
+ if (this.contentType !== "multipart/form-data") this.enforceBodyLimit();
246
+ switch (this.contentType) {
247
+ case "application/xml":
248
+ case "text/xml":
249
+ this.parser = new ParserXml(this);
250
+ return this.parser;
251
+ case "application/x-www-form-urlencoded":
252
+ this.parser = new ParserQs(this);
253
+ return this.parser;
254
+ case "multipart/form-data": return this.parseMultipart();
255
+ default:
256
+ if (this.contentType === "application/json" || this.contentType?.endsWith("+json")) {
257
+ const jsonParser = new ParserJson(this);
258
+ this.parser = jsonParser;
259
+ await jsonParser.parse();
260
+ await this.fireRequestEnd();
261
+ return BODY_DONE;
262
+ }
263
+ this.parser = new Parser(this);
264
+ return this.parser;
265
+ }
266
+ }
267
+ /**
268
+ * Parse un corps `multipart/form-data` via **busboy** (streaming pur).
269
+ *
270
+ * Contrairement à l'ancien chemin formidable, le corps brut n'est **jamais**
271
+ * bufferisé en RAM : busboy lit le flux, les fichiers sont écrits au fil de
272
+ * l'eau dans le répertoire temp (`UploadService.path`), seuls les champs
273
+ * texte (petits) restent en mémoire → fin de la double-bufferisation
274
+ * (`Parser.chunks` + parser de fichiers) qui doublait la RAM par upload.
275
+ *
276
+ * Robustesse / sécurité :
277
+ * - `new Busboy()` lève **synchroniquement** si le Content-Type n'est pas un
278
+ * multipart exploitable (boundary manquant, en-tête absent) → le flux n'a
279
+ * pas encore été consommé → bascule sur le `Parser` de secours.
280
+ * - `limits.fileSize` borne CHAQUE fichier ; `maxTotalFileSize` borne le
281
+ * CUMUL par requête (compteur Nodefony) ; `files`/`fields`/`parts` bornent
282
+ * la quantité → 413. Toute erreur nettoie les temporaires déjà posés
283
+ * (anti-orphelins / saturation disque sur endpoint d'upload public).
284
+ * - nom temp = `randomUUID()` + extension d'origine → jamais le nom client
285
+ * dans le chemin (anti path-traversal).
286
+ *
287
+ * @returns la sentinelle `BODY_DONE` (corps déjà traité), ou le `Parser`
288
+ * de secours quand le corps n'est pas un multipart.
289
+ * @throws {HttpError} 413 si une limite de taille/quantité est dépassée.
290
+ */
291
+ async parseMultipart() {
292
+ let bb;
293
+ try {
294
+ bb = new Busboy({
295
+ headers: this.request.headers,
296
+ defCharset: this.uploadOption.encoding || "utf8",
297
+ limits: {
298
+ fileSize: this.uploadOption.maxFileSize,
299
+ files: this.uploadOption.maxFiles,
300
+ fields: this.uploadOption.maxFields,
301
+ fieldSize: this.uploadOption.maxFieldsSize
302
+ }
303
+ });
304
+ } catch (e) {
305
+ this.log(`${e.message} : fallback simple parser`, "WARNING");
306
+ this.parser = new Parser(this);
307
+ return this.parser;
308
+ }
309
+ const { fields, files } = await this.streamMultipart(bb);
310
+ this.queryPost = fields;
311
+ this.query = extend({}, this.query, this.queryPost);
312
+ for (const pf of files) await this.createFileUpload(pf.field, pf.file);
313
+ this.context.requestEnded = true;
314
+ await this.fireRequestEnd();
315
+ return BODY_DONE;
316
+ }
317
+ /**
318
+ * Pilote busboy : pipe le flux requête, écrit chaque fichier dans un temp au
319
+ * fil de l'eau, accumule les champs texte. Résout `{ fields, files }` à
320
+ * `finish` (après flush de tous les writes), rejette — avec cleanup des temp
321
+ * déjà posés — à la moindre erreur ou dépassement de limite.
322
+ */
323
+ streamMultipart(bb) {
324
+ const dir = this.context.uploadService?.path || this.uploadOption.uploadDir || os.tmpdir();
325
+ const maxTotal = this.uploadOption.maxTotalFileSize;
326
+ const algo = this.uploadOption.hashAlgorithm || false;
327
+ return new Promise((resolve, reject) => {
328
+ const fields = {};
329
+ const files = [];
330
+ const pending = [];
331
+ const tempPaths = [];
332
+ const openStreams = /* @__PURE__ */ new Set();
333
+ let totalBytes = 0;
334
+ let aborted = false;
335
+ const abort = (err) => {
336
+ if (aborted) return;
337
+ aborted = true;
338
+ this.request.unpipe(bb);
339
+ this.request.resume();
340
+ for (const ws of openStreams) ws.destroy();
341
+ Promise.all(tempPaths.map(unlinkQuiet)).finally(() => reject(err));
342
+ };
343
+ bb.on("field", (name, value) => {
344
+ if (aborted) return;
345
+ this.assignField(fields, name, value);
346
+ });
347
+ bb.on("file", (fieldname, stream, filename, _enc, mimeType) => {
348
+ if (aborted) {
349
+ stream.resume();
350
+ return;
351
+ }
352
+ const match = reg.exec(fieldname);
353
+ const field = match ? match[1] : fieldname;
354
+ const ext = filename ? path.extname(filename) : "";
355
+ const newFilename = `${randomUUID()}${ext}`;
356
+ const filepath = path.join(dir, newFilename);
357
+ tempPaths.push(filepath);
358
+ const hasher = algo ? createHash(algo) : null;
359
+ const ws = fs.createWriteStream(filepath);
360
+ openStreams.add(ws);
361
+ let size = 0;
362
+ stream.on("data", (chunk) => {
363
+ size += chunk.length;
364
+ totalBytes += chunk.length;
365
+ hasher?.update(chunk);
366
+ if (maxTotal && totalBytes > maxTotal) abort(new HttpError(`maxTotalFileSize exceeded (${maxTotal} bytes)`, 413, this.context));
367
+ });
368
+ stream.on("limit", () => {
369
+ abort(new HttpError(`maxFileSize exceeded for ${filename || field}`, 413, this.context));
370
+ });
371
+ const done = new Promise((res, rej) => {
372
+ stream.on("error", rej);
373
+ ws.on("error", rej);
374
+ ws.on("close", () => {
375
+ openStreams.delete(ws);
376
+ if (aborted) return res();
377
+ files.push({
378
+ field,
379
+ file: {
380
+ filepath,
381
+ newFilename,
382
+ originalFilename: filename || null,
383
+ mimetype: mimeType || null,
384
+ size,
385
+ mtime: /* @__PURE__ */ new Date(),
386
+ hashAlgorithm: algo,
387
+ hash: hasher ? hasher.digest("hex") : null
388
+ }
389
+ });
390
+ res();
391
+ });
392
+ });
393
+ pending.push(done.catch((e) => abort(e instanceof Error ? e : new Error(String(e)))));
394
+ stream.pipe(ws);
395
+ });
396
+ bb.on("filesLimit", () => abort(new HttpError("too many files", 413, this.context)));
397
+ bb.on("fieldsLimit", () => abort(new HttpError("too many fields", 413, this.context)));
398
+ bb.on("partsLimit", () => abort(new HttpError("too many parts", 413, this.context)));
399
+ bb.on("error", (e) => abort(e instanceof Error ? e : new Error(String(e))));
400
+ bb.on("finish", () => {
401
+ if (aborted) return;
402
+ Promise.all(pending).then(() => {
403
+ if (!aborted) resolve({
404
+ fields,
405
+ files
406
+ });
407
+ }).catch(abort);
408
+ });
409
+ this.request.pipe(bb);
410
+ });
411
+ }
412
+ /**
413
+ * Accumule un champ texte multipart. `foo[]` (ou répétition de `foo`) →
414
+ * tableau ; sinon valeur scalaire. (Anciennement délégué à formidable.)
415
+ */
416
+ assignField(fields, name, value) {
417
+ const match = reg.exec(name);
418
+ const key = match ? match[1] : name;
419
+ if (key in fields) {
420
+ const cur = fields[key];
421
+ if (Array.isArray(cur)) cur.push(value);
422
+ else fields[key] = [cur, value];
423
+ } else fields[key] = match ? [value] : value;
424
+ }
425
+ accepts(Type) {
426
+ let parts = [];
427
+ let subtype = "*";
428
+ let type = "*";
429
+ if (Type) parts = Type.split("/");
430
+ if (parts) switch (parts.length) {
431
+ case 1:
432
+ subtype = parts.shift();
433
+ break;
434
+ case 2:
435
+ type = parts.shift();
436
+ subtype = parts.shift();
437
+ break;
438
+ default: throw new Error("request accepts method bad type format");
439
+ }
440
+ for (let i = 0; i < this.accept.length; i++) {
441
+ const line = this.accept[i];
442
+ if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
443
+ }
444
+ return false;
445
+ }
446
+ async createFileUpload(name, file) {
447
+ const fileUpload = await this.context.uploadService?.createUploadFile(file, name);
448
+ if (fileUpload) this.queryFile.push(fileUpload);
449
+ return fileUpload;
450
+ }
451
+ getMethod() {
452
+ return this.request.method;
453
+ }
454
+ getContentType(request) {
455
+ if (request.headers["content-type"]) {
456
+ const tab = request.headers["content-type"].split(";");
457
+ if (tab.length > 1) for (let i = 1; i < tab.length; i++) if (typeof tab[i] === "string") {
458
+ const ele = tab[i].split("=");
459
+ const key = ele[0].replace(/\s+/g, "").toLowerCase();
460
+ this.rawContentType[key] = ele[1];
461
+ } else continue;
462
+ return tab[0];
463
+ }
464
+ return null;
465
+ }
466
+ /**
467
+ * Charset du corps déduit du `Content-Type` (`charset=…`), normalisé en un
468
+ * `BufferEncoding` Node valide. Les alias IANA courants sont mappés
469
+ * (`iso-8859-1` → `latin1`, `us-ascii` → `ascii`…) ; tout charset inconnu ou
470
+ * non supporté par Node retombe sur `utf8` — JAMAIS de throw `.toString()`
471
+ * sur un encoding invalide (erreur propre, pas de 500 sur charset exotique).
472
+ */
473
+ getCharset() {
474
+ const raw = this.rawContentType.charset;
475
+ if (!raw) return "utf8";
476
+ const normalized = raw.trim().toLowerCase().replace(QUOTE_TRIM, "");
477
+ const enc = CHARSET_ALIASES[normalized] ?? normalized;
478
+ return Buffer.isEncoding(enc) ? enc : "utf8";
479
+ }
480
+ getDomain() {
481
+ return this.getHostName();
482
+ }
483
+ getUserAgent() {
484
+ return this.request.headers["user-agent"];
485
+ }
486
+ getHostName(host) {
487
+ if (this.#url?.hostname) return this.#url.hostname;
488
+ if (host) return host.split(":")[0];
489
+ if (host = this.getHost()) return host.split(":")[0];
490
+ return "";
491
+ }
492
+ getHost() {
493
+ return this.request.headers.host;
494
+ }
495
+ getRemoteAddress() {
496
+ if (this.forwarded) return this.forwarded.clientIp;
497
+ return this.request.socket?.remoteAddress ?? null;
498
+ }
499
+ /**
500
+ * Request-target brut (origin-form) tel que reçu du transport — SOURCE
501
+ * UNIQUE partagée par la concaténation de {@link getFullUrl} et la découpe
502
+ * fast-path du ctor. HTTP/2 lit le pseudo-header `:path` (override).
503
+ */
504
+ getRawTarget() {
505
+ return this.request.url;
506
+ }
507
+ /**
508
+ * Scheme effectif côté client : `Forwarded`/`X-Forwarded-*` résolu de façon
509
+ * canonique (this.forwarded, gated proxy de confiance) ; sinon le transport
510
+ * réel. HTTP/2 remplace la lecture du socket par le pseudo-header `:scheme`.
511
+ */
512
+ resolveScheme(request) {
513
+ if (this.forwarded?.proto) return this.forwarded.proto;
514
+ if ("encrypted" in request.socket && request.socket.encrypted) return "https";
515
+ return "http";
516
+ }
517
+ getFullUrl(request = this.request) {
518
+ this.scheme = this.resolveScheme(request);
519
+ return `${this.scheme}://${this.host}${this.getRawTarget()}`;
520
+ }
521
+ getHeader(name) {
522
+ if (name in this.headers) return this.headers[name];
523
+ return null;
524
+ }
525
+ setUrl(Url) {
526
+ const u = this.getUrl(Url);
527
+ this.#url = u;
528
+ this.sUrl = u.href;
529
+ this.pathname = u.pathname;
530
+ this.search = u.search;
531
+ this.scheme = u.protocol.replace(":", "");
532
+ return u;
533
+ }
534
+ getUrl(sUrl, baseUrl) {
535
+ return new URL(sUrl, baseUrl);
536
+ }
537
+ log(pci, severity, msgid, msg) {
538
+ if (!msgid) msgid = `${this.context.type} REQUEST `;
539
+ return this.context.log(pci, severity, msgid, msg);
540
+ }
541
+ };
542
+ //#endregion
543
+ export { HttpRequest as default };