@nodefony/http 10.0.0-alpha.9 → 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 +12 -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 +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -2
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +26 -4
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +87 -144
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +22 -15
- package/dist/nodefony/service/trace.js +3 -2
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +108 -46
- package/dist/nodefony/src/context/http/Response.js +134 -54
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +65 -23
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +23 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +42 -57
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/index.md +4 -2
- package/docs/observabilite.md +27 -27
- package/docs/rate-limit.md +20 -20
- package/docs/servers.md +82 -79
- package/docs/session.md +74 -69
- package/docs/upload.md +49 -43
- package/package.json +9 -12
|
@@ -7,7 +7,7 @@ import HttpResponse from "./Response.js";
|
|
|
7
7
|
import Http2Request from "../http2/Request.js";
|
|
8
8
|
import Http2Response from "../http2/Response.js";
|
|
9
9
|
import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
|
|
10
|
-
import { extend, typeOf } from "nodefony";
|
|
10
|
+
import { extend, finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
|
|
11
11
|
import url, { URL } from "node:url";
|
|
12
12
|
//#region nodefony/src/context/http/HttpContext.ts
|
|
13
13
|
const socketActiveContext = /* @__PURE__ */ new WeakMap();
|
|
@@ -20,7 +20,6 @@ var HttpContext = class extends Context {
|
|
|
20
20
|
request;
|
|
21
21
|
response;
|
|
22
22
|
uploadService;
|
|
23
|
-
csrf;
|
|
24
23
|
constructor(container, request, response, type) {
|
|
25
24
|
super(container, type);
|
|
26
25
|
this.uploadService = this.get("upload");
|
|
@@ -97,18 +96,28 @@ var HttpContext = class extends Context {
|
|
|
97
96
|
if (req.hasParsedUrl) return req.url.protocol.replace(":", "");
|
|
98
97
|
return req.scheme;
|
|
99
98
|
}
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Sert la requête : délai de réponse, hooks `onRequest`, puis l'action
|
|
101
|
+
* résolue et son rendu (`resolver.callController()`).
|
|
102
|
+
*
|
|
103
|
+
* Pas `async` : synchrone quand l'action et son rendu le sont, rien n'y est
|
|
104
|
+
* alors attendu (#505).
|
|
105
|
+
*
|
|
106
|
+
* @returns ce que rend `callController()` — le résultat du rendu, pas le
|
|
107
|
+
* contexte (comportement historique, conservé) —, ou sa promesse. Sur une
|
|
108
|
+
* redirection déjà posée : le contexte, une fois l'envoi fait.
|
|
109
|
+
* @throws HttpError 404, de façon SYNCHRONE, quand aucune route ne résout.
|
|
110
|
+
*/
|
|
111
|
+
handle() {
|
|
101
112
|
this.setTimeout();
|
|
102
|
-
if (this.isRedirect)
|
|
103
|
-
await this.send();
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
113
|
+
if (this.isRedirect) return thenMaybe(this.send(), () => this);
|
|
106
114
|
this.fire("onRequest", this);
|
|
107
|
-
this.kernel
|
|
115
|
+
const kernel = this.kernel;
|
|
116
|
+
if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
|
|
108
117
|
if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
|
|
109
|
-
if (this.resolver
|
|
118
|
+
if (this.resolver?.resolve) {
|
|
110
119
|
this.setMetaData();
|
|
111
|
-
return
|
|
120
|
+
return this.resolver.callController();
|
|
112
121
|
}
|
|
113
122
|
throw new HttpError("", 404, this);
|
|
114
123
|
}
|
|
@@ -117,7 +126,7 @@ var HttpContext = class extends Context {
|
|
|
117
126
|
if (!res) return;
|
|
118
127
|
if (this.response.stream) {
|
|
119
128
|
res.setTimeout(this.response.timeout, () => {
|
|
120
|
-
if (!this.response
|
|
129
|
+
if (!this.response.response?.writableEnded) this._onTimeout();
|
|
121
130
|
});
|
|
122
131
|
return;
|
|
123
132
|
}
|
|
@@ -130,27 +139,28 @@ var HttpContext = class extends Context {
|
|
|
130
139
|
socketTimeoutArmed.add(socket);
|
|
131
140
|
socket.on("timeout", () => {
|
|
132
141
|
const ctx = socketActiveContext.get(socket);
|
|
133
|
-
if (ctx && !ctx.response
|
|
142
|
+
if (ctx && !ctx.response.response?.writableEnded) ctx._onTimeout();
|
|
134
143
|
});
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
|
|
148
|
+
* en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
|
|
149
|
+
*
|
|
150
|
+
* @returns la réponse, ou sa promesse.
|
|
151
|
+
* @throws de façon SYNCHRONE ce que lèvent la sérialisation ou l'envoi.
|
|
152
|
+
*/
|
|
153
|
+
render(chunk, encoding, status, headers) {
|
|
138
154
|
let data = chunk;
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
if (this.isJson) data = JSON.stringify(chunk);
|
|
156
|
+
else {
|
|
157
|
+
const type = typeOf(chunk);
|
|
158
|
+
if (type === "object") {
|
|
159
|
+
this.setContextJson();
|
|
141
160
|
data = JSON.stringify(chunk);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case "object":
|
|
146
|
-
this.setContextJson();
|
|
147
|
-
data = JSON.stringify(chunk);
|
|
148
|
-
break;
|
|
149
|
-
case "string":
|
|
150
|
-
if (this.response.contentType === "application/octet-stream") this.setContextHtml();
|
|
151
|
-
break;
|
|
152
|
-
default: if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
|
|
153
|
-
}
|
|
161
|
+
} else if (type === "string") {
|
|
162
|
+
if (this.response.contentType === "application/octet-stream") this.setContextHtml();
|
|
163
|
+
} else if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
|
|
154
164
|
}
|
|
155
165
|
if (headers) this.response.setHeaders(headers);
|
|
156
166
|
if (status) this.response.setStatusCode(status);
|
|
@@ -161,83 +171,118 @@ var HttpContext = class extends Context {
|
|
|
161
171
|
this.log(describeSessionStoreFailure(e).message, "CRITIC", "SESSION-STORE");
|
|
162
172
|
return null;
|
|
163
173
|
}).then(async (_session) => {
|
|
164
|
-
return this.close()
|
|
165
|
-
throw e;
|
|
166
|
-
});
|
|
174
|
+
return this.close();
|
|
167
175
|
});
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Envoie la réponse : session sauvée, hook `onSend`, en-têtes, corps, puis
|
|
179
|
+
* fin de la réponse.
|
|
180
|
+
*
|
|
181
|
+
* Synchrone dans le cas nominal — aucune session ouverte, aucun écouteur
|
|
182
|
+
* `onSend`, réponse unique : rien n'y est à attendre (#505). Une promesse
|
|
183
|
+
* seulement quand une étape attend réellement (écriture du store de session,
|
|
184
|
+
* hook asynchrone, streaming chunké, flux HTTP/2).
|
|
185
|
+
*
|
|
186
|
+
* @param chunk - le corps ; absent, le corps déjà posé sur la réponse.
|
|
187
|
+
* @param encoding - l'encodage d'un corps texte.
|
|
188
|
+
* @returns la réponse, ou sa promesse.
|
|
189
|
+
* @throws de façon SYNCHRONE `Response Already sended` quand la réponse est
|
|
190
|
+
* déjà partie, et ce que lève une étape synchrone de l'envoi.
|
|
191
|
+
*/
|
|
192
|
+
send(chunk, encoding) {
|
|
170
193
|
if (this.finished && !this.sended) {
|
|
171
194
|
this.log("send() on finished context — client disconnected", "DEBUG");
|
|
172
195
|
return this.response;
|
|
173
196
|
}
|
|
174
|
-
if (this.sended || this.finished || this.response.isHeaderSent())
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
if (!this.timingEnabled) return this.#doSend(chunk, encoding);
|
|
197
|
+
if (this.sended || this.finished || this.response.isHeaderSent()) throw new Error("Response Already sended");
|
|
198
|
+
if (!this.timingEnabled) return this.doSend(chunk, encoding);
|
|
178
199
|
this.phaseStart("send");
|
|
179
|
-
|
|
180
|
-
return await this.#doSend(chunk, encoding);
|
|
181
|
-
} finally {
|
|
182
|
-
this.phaseEnd("send");
|
|
183
|
-
}
|
|
200
|
+
return finallyMaybe(() => this.doSend(chunk, encoding), () => this.phaseEnd("send"));
|
|
184
201
|
}
|
|
185
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Le travail de {@link send}, journalisé en ERROR quand il échoue — de façon
|
|
204
|
+
* synchrone ou par rejet, selon l'étape qui échoue.
|
|
205
|
+
*/
|
|
206
|
+
doSend(chunk, encoding) {
|
|
207
|
+
let sent;
|
|
186
208
|
try {
|
|
187
|
-
|
|
188
|
-
if (this.session != null) try {
|
|
189
|
-
await this.saveSession();
|
|
190
|
-
} catch (e) {
|
|
191
|
-
const failure = describeSessionStoreFailure(e);
|
|
192
|
-
this.log(failure.message, "CRITIC", "SESSION-STORE");
|
|
193
|
-
this.response.statusCode = failure.statusCode;
|
|
194
|
-
body = failure.body;
|
|
195
|
-
}
|
|
196
|
-
if (body) this.response?.setBody(body);
|
|
197
|
-
if (this.listenerCount("onSend") > 0) await this.fireAsync("onSend", this.response, this);
|
|
198
|
-
try {
|
|
199
|
-
this.writeHead();
|
|
200
|
-
} catch (e) {
|
|
201
|
-
this.log(e, "WARNING");
|
|
202
|
-
}
|
|
203
|
-
if (this.isRedirect) return await this.close();
|
|
204
|
-
return await this.write(body, encoding);
|
|
209
|
+
sent = this.sendSteps(chunk, encoding);
|
|
205
210
|
} catch (error) {
|
|
206
211
|
this.log(error, "ERROR");
|
|
207
212
|
throw error;
|
|
208
213
|
}
|
|
214
|
+
if (!isPromise(sent)) return sent;
|
|
215
|
+
return sent.catch((error) => {
|
|
216
|
+
this.log(error, "ERROR");
|
|
217
|
+
throw error;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Session, puis hook `onSend`, puis en-têtes et corps — chacun attendu seulement s'il attend. */
|
|
221
|
+
sendSteps(chunk, encoding) {
|
|
222
|
+
if (this.session != null) return this.saveSession().then(() => this.emitAndWrite(chunk, encoding), (e) => {
|
|
223
|
+
const failure = describeSessionStoreFailure(e);
|
|
224
|
+
this.log(failure.message, "CRITIC", "SESSION-STORE");
|
|
225
|
+
this.response.statusCode = failure.statusCode;
|
|
226
|
+
return this.emitAndWrite(failure.body, encoding);
|
|
227
|
+
});
|
|
228
|
+
return this.emitAndWrite(chunk, encoding);
|
|
229
|
+
}
|
|
230
|
+
/** Hook `onSend` (attendu seulement s'il a un écouteur), puis en-têtes et corps. */
|
|
231
|
+
emitAndWrite(body, encoding) {
|
|
232
|
+
if (body) this.response.setBody(body);
|
|
233
|
+
if (this.listenerCount("onSend") > 0) return this.fireAsync("onSend", this.response, this).then(() => this.writeHeadAndBody(body, encoding));
|
|
234
|
+
return this.writeHeadAndBody(body, encoding);
|
|
235
|
+
}
|
|
236
|
+
/** En-têtes, puis corps — ou la seule fin de réponse pour une redirection. */
|
|
237
|
+
writeHeadAndBody(body, encoding) {
|
|
238
|
+
try {
|
|
239
|
+
this.writeHead();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
this.log(e, "WARNING");
|
|
242
|
+
}
|
|
243
|
+
if (this.isRedirect) return this.close();
|
|
244
|
+
return this.write(body, encoding);
|
|
209
245
|
}
|
|
210
246
|
writeHead(statusCode, headers) {
|
|
211
|
-
if (this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.response.writeHead(statusCode, headers);
|
|
220
|
-
}
|
|
247
|
+
if (this.csrfToken) this.setCookie(new Cookie("csrf-token", this.csrfToken, {
|
|
248
|
+
httpOnly: false,
|
|
249
|
+
sameSite: "Strict",
|
|
250
|
+
secure: this.scheme === "https",
|
|
251
|
+
path: "/"
|
|
252
|
+
}));
|
|
253
|
+
this.response.setCookies();
|
|
254
|
+
this.response.writeHead(statusCode, headers);
|
|
221
255
|
}
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Écrit le corps, puis termine la requête ({@link close}). Synchrone pour une
|
|
258
|
+
* réponse unique (#505).
|
|
259
|
+
*
|
|
260
|
+
* @returns la réponse, ou sa promesse quand l'écriture attend.
|
|
261
|
+
*/
|
|
262
|
+
write(chunk, encoding, _flush = false) {
|
|
263
|
+
return thenMaybe(this.response.send(chunk, encoding || this.response.encoding), () => {
|
|
224
264
|
this.sended = true;
|
|
225
|
-
|
|
226
|
-
throw e;
|
|
227
|
-
});
|
|
228
|
-
return this.close().catch((e) => {
|
|
229
|
-
throw e;
|
|
265
|
+
return this.close();
|
|
230
266
|
});
|
|
231
267
|
}
|
|
232
268
|
flush(chunk, encoding) {
|
|
233
269
|
return this.response.flush(chunk, encoding);
|
|
234
270
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Termine la requête : hook `onClose`, puis fin de la réponse si l'envoi ne
|
|
273
|
+
* l'a pas déjà faite. Synchrone sans écouteur `onClose` (#505).
|
|
274
|
+
*
|
|
275
|
+
* @returns la réponse, ou sa promesse quand le hook attend.
|
|
276
|
+
*/
|
|
277
|
+
close() {
|
|
278
|
+
if (this.finished) return this.response;
|
|
279
|
+
if (this.listenerCount("onClose") > 0) return this.fireAsync("onClose", this).then(() => this.endResponse());
|
|
280
|
+
return this.endResponse();
|
|
281
|
+
}
|
|
282
|
+
/** Fin de la réponse, sauf si l'envoi l'a déjà terminée. */
|
|
283
|
+
endResponse() {
|
|
284
|
+
if (this.response.response?.writableEnded) return this.response;
|
|
285
|
+
return thenMaybe(this.response.end(), () => this.response);
|
|
241
286
|
}
|
|
242
287
|
redirect(Url, status, headers) {
|
|
243
288
|
if (typeof Url === "object") return this.response.redirect(url.format(Url), status, headers);
|
|
@@ -280,25 +325,25 @@ var HttpContext = class extends Context {
|
|
|
280
325
|
return this.redirect(newUrl, status, headers);
|
|
281
326
|
}
|
|
282
327
|
getHostName() {
|
|
283
|
-
return this.request
|
|
328
|
+
return this.request.getHostName();
|
|
284
329
|
}
|
|
285
330
|
getRemoteAddress() {
|
|
286
|
-
return this.request
|
|
331
|
+
return this.request.getRemoteAddress();
|
|
287
332
|
}
|
|
288
333
|
getHost() {
|
|
289
|
-
return this.request
|
|
334
|
+
return this.request.getHost();
|
|
290
335
|
}
|
|
291
336
|
getUserAgent() {
|
|
292
|
-
return this.request
|
|
337
|
+
return this.request.getUserAgent();
|
|
293
338
|
}
|
|
294
339
|
getMethod() {
|
|
295
|
-
return this.request
|
|
340
|
+
return this.request.getMethod();
|
|
296
341
|
}
|
|
297
342
|
setContentType(type, encoding) {
|
|
298
343
|
return this.response.setContentType(type, encoding);
|
|
299
344
|
}
|
|
300
345
|
resolveIsHtml() {
|
|
301
|
-
return this.request
|
|
346
|
+
return this.request.acceptHtml;
|
|
302
347
|
}
|
|
303
348
|
setDefaultContentType() {
|
|
304
349
|
if (this.isHtml) this.response.setContentType("html", "utf-8");
|
|
@@ -2,7 +2,7 @@ import HttpError from "../../errors/httpError.js";
|
|
|
2
2
|
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
3
3
|
import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
|
|
4
4
|
import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
|
|
5
|
-
import { extend } from "nodefony";
|
|
5
|
+
import { RequestContext, extend, useConfig } from "nodefony";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { URL } from "node:url";
|
|
8
8
|
import fs from "node:fs";
|
|
@@ -39,15 +39,34 @@ const parse = {
|
|
|
39
39
|
PATCH: true,
|
|
40
40
|
DELETE: true
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* La méthode porte-t-elle un corps à lire ? La règle a UNE implémentation : le
|
|
44
|
+
* choix « lire le corps » (`parseRequest`) et le choix « rien à attendre »
|
|
45
|
+
* (`initialize`) la lisent tous deux ici.
|
|
46
|
+
*/
|
|
47
|
+
function hasRequestBody(method) {
|
|
48
|
+
return method in parse;
|
|
49
|
+
}
|
|
42
50
|
const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
|
|
43
51
|
function perfMark(field) {
|
|
44
52
|
const p = globalThis.__nfPerfProbe;
|
|
45
53
|
if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
|
|
46
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Nom d'hôte d'une autorité brute (`host[:port]`) : tout ce qui précède le
|
|
57
|
+
* premier `:`. Même résultat que `host.split(":")[0]`, sans tableau (#508).
|
|
58
|
+
*/
|
|
59
|
+
function hostnameOf(host) {
|
|
60
|
+
if (!host) return "";
|
|
61
|
+
const colon = host.indexOf(":");
|
|
62
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
63
|
+
}
|
|
47
64
|
var HttpRequest = class {
|
|
48
65
|
context;
|
|
49
66
|
request;
|
|
50
67
|
#url = null;
|
|
68
|
+
#rawHostname;
|
|
69
|
+
#rawHostSource;
|
|
51
70
|
headers = {};
|
|
52
71
|
host = "";
|
|
53
72
|
method;
|
|
@@ -129,12 +148,12 @@ var HttpRequest = class {
|
|
|
129
148
|
#acceptHtml = null;
|
|
130
149
|
/** Media-ranges de l'en-tête `Accept`, parsés au premier accès (mémoïsé). */
|
|
131
150
|
get accept() {
|
|
132
|
-
|
|
151
|
+
this.#accept ??= acceptParser(this.headers.accept);
|
|
133
152
|
return this.#accept;
|
|
134
153
|
}
|
|
135
154
|
/** `true` si le client accepte `text/html` — résolu au premier accès (mémoïsé). */
|
|
136
155
|
get acceptHtml() {
|
|
137
|
-
|
|
156
|
+
this.#acceptHtml ??= this.accepts("html");
|
|
138
157
|
return this.#acceptHtml;
|
|
139
158
|
}
|
|
140
159
|
origin;
|
|
@@ -147,14 +166,16 @@ var HttpRequest = class {
|
|
|
147
166
|
this.request.body = null;
|
|
148
167
|
this.headers = request.headers;
|
|
149
168
|
this.origin = this.headers.origin;
|
|
150
|
-
const checker = this.context
|
|
151
|
-
const socketAddress = this.request.socket
|
|
169
|
+
const checker = this.context.httpKernel?.getTrustProxyChecker();
|
|
170
|
+
const socketAddress = this.request.socket.remoteAddress;
|
|
152
171
|
this.trustedProxy = !!checker?.isTrusted(socketAddress);
|
|
153
172
|
if (this.trustedProxy && checker && hasForwardingHeaders(this.headers)) this.forwarded = resolveForwarded(this.headers, socketAddress, checker);
|
|
154
173
|
if (PERF_PROBE_SUB) perfMark("reqProxyNs");
|
|
155
174
|
this.method = this.getMethod();
|
|
156
175
|
this.host = this.getHost();
|
|
157
|
-
this
|
|
176
|
+
this.#rawHostSource = this.host;
|
|
177
|
+
this.#rawHostname = hostnameOf(this.host);
|
|
178
|
+
this.hostname = this.#rawHostname;
|
|
158
179
|
this.sUrl = this.getFullUrl(request);
|
|
159
180
|
const fast = (this.scheme === "http" || this.scheme === "https") && isCanonicalAuthority(this.host, this.scheme) ? splitTarget(this.getRawTarget()) : null;
|
|
160
181
|
if (fast !== null) {
|
|
@@ -167,11 +188,9 @@ var HttpRequest = class {
|
|
|
167
188
|
this.search = u.search;
|
|
168
189
|
}
|
|
169
190
|
if (PERF_PROBE_SUB) perfMark("reqUrlNs");
|
|
170
|
-
this.queryStringOptions = this.context
|
|
171
|
-
this.uploadOption = this.context?.httpKernel?.module.options.upload || {};
|
|
172
|
-
this.maxBodySize = this.context?.httpKernel?.module.options.maxBodySize ?? 0;
|
|
191
|
+
this.queryStringOptions = this.context.httpKernel?.module.options.queryString || {};
|
|
173
192
|
let query;
|
|
174
|
-
if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions
|
|
193
|
+
if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
|
|
175
194
|
else query = {};
|
|
176
195
|
if (this.#url !== null) this.#url.query = query;
|
|
177
196
|
this.queryGet = query;
|
|
@@ -198,32 +217,64 @@ var HttpRequest = class {
|
|
|
198
217
|
if (this.context.listenerCount("onRequestEnd") === 0) return false;
|
|
199
218
|
return this.context.fireAsync("onRequestEnd", this);
|
|
200
219
|
}
|
|
201
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Lit le corps de la requête, s'il y en a un, puis émet `onRequestEnd`.
|
|
222
|
+
*
|
|
223
|
+
* Le contrat de retour dit ce qui reste à ATTENDRE :
|
|
224
|
+
* - `undefined` — la méthode ne porte pas de corps (GET, HEAD, OPTIONS…) et
|
|
225
|
+
* aucun écouteur `onRequestEnd` ne rend de promesse : tout est fait, de
|
|
226
|
+
* façon SYNCHRONE ;
|
|
227
|
+
* - une `Promise` — le corps arrive du socket (POST, PUT, PATCH, DELETE), ou
|
|
228
|
+
* un écouteur `onRequestEnd` est à attendre. Sa valeur n'est consommée par
|
|
229
|
+
* personne : elle ne sert qu'au séquençage.
|
|
230
|
+
*
|
|
231
|
+
* L'appelant n'attend donc que ce qui attend vraiment
|
|
232
|
+
* (`const pending = request.initialize(); if (pending) await pending;`) : un
|
|
233
|
+
* `await` inconditionnel reportait le pipeline d'une micro-tâche, et la
|
|
234
|
+
* chaîne de promesses coûtait ~6 évènements asynchrones à un GET qui n'avait
|
|
235
|
+
* rien à lire (#505).
|
|
236
|
+
*
|
|
237
|
+
* Ne lève jamais de façon synchrone : un corps refusé (413) ou illisible rompt
|
|
238
|
+
* la promesse rendue, comme avant.
|
|
239
|
+
*
|
|
240
|
+
* @returns `undefined` quand tout est fait, sinon la promesse à attendre.
|
|
241
|
+
*/
|
|
242
|
+
initialize() {
|
|
243
|
+
if (!hasRequestBody(this.method)) return this.endWithoutBody();
|
|
202
244
|
return this.parseRequest().then(async (parser) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
case parser instanceof Parser: try {
|
|
245
|
+
if (parser instanceof ParserXml || parser instanceof ParserQs || parser instanceof Parser) {
|
|
246
|
+
let requestEnd;
|
|
247
|
+
try {
|
|
207
248
|
if (this.context.finished) return;
|
|
208
249
|
await parser.parse();
|
|
209
|
-
|
|
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();
|
|
250
|
+
requestEnd = this.fireRequestEnd();
|
|
217
251
|
} catch (error) {
|
|
218
252
|
return this.context.httpKernel?.onError(error, this.context);
|
|
219
253
|
}
|
|
254
|
+
return requestEnd;
|
|
220
255
|
}
|
|
256
|
+
if (!parser) return this.endWithoutBody();
|
|
221
257
|
return parser;
|
|
222
|
-
}).catch((e) => {
|
|
223
|
-
throw e;
|
|
224
258
|
});
|
|
225
259
|
}
|
|
226
260
|
/**
|
|
261
|
+
* Fin d'une requête SANS corps à lire : la marque terminée et émet
|
|
262
|
+
* `onRequestEnd`. Synchrone — ne rend une promesse que lorsqu'un écouteur
|
|
263
|
+
* `onRequestEnd` est à attendre, ou quand l'émission lève (celle d'`onError`).
|
|
264
|
+
*
|
|
265
|
+
* @returns `undefined` quand tout est fait, sinon la promesse à attendre.
|
|
266
|
+
*/
|
|
267
|
+
endWithoutBody() {
|
|
268
|
+
if (this.context.finished) return;
|
|
269
|
+
try {
|
|
270
|
+
this.context.requestEnded = true;
|
|
271
|
+
const requestEnd = this.fireRequestEnd();
|
|
272
|
+
return requestEnd === false ? void 0 : requestEnd;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return this.context.httpKernel?.onError(error, this.context);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
227
278
|
* B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
|
|
228
279
|
* rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
|
|
229
280
|
* quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
|
|
@@ -232,16 +283,28 @@ var HttpRequest = class {
|
|
|
232
283
|
*
|
|
233
284
|
* @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
|
|
234
285
|
*/
|
|
286
|
+
/**
|
|
287
|
+
* Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
|
|
288
|
+
* requête : la configuration http, surchargée par le calque que la requête a
|
|
289
|
+
* pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
|
|
290
|
+
* (aucun scope ouvert), la configuration du module telle quelle.
|
|
291
|
+
*/
|
|
292
|
+
readBodyLimits() {
|
|
293
|
+
const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
|
|
294
|
+
this.uploadOption = options?.upload ?? {};
|
|
295
|
+
this.maxBodySize = options?.maxBodySize ?? 0;
|
|
296
|
+
}
|
|
235
297
|
enforceBodyLimit() {
|
|
236
298
|
const max = this.maxBodySize;
|
|
237
299
|
if (max <= 0) return;
|
|
238
300
|
const cl = this.headers["content-length"];
|
|
239
301
|
if (cl === void 0) return;
|
|
240
|
-
const declared = parseInt(
|
|
302
|
+
const declared = parseInt(typeof cl === "string" ? cl : cl[0], 10);
|
|
241
303
|
if (Number.isFinite(declared) && declared > max) throw new HttpError(`Request body too large: ${declared} bytes > maxBodySize ${max}`, 413, this.context);
|
|
242
304
|
}
|
|
243
305
|
async parseRequest() {
|
|
244
|
-
if (!(this.method
|
|
306
|
+
if (!hasRequestBody(this.method)) return this.parser;
|
|
307
|
+
this.readBodyLimits();
|
|
245
308
|
if (this.contentType !== "multipart/form-data") this.enforceBodyLimit();
|
|
246
309
|
switch (this.contentType) {
|
|
247
310
|
case "application/xml":
|
|
@@ -349,8 +412,7 @@ var HttpRequest = class {
|
|
|
349
412
|
stream.resume();
|
|
350
413
|
return;
|
|
351
414
|
}
|
|
352
|
-
const
|
|
353
|
-
const field = match ? match[1] : fieldname;
|
|
415
|
+
const field = reg.exec(fieldname)?.[1] ?? fieldname;
|
|
354
416
|
const ext = filename ? path.extname(filename) : "";
|
|
355
417
|
const newFilename = `${randomUUID()}${ext}`;
|
|
356
418
|
const filepath = path.join(dir, newFilename);
|
|
@@ -415,7 +477,7 @@ var HttpRequest = class {
|
|
|
415
477
|
*/
|
|
416
478
|
assignField(fields, name, value) {
|
|
417
479
|
const match = reg.exec(name);
|
|
418
|
-
const key = match
|
|
480
|
+
const key = match?.[1] ?? name;
|
|
419
481
|
if (key in fields) {
|
|
420
482
|
const cur = fields[key];
|
|
421
483
|
if (Array.isArray(cur)) cur.push(value);
|
|
@@ -427,7 +489,7 @@ var HttpRequest = class {
|
|
|
427
489
|
let subtype = "*";
|
|
428
490
|
let type = "*";
|
|
429
491
|
if (Type) parts = Type.split("/");
|
|
430
|
-
|
|
492
|
+
switch (parts.length) {
|
|
431
493
|
case 1:
|
|
432
494
|
subtype = parts.shift();
|
|
433
495
|
break;
|
|
@@ -437,10 +499,7 @@ var HttpRequest = class {
|
|
|
437
499
|
break;
|
|
438
500
|
default: throw new Error("request accepts method bad type format");
|
|
439
501
|
}
|
|
440
|
-
for (
|
|
441
|
-
const line = this.accept[i];
|
|
442
|
-
if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
|
|
443
|
-
}
|
|
502
|
+
for (const line of this.accept) if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
|
|
444
503
|
return false;
|
|
445
504
|
}
|
|
446
505
|
async createFileUpload(name, file) {
|
|
@@ -454,12 +513,15 @@ var HttpRequest = class {
|
|
|
454
513
|
getContentType(request) {
|
|
455
514
|
if (request.headers["content-type"]) {
|
|
456
515
|
const tab = request.headers["content-type"].split(";");
|
|
457
|
-
if (tab.length > 1) for (let i = 1; i < tab.length; i++)
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
516
|
+
if (tab.length > 1) for (let i = 1; i < tab.length; i++) {
|
|
517
|
+
const part = tab[i];
|
|
518
|
+
if (typeof part === "string") {
|
|
519
|
+
const [rawKey = "", value] = part.split("=");
|
|
520
|
+
const key = rawKey.replace(/\s+/g, "").toLowerCase();
|
|
521
|
+
this.rawContentType[key] = value;
|
|
522
|
+
} else continue;
|
|
523
|
+
}
|
|
524
|
+
return tab[0] ?? null;
|
|
463
525
|
}
|
|
464
526
|
return null;
|
|
465
527
|
}
|
|
@@ -485,16 +547,16 @@ var HttpRequest = class {
|
|
|
485
547
|
}
|
|
486
548
|
getHostName(host) {
|
|
487
549
|
if (this.#url?.hostname) return this.#url.hostname;
|
|
488
|
-
if (host) return host
|
|
489
|
-
|
|
490
|
-
return
|
|
550
|
+
if (host) return hostnameOf(host);
|
|
551
|
+
host = this.getHost();
|
|
552
|
+
return host === this.#rawHostSource ? this.#rawHostname : hostnameOf(host);
|
|
491
553
|
}
|
|
492
554
|
getHost() {
|
|
493
555
|
return this.request.headers.host;
|
|
494
556
|
}
|
|
495
557
|
getRemoteAddress() {
|
|
496
558
|
if (this.forwarded) return this.forwarded.clientIp;
|
|
497
|
-
return this.request.socket
|
|
559
|
+
return this.request.socket.remoteAddress ?? null;
|
|
498
560
|
}
|
|
499
561
|
/**
|
|
500
562
|
* Request-target brut (origin-form) tel que reçu du transport — SOURCE
|
|
@@ -511,7 +573,7 @@ var HttpRequest = class {
|
|
|
511
573
|
*/
|
|
512
574
|
resolveScheme(request) {
|
|
513
575
|
if (this.forwarded?.proto) return this.forwarded.proto;
|
|
514
|
-
if (
|
|
576
|
+
if (request.socket.encrypted) return "https";
|
|
515
577
|
return "http";
|
|
516
578
|
}
|
|
517
579
|
getFullUrl(request = this.request) {
|