@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.
- package/LICENSE +544 -0
- package/README.md +77 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +108 -0
- package/dist/nodefony/command/assetsPublishCommand.js +102 -0
- package/dist/nodefony/command/certificatesCommand.js +47 -0
- package/dist/nodefony/command/networkCommand.js +27 -0
- package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
- package/dist/nodefony/config/config.js +335 -0
- package/dist/nodefony/config/defineModuleConfig.js +93 -0
- package/dist/nodefony/interfaces/IContext.js +1 -0
- package/dist/nodefony/interfaces/ICookie.js +1 -0
- package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
- package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
- package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
- package/dist/nodefony/interfaces/IRequest.js +1 -0
- package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
- package/dist/nodefony/interfaces/IResponse.js +1 -0
- package/dist/nodefony/interfaces/ISession.js +1 -0
- package/dist/nodefony/interfaces/IUpload.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +376 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
- package/dist/nodefony/service/audit-logger.js +159 -0
- package/dist/nodefony/service/certificates.js +545 -0
- package/dist/nodefony/service/error-renderer.js +320 -0
- package/dist/nodefony/service/http-kernel.js +948 -0
- package/dist/nodefony/service/pretty-request-logger.js +72 -0
- package/dist/nodefony/service/request-logger.js +54 -0
- package/dist/nodefony/service/servers/clientError.js +20 -0
- package/dist/nodefony/service/servers/server-http.js +135 -0
- package/dist/nodefony/service/servers/server-https.js +204 -0
- package/dist/nodefony/service/servers/server-static.js +192 -0
- package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
- package/dist/nodefony/service/servers/server-websocket.js +104 -0
- package/dist/nodefony/service/servers/serverShutdown.js +31 -0
- package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
- package/dist/nodefony/service/sessions/sessions-service.js +580 -0
- package/dist/nodefony/service/trace.js +72 -0
- package/dist/nodefony/service/upload/upload-service.js +171 -0
- package/dist/nodefony/src/assets/collectAssets.js +34 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
- package/dist/nodefony/src/context/Context.js +415 -0
- package/dist/nodefony/src/context/domainMatcher.js +88 -0
- package/dist/nodefony/src/context/forwarded.js +185 -0
- package/dist/nodefony/src/context/http/HttpContext.js +309 -0
- package/dist/nodefony/src/context/http/Request.js +543 -0
- package/dist/nodefony/src/context/http/Response.js +368 -0
- package/dist/nodefony/src/context/http/parser.js +188 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
- package/dist/nodefony/src/context/http2/Request.js +29 -0
- package/dist/nodefony/src/context/http2/Response.js +97 -0
- package/dist/nodefony/src/context/metaData.js +47 -0
- package/dist/nodefony/src/context/requestId.js +41 -0
- package/dist/nodefony/src/context/trustProxy.js +167 -0
- package/dist/nodefony/src/context/websocket/Response.js +181 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
- package/dist/nodefony/src/cookies/cookie.js +258 -0
- package/dist/nodefony/src/errors/httpError.js +69 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
- package/dist/nodefony/src/profiler/Profiler.js +139 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
- package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
- package/dist/nodefony/src/servers/portBinder.js +114 -0
- package/dist/nodefony/src/session/session.js +390 -0
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
- package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
- package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
- package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
- package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
- package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
- package/dist/types/nodefony/config/config.d.ts +197 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
- package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
- package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
- package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
- package/dist/types/nodefony/service/certificates.d.ts +246 -0
- package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
- package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
- package/dist/types/nodefony/service/request-logger.d.ts +18 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
- package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
- package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
- package/dist/types/nodefony/service/trace.d.ts +39 -0
- package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
- package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
- package/dist/types/nodefony/src/context/Context.d.ts +195 -0
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
- package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
- package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
- package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
- package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
- package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
- package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
- package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
- package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
- package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
- package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
- package/dist/types/nodefony/src/session/session.d.ts +171 -0
- package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
- package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
- package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
- package/docs/cookies.md +365 -0
- package/docs/index.md +163 -0
- package/docs/observabilite.md +460 -0
- package/docs/rate-limit.md +372 -0
- package/docs/servers.md +935 -0
- package/docs/session.md +768 -0
- package/docs/upload.md +460 -0
- 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 };
|