@zudojs/http 1.0.0 → 1.2.0
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 +64 -6
- package/dist/httpAdapter/http.adapter.d.ts +34 -3
- package/dist/httpAdapter/http.adapter.js +28 -10
- package/dist/httpAdapter/http.adapters.js +4 -34
- package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +35 -0
- package/dist/httpAdapter/httpAdapter.errorResponse.js +137 -0
- package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
- package/dist/httpAdapter/httpAdapter.logger.js +21 -0
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +20 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +80 -20
- package/dist/httpAdapter/node/httpNode.request.d.ts +12 -1
- package/dist/httpAdapter/node/httpNode.request.js +23 -16
- package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
- package/dist/httpAdapter/node/httpNode.server.js +6 -0
- package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
- package/dist/httpClient/httpClient.executor.js +14 -3
- package/dist/httpCookies/http.cookies.d.ts +29 -2
- package/dist/httpCookies/http.cookies.js +44 -24
- package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
- package/dist/httpCookies/httpCookies.defaults.js +42 -0
- package/dist/httpCookies/index.d.ts +1 -0
- package/dist/httpCookies/index.js +1 -0
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
- package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
- package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
- package/dist/httpMiddleware/builtin/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/index.js +1 -0
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
- package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +30 -4
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
- package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
- package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
- package/dist/httpProxy/http.proxy.d.ts +7 -0
- package/dist/httpProxy/http.proxy.js +9 -0
- package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
- package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
- package/dist/httpProxy/index.d.ts +1 -0
- package/dist/httpProxy/index.js +1 -0
- package/dist/httpRequest/httpRequest.context.d.ts +7 -0
- package/dist/httpRequest/httpRequest.context.js +15 -16
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
- package/dist/httpRequest/target/httpRequest.target.js +93 -0
- package/dist/httpRequest/target/index.d.ts +9 -0
- package/dist/httpRequest/target/index.js +9 -0
- package/dist/httpResponse/httpResponse.context.d.ts +10 -0
- package/dist/httpResponse/httpResponse.context.js +13 -1
- package/dist/httpResponse/httpResponse.helper.d.ts +22 -0
- package/dist/httpResponse/httpResponse.helper.js +94 -24
- package/dist/httpRouter/core/registry/core/httpRegistry.helper.js +1 -1
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +14 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +12 -1
- package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
- package/dist/httpSecurity/httpSecurity.config.js +1 -0
- package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
- package/dist/httpSecurity/httpSecurity.guard.js +7 -14
- package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
- package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
- package/dist/httpSecurity/httpSecurity.validator.js +4 -2
- package/dist/httpSecurity/index.d.ts +2 -0
- package/dist/httpSecurity/index.js +1 -0
- package/dist/httpServer/core/httpServer.core.d.ts +11 -0
- package/dist/httpServer/core/httpServer.core.js +30 -4
- package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
- package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
- package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
- package/dist/index.js +0 -1
- package/package.json +9 -5
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
import { IncomingMessage, Server, ServerResponse, createServer, } from "node:http";
|
|
10
10
|
import { HttpRequestContext } from "../../httpRequest/httpRequest.context.js";
|
|
11
11
|
import { HttpResponseContext, createResponseContext, } from "../../httpResponse/httpResponse.context.js";
|
|
12
|
-
import { BaseHttpAdapter } from "../http.adapter.js";
|
|
12
|
+
import { BaseHttpAdapter, normalizeHandlerResult } from "../http.adapter.js";
|
|
13
13
|
import { writeResponse } from "../../httpResponse/httpResponse.writer.js";
|
|
14
14
|
import { DEFAULT_HOST, DEFAULT_PORT, DEFAULT_MAX_BODY_SIZE, NODE_DEFAULT_HEADERS_TIMEOUT, NODE_DEFAULT_REQUEST_TIMEOUT, NODE_DEFAULT_KEEP_ALIVE_TIMEOUT, validatePort, validateMaxBodySize, } from "./httpNode.type.js";
|
|
15
15
|
import { NodeResponseWriter } from "./httpNode.response.js";
|
|
16
|
+
import { compileTrustProxy } from "../../httpTrustProxy/httpTrustProxy.compilation.js";
|
|
17
|
+
import { createNodeRequestGuard } from "../../httpSecurity/httpSecurity.nodeGuard.js";
|
|
16
18
|
import { createNodeRequestContext } from "./httpNode.request.js";
|
|
17
|
-
import {
|
|
19
|
+
import { resolveErrorResponse } from "../httpAdapter.errorResponse.js";
|
|
20
|
+
import { isIncomingMessage, isServerResponse, isNodeRequestResponsePair, configureServer, listen, closeServer, readNodeRequestBody, NodeRequestBodyTooLargeError, } from "./httpNode.server.js";
|
|
18
21
|
/* -------------------------------------------------------------------------- */
|
|
19
22
|
/* Node HTTP Adapter */
|
|
20
23
|
/* -------------------------------------------------------------------------- */
|
|
@@ -31,8 +34,16 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
31
34
|
connectionsCheckingInterval;
|
|
32
35
|
shutdownGraceMs;
|
|
33
36
|
events;
|
|
37
|
+
requestGuard;
|
|
34
38
|
server;
|
|
35
39
|
ownsServer = false;
|
|
40
|
+
/**
|
|
41
|
+
* The `clientError` listener installed by `start()`, kept so `stop()` can
|
|
42
|
+
* remove it. On an externally supplied server the instance survives a
|
|
43
|
+
* stop/start cycle, and re-adding the listener on every start leaked one
|
|
44
|
+
* per restart.
|
|
45
|
+
*/
|
|
46
|
+
clientErrorListener;
|
|
36
47
|
constructor(options = {}) {
|
|
37
48
|
super({
|
|
38
49
|
...options,
|
|
@@ -65,7 +76,11 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
65
76
|
Math.min(30_000, this.headersTimeout);
|
|
66
77
|
this.shutdownGraceMs = options.shutdownGraceMs;
|
|
67
78
|
this.trustProxy = options.trustProxy;
|
|
79
|
+
if (options.trustProxy !== undefined) {
|
|
80
|
+
compileTrustProxy(options.trustProxy);
|
|
81
|
+
}
|
|
68
82
|
this.events = options.events ?? {};
|
|
83
|
+
this.requestGuard = createNodeRequestGuard(options.security);
|
|
69
84
|
this.server = options.server;
|
|
70
85
|
this.ownsServer = !options.server;
|
|
71
86
|
}
|
|
@@ -124,7 +139,25 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
124
139
|
}
|
|
125
140
|
const request = input.request;
|
|
126
141
|
const response = input.response;
|
|
127
|
-
|
|
142
|
+
if (this.requestGuard && !this.requestGuard(request).allowed) {
|
|
143
|
+
await this.writeBadRequest(response);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let context;
|
|
147
|
+
try {
|
|
148
|
+
context = this.createRequest(request);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
/*
|
|
152
|
+
* The request could not even be described (an unparseable request
|
|
153
|
+
* target, a header the context refuses). It is the client's fault, so
|
|
154
|
+
* answer 400 rather than letting the rejection destroy the socket
|
|
155
|
+
* without a response.
|
|
156
|
+
*/
|
|
157
|
+
this.emitAdapterError(error);
|
|
158
|
+
await this.writeBadRequest(response);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
128
161
|
try {
|
|
129
162
|
await this.attachNodeBody(request, context);
|
|
130
163
|
const result = await this.executeNodeHandler(context);
|
|
@@ -161,17 +194,12 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
161
194
|
}
|
|
162
195
|
return this.handler(request);
|
|
163
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Plain objects are data and are sent as JSON; see
|
|
199
|
+
* `normalizeHandlerResult`.
|
|
200
|
+
*/
|
|
164
201
|
normalizeResult(result) {
|
|
165
|
-
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
if (result === undefined || result === null) {
|
|
169
|
-
return createResponseContext();
|
|
170
|
-
}
|
|
171
|
-
if (isResponseContextLike(result)) {
|
|
172
|
-
return createResponseContext(result);
|
|
173
|
-
}
|
|
174
|
-
return createResponseContext().json(result);
|
|
202
|
+
return normalizeHandlerResult(result);
|
|
175
203
|
}
|
|
176
204
|
async handleNodeError(error, request, response) {
|
|
177
205
|
if (response.headersSent) {
|
|
@@ -199,11 +227,27 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
199
227
|
// Fall through to the safe internal server error response.
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
/*
|
|
231
|
+
* A thrown `HttpError` (or one buried under the middleware pipeline's
|
|
232
|
+
* wrappers) is answered with its own status, exposed message and headers;
|
|
233
|
+
* anything else stays a generic 500.
|
|
234
|
+
*/
|
|
235
|
+
const resolved = resolveErrorResponse(error);
|
|
236
|
+
for (const [name, value] of Object.entries(resolved.headers)) {
|
|
237
|
+
context.setHeader(name, value);
|
|
238
|
+
}
|
|
239
|
+
context.setStatus(resolved.status).json(resolved.body);
|
|
205
240
|
await this.writeNodeResponse(response, context);
|
|
206
241
|
}
|
|
242
|
+
async writeBadRequest(response) {
|
|
243
|
+
if (response.headersSent) {
|
|
244
|
+
response.destroy();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const context = createResponseContext();
|
|
248
|
+
context.setHeader("connection", "close");
|
|
249
|
+
await this.writeNodeResponse(response, context.setStatus(400).json({ error: "Bad Request" }));
|
|
250
|
+
}
|
|
207
251
|
async writeNodeResponse(response, context) {
|
|
208
252
|
const writer = this.createWriter(response);
|
|
209
253
|
await writeResponse(context, writer);
|
|
@@ -265,13 +309,17 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
265
309
|
connectionTimeout: this.connectionTimeout,
|
|
266
310
|
maxConnections: this.maxConnections,
|
|
267
311
|
});
|
|
268
|
-
this.
|
|
312
|
+
if (this.clientErrorListener) {
|
|
313
|
+
this.server.off("clientError", this.clientErrorListener);
|
|
314
|
+
}
|
|
315
|
+
this.clientErrorListener = (error, socket) => {
|
|
269
316
|
this.emitAdapterError(error);
|
|
270
317
|
if (socket.writable) {
|
|
271
318
|
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
272
319
|
}
|
|
273
320
|
socket.destroy();
|
|
274
|
-
}
|
|
321
|
+
};
|
|
322
|
+
this.server.on("clientError", this.clientErrorListener);
|
|
275
323
|
await listen(this.server, this.port, this.host);
|
|
276
324
|
const address = this.address;
|
|
277
325
|
if (address && this.events.onListening) {
|
|
@@ -284,12 +332,24 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
|
|
|
284
332
|
}
|
|
285
333
|
await super.start();
|
|
286
334
|
}
|
|
287
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Closes the server. The grace period is `options.graceMs` (the server's
|
|
337
|
+
* `gracefulShutdownTimeout`), capped by an explicit `shutdownGraceMs`;
|
|
338
|
+
* with neither it is 10 s.
|
|
339
|
+
*/
|
|
340
|
+
async stop(options = {}) {
|
|
288
341
|
if (!this.server || !this.server.listening) {
|
|
289
342
|
await super.stop();
|
|
290
343
|
return;
|
|
291
344
|
}
|
|
292
|
-
|
|
345
|
+
const graceMs = options.graceMs === undefined
|
|
346
|
+
? this.shutdownGraceMs
|
|
347
|
+
: Math.min(options.graceMs, this.shutdownGraceMs ?? Infinity);
|
|
348
|
+
await closeServer(this.server, { graceMs });
|
|
349
|
+
if (this.clientErrorListener) {
|
|
350
|
+
this.server.off("clientError", this.clientErrorListener);
|
|
351
|
+
this.clientErrorListener = undefined;
|
|
352
|
+
}
|
|
293
353
|
if (this.ownsServer) {
|
|
294
354
|
this.server = undefined;
|
|
295
355
|
}
|
|
@@ -18,6 +18,17 @@ export declare function getNodeRequestPort(request: IncomingMessage, options?: N
|
|
|
18
18
|
* `getClientIp` so there is a single implementation of the hop logic.
|
|
19
19
|
*/
|
|
20
20
|
export declare function getNodeRemoteAddress(request: IncomingMessage, options?: NodeRequestOptions): string | undefined;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Parses the request-target's query string into a flat record.
|
|
23
|
+
*
|
|
24
|
+
* Every value is attacker-controlled, so decoding never throws (a malformed
|
|
25
|
+
* `%E0` is kept raw) and `+` has its form-encoding meaning. This uses the same
|
|
26
|
+
* parser as the router's `ctx.query` (`parseQueryString`), so both agree: a
|
|
27
|
+
* repeated name is an array (`?role=user&role=admin` gives
|
|
28
|
+
* `["user", "admin"]`), the record has a `null` prototype, and
|
|
29
|
+
* `__proto__` / `constructor` / `prototype` are dropped. Previously the last
|
|
30
|
+
* value silently won here while the router returned the array.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseNodeQuery(request: IncomingMessage): Readonly<Record<string, string | readonly string[]>>;
|
|
22
33
|
export declare function createNodeRequestContext(request: IncomingMessage, options?: NodeRequestOptions): HttpRequestContext;
|
|
23
34
|
//# sourceMappingURL=httpNode.request.d.ts.map
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { HttpRequestContext, createRequestContext, } from "../../httpRequest/httpRequest.context.js";
|
|
7
7
|
import { getClientIp, isTrustedProxy, } from "../../httpTrustProxy/httpTrustProxy.core.js";
|
|
8
8
|
import { removePort, extractPort } from "./httpNode.server.js";
|
|
9
|
+
import { parseQueryString } from "../../httpQuery/http.query.js";
|
|
10
|
+
import { findRequestTargetViolation } from "../../httpRequest/target/httpRequest.target.js";
|
|
9
11
|
/* -------------------------------------------------------------------------- */
|
|
10
12
|
/* Proxy Trust */
|
|
11
13
|
/* -------------------------------------------------------------------------- */
|
|
@@ -118,40 +120,45 @@ export function getNodeRemoteAddress(request, options = {}) {
|
|
|
118
120
|
};
|
|
119
121
|
return getClientIp(proxyRequest, trustProxy) ?? peer;
|
|
120
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Parses the request-target's query string into a flat record.
|
|
125
|
+
*
|
|
126
|
+
* Every value is attacker-controlled, so decoding never throws (a malformed
|
|
127
|
+
* `%E0` is kept raw) and `+` has its form-encoding meaning. This uses the same
|
|
128
|
+
* parser as the router's `ctx.query` (`parseQueryString`), so both agree: a
|
|
129
|
+
* repeated name is an array (`?role=user&role=admin` gives
|
|
130
|
+
* `["user", "admin"]`), the record has a `null` prototype, and
|
|
131
|
+
* `__proto__` / `constructor` / `prototype` are dropped. Previously the last
|
|
132
|
+
* value silently won here while the router returned the array.
|
|
133
|
+
*/
|
|
121
134
|
export function parseNodeQuery(request) {
|
|
122
135
|
const url = request.url;
|
|
123
136
|
if (!url) {
|
|
124
|
-
return Object.freeze(
|
|
137
|
+
return Object.freeze(Object.create(null));
|
|
125
138
|
}
|
|
126
139
|
const questionIndex = url.indexOf("?");
|
|
127
140
|
if (questionIndex === -1) {
|
|
128
|
-
return Object.freeze(
|
|
141
|
+
return Object.freeze(Object.create(null));
|
|
129
142
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
const params = {};
|
|
135
|
-
for (const pair of queryString.split("&")) {
|
|
136
|
-
const [key, value] = pair.split("=");
|
|
137
|
-
if (key) {
|
|
138
|
-
params[decodeURIComponent(key)] =
|
|
139
|
-
value !== undefined ? decodeURIComponent(value) : "";
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return Object.freeze(params);
|
|
143
|
+
const hashIndex = url.indexOf("#", questionIndex + 1);
|
|
144
|
+
const queryString = url.slice(questionIndex + 1, hashIndex === -1 ? undefined : hashIndex);
|
|
145
|
+
return Object.freeze(parseQueryString(queryString));
|
|
143
146
|
}
|
|
144
147
|
/* -------------------------------------------------------------------------- */
|
|
145
148
|
/* Request Context */
|
|
146
149
|
/* -------------------------------------------------------------------------- */
|
|
147
150
|
export function createNodeRequestContext(request, options = {}) {
|
|
151
|
+
const url = request.url ?? "/";
|
|
152
|
+
const violation = findRequestTargetViolation(url);
|
|
153
|
+
if (violation !== undefined) {
|
|
154
|
+
throw new TypeError(violation);
|
|
155
|
+
}
|
|
148
156
|
const headers = getNodeRequestHeaders(request);
|
|
149
157
|
const protocol = getNodeRequestProtocol(request, options);
|
|
150
158
|
const hostname = getNodeRequestHostname(request, options);
|
|
151
159
|
const port = getNodeRequestPort(request, options);
|
|
152
160
|
const remoteAddress = getNodeRemoteAddress(request, options);
|
|
153
161
|
const query = parseNodeQuery(request);
|
|
154
|
-
const url = request.url ?? "/";
|
|
155
162
|
return createRequestContext({
|
|
156
163
|
method: request.method?.toUpperCase() ?? "GET",
|
|
157
164
|
url,
|
|
@@ -43,6 +43,12 @@ export declare function getSearchPart(url: string): string;
|
|
|
43
43
|
import { RequestBodyTooLargeError as NodeRequestBodyTooLargeError } from "@zudojs/errors";
|
|
44
44
|
export { NodeRequestBodyTooLargeError };
|
|
45
45
|
export declare function readNodeRequestBody(request: IncomingMessage, maxBodySize?: number): Promise<Uint8Array>;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a value has the shape of a response init.
|
|
48
|
+
*
|
|
49
|
+
* @deprecated No longer used by the Node adapter: a plain object returned
|
|
50
|
+
* from a handler is always sent as JSON. Kept for API compatibility.
|
|
51
|
+
*/
|
|
46
52
|
export declare function isResponseContextLike(value: unknown): value is {
|
|
47
53
|
readonly status?: number;
|
|
48
54
|
readonly statusText?: string;
|
|
@@ -250,6 +250,12 @@ export function readNodeRequestBody(request, maxBodySize = DEFAULT_MAX_BODY_SIZE
|
|
|
250
250
|
/* -------------------------------------------------------------------------- */
|
|
251
251
|
/* Response Result Detection */
|
|
252
252
|
/* -------------------------------------------------------------------------- */
|
|
253
|
+
/**
|
|
254
|
+
* Whether a value has the shape of a response init.
|
|
255
|
+
*
|
|
256
|
+
* @deprecated No longer used by the Node adapter: a plain object returned
|
|
257
|
+
* from a handler is always sent as JSON. Kept for API compatibility.
|
|
258
|
+
*/
|
|
253
259
|
export function isResponseContextLike(value) {
|
|
254
260
|
if (value === null || typeof value !== "object") {
|
|
255
261
|
return false;
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import type { Server } from "node:http";
|
|
7
7
|
import type { HttpAdapterOptions } from "../http.adapter.js";
|
|
8
8
|
import type { TrustProxy } from "../../httpTrustProxy/httpTrustProxy.core.js";
|
|
9
|
-
|
|
9
|
+
import type { NodeAdapterSecurityOptions } from "../../httpSecurity/httpSecurity.nodeGuard.js";
|
|
10
|
+
export interface NodeAdapterOptions extends HttpAdapterOptions, NodeAdapterSecurityOptions {
|
|
10
11
|
readonly host?: string;
|
|
11
12
|
readonly port?: number;
|
|
12
13
|
readonly server?: Server;
|
|
@@ -60,9 +60,20 @@ async function executeOnce(context, client) {
|
|
|
60
60
|
}, timeout);
|
|
61
61
|
}
|
|
62
62
|
const signal = combineAbortSignals(context.config.signal, controller?.signal);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
/*
|
|
64
|
+
* A `Request` body can be dispatched once. Every attempt used to be built
|
|
65
|
+
* from `context.request` itself, which consumed its body on the first try
|
|
66
|
+
* and made the first *retry* of any body-bearing request fail with
|
|
67
|
+
* "Request object that has already been used" — so `retryMethods: ["POST"]`
|
|
68
|
+
* could never retry. Each attempt now works on a clone, and the original
|
|
69
|
+
* is left untouched for the next one.
|
|
70
|
+
*/
|
|
71
|
+
const attemptRequest = context.request.body !== null && !context.request.bodyUsed
|
|
72
|
+
? context.request.clone()
|
|
73
|
+
: context.request;
|
|
74
|
+
const request = signal === attemptRequest.signal
|
|
75
|
+
? attemptRequest
|
|
76
|
+
: new Request(attemptRequest, { signal });
|
|
66
77
|
try {
|
|
67
78
|
const raw = await fetchFollowingRedirects(request, client);
|
|
68
79
|
const response = await parseResponse(raw, context.config.responseType ?? "auto");
|
|
@@ -57,7 +57,14 @@ export declare const MAX_COOKIE_HEADER_LENGTH: number;
|
|
|
57
57
|
* @returns The parsed cookie jar.
|
|
58
58
|
*/
|
|
59
59
|
export declare function parseCookies(header: string | undefined): CookieCollection;
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Serializes a `Set-Cookie` header value.
|
|
62
|
+
*
|
|
63
|
+
* Unset attributes take the secure defaults (`Path=/; HttpOnly; Secure;
|
|
64
|
+
* SameSite=Lax`, see `DEFAULT_COOKIE_ATTRIBUTES`); pass `httpOnly: false`,
|
|
65
|
+
* `secure: false`, another `sameSite` or `path` to override them.
|
|
66
|
+
*/
|
|
67
|
+
export declare function serializeCookie(name: string, value: CookieValue, cookieOptions?: CookieOptions): string;
|
|
61
68
|
export interface CookieManager {
|
|
62
69
|
get(name: string): string | undefined;
|
|
63
70
|
has(name: string): boolean;
|
|
@@ -78,11 +85,31 @@ export interface SignedCookie {
|
|
|
78
85
|
readonly value: string;
|
|
79
86
|
readonly signature: string;
|
|
80
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Serializes a signed cookie. The MAC covers the cookie **name** as well as
|
|
90
|
+
* the value, so a signature minted for one cookie is not accepted for
|
|
91
|
+
* another; read it back with `parseSignedCookie(value, secret, name)`.
|
|
92
|
+
*/
|
|
81
93
|
export declare function serializeSignedCookie(name: string, value: string, options: SignedCookieOptions): string;
|
|
82
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Verifies a signed cookie value and returns the original value, or
|
|
96
|
+
* `undefined` when the signature does not match.
|
|
97
|
+
*
|
|
98
|
+
* Pass the cookie's `name`: the signature produced by
|
|
99
|
+
* `serializeSignedCookie` is bound to it, so a value lifted from another
|
|
100
|
+
* signed cookie (for example a user-chosen display name replayed as
|
|
101
|
+
* `session_user`) is rejected. Without `name` only a legacy, value-only
|
|
102
|
+
* signature (from `signCookieValue(value, secret)`) verifies, and
|
|
103
|
+
* name-bound signatures never do.
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseSignedCookie(value: string | undefined, secret: string, name?: string): string | undefined;
|
|
83
106
|
/**
|
|
84
107
|
* Signs a cookie value with HMAC-SHA256.
|
|
85
108
|
*
|
|
109
|
+
* Synchronous by contract, so the MAC is computed with `node:crypto`: every
|
|
110
|
+
* `@zudojs/crypto` HMAC helper is asynchronous. Verification compares with
|
|
111
|
+
* `@zudojs/crypto`'s constant-time `timingSafeEqualString`.
|
|
112
|
+
*
|
|
86
113
|
* @param value - The value to authenticate.
|
|
87
114
|
* @param secret - The signing key.
|
|
88
115
|
* @returns The base64url signature.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { createHmac
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { timingSafeEqualString } from "@zudojs/crypto";
|
|
3
|
+
import { withSecureCookieDefaults } from "./httpCookies.defaults.js";
|
|
2
4
|
/* -------------------------------------------------------------------------- */
|
|
3
5
|
/* Cookie Collection */
|
|
4
6
|
/* -------------------------------------------------------------------------- */
|
|
@@ -123,7 +125,15 @@ export function parseCookies(header) {
|
|
|
123
125
|
/* -------------------------------------------------------------------------- */
|
|
124
126
|
/* Serialize Cookie */
|
|
125
127
|
/* -------------------------------------------------------------------------- */
|
|
126
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Serializes a `Set-Cookie` header value.
|
|
130
|
+
*
|
|
131
|
+
* Unset attributes take the secure defaults (`Path=/; HttpOnly; Secure;
|
|
132
|
+
* SameSite=Lax`, see `DEFAULT_COOKIE_ATTRIBUTES`); pass `httpOnly: false`,
|
|
133
|
+
* `secure: false`, another `sameSite` or `path` to override them.
|
|
134
|
+
*/
|
|
135
|
+
export function serializeCookie(name, value, cookieOptions = {}) {
|
|
136
|
+
const options = withSecureCookieDefaults(cookieOptions, "lax");
|
|
127
137
|
validateCookieName(name);
|
|
128
138
|
validateCookiePrefix(name, options);
|
|
129
139
|
const encodedName = name;
|
|
@@ -229,11 +239,27 @@ export function createCookieManager(request, response) {
|
|
|
229
239
|
},
|
|
230
240
|
};
|
|
231
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Serializes a signed cookie. The MAC covers the cookie **name** as well as
|
|
244
|
+
* the value, so a signature minted for one cookie is not accepted for
|
|
245
|
+
* another; read it back with `parseSignedCookie(value, secret, name)`.
|
|
246
|
+
*/
|
|
232
247
|
export function serializeSignedCookie(name, value, options) {
|
|
233
|
-
const signature = signCookieValue(value, options.secret);
|
|
248
|
+
const signature = signCookieValue(bindCookieName(name, value), options.secret);
|
|
234
249
|
return serializeCookie(name, `${value}.${signature}`, options);
|
|
235
250
|
}
|
|
236
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Verifies a signed cookie value and returns the original value, or
|
|
253
|
+
* `undefined` when the signature does not match.
|
|
254
|
+
*
|
|
255
|
+
* Pass the cookie's `name`: the signature produced by
|
|
256
|
+
* `serializeSignedCookie` is bound to it, so a value lifted from another
|
|
257
|
+
* signed cookie (for example a user-chosen display name replayed as
|
|
258
|
+
* `session_user`) is rejected. Without `name` only a legacy, value-only
|
|
259
|
+
* signature (from `signCookieValue(value, secret)`) verifies, and
|
|
260
|
+
* name-bound signatures never do.
|
|
261
|
+
*/
|
|
262
|
+
export function parseSignedCookie(value, secret, name) {
|
|
237
263
|
if (!value) {
|
|
238
264
|
return undefined;
|
|
239
265
|
}
|
|
@@ -243,15 +269,27 @@ export function parseSignedCookie(value, secret) {
|
|
|
243
269
|
}
|
|
244
270
|
const originalValue = value.slice(0, separator);
|
|
245
271
|
const signature = value.slice(separator + 1);
|
|
246
|
-
const expected = signCookieValue(originalValue, secret);
|
|
247
|
-
if (!
|
|
272
|
+
const expected = signCookieValue(name === undefined ? originalValue : bindCookieName(name, originalValue), secret);
|
|
273
|
+
if (!timingSafeEqualString(signature, expected)) {
|
|
248
274
|
return undefined;
|
|
249
275
|
}
|
|
250
276
|
return originalValue;
|
|
251
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The MAC input for a name-bound signature. A cookie name is an RFC 6265
|
|
280
|
+
* token and cannot contain `=`, so the encoding is unambiguous.
|
|
281
|
+
*/
|
|
282
|
+
function bindCookieName(name, value) {
|
|
283
|
+
validateCookieName(name);
|
|
284
|
+
return `${name}=${value}`;
|
|
285
|
+
}
|
|
252
286
|
/**
|
|
253
287
|
* Signs a cookie value with HMAC-SHA256.
|
|
254
288
|
*
|
|
289
|
+
* Synchronous by contract, so the MAC is computed with `node:crypto`: every
|
|
290
|
+
* `@zudojs/crypto` HMAC helper is asynchronous. Verification compares with
|
|
291
|
+
* `@zudojs/crypto`'s constant-time `timingSafeEqualString`.
|
|
292
|
+
*
|
|
255
293
|
* @param value - The value to authenticate.
|
|
256
294
|
* @param secret - The signing key.
|
|
257
295
|
* @returns The base64url signature.
|
|
@@ -396,22 +434,4 @@ function normalizePriority(value) {
|
|
|
396
434
|
throw new TypeError(`Invalid cookie priority: ${String(value)}`);
|
|
397
435
|
}
|
|
398
436
|
}
|
|
399
|
-
/* -------------------------------------------------------------------------- */
|
|
400
|
-
/* Hash Helpers */
|
|
401
|
-
/* -------------------------------------------------------------------------- */
|
|
402
|
-
/**
|
|
403
|
-
* Compares two signatures in constant time.
|
|
404
|
-
*
|
|
405
|
-
* @param left - The candidate signature.
|
|
406
|
-
* @param right - The expected signature.
|
|
407
|
-
* @returns `true` if the two are byte-identical.
|
|
408
|
-
*/
|
|
409
|
-
function timingSafeEqual(left, right) {
|
|
410
|
-
const leftBuffer = Buffer.from(left, "utf8");
|
|
411
|
-
const rightBuffer = Buffer.from(right, "utf8");
|
|
412
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
return cryptoTimingSafeEqual(leftBuffer, rightBuffer);
|
|
416
|
-
}
|
|
417
437
|
//# sourceMappingURL=http.cookies.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure cookie defaults.
|
|
3
|
+
*
|
|
4
|
+
* Both cookie serializers (`serializeCookie` in this module and the
|
|
5
|
+
* `serializeResponseCookie` the adapters use for `response.cookie()`) start
|
|
6
|
+
* from these defaults, so a cookie set "the obvious way" is `HttpOnly`,
|
|
7
|
+
* `Secure`, `SameSite=Lax` and `Path=/`, matching the defaults of
|
|
8
|
+
* `@zudojs/security`'s `serializeCookie` and the AGENTS.md secure-defaults
|
|
9
|
+
* rule. Every default can be overridden explicitly (`httpOnly: false`,
|
|
10
|
+
* `secure: false`, `sameSite: "none"`, `path: "/app"`); an option left
|
|
11
|
+
* `undefined` keeps the default.
|
|
12
|
+
*
|
|
13
|
+
* @module httpCookies/defaults
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The attributes every cookie gets unless the caller overrides them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_COOKIE_ATTRIBUTES: Readonly<{
|
|
19
|
+
readonly path: "/";
|
|
20
|
+
readonly httpOnly: true;
|
|
21
|
+
readonly secure: true;
|
|
22
|
+
readonly sameSite: "lax";
|
|
23
|
+
}>;
|
|
24
|
+
interface DefaultableCookieOptions<S extends string> {
|
|
25
|
+
readonly path?: string;
|
|
26
|
+
readonly httpOnly?: boolean;
|
|
27
|
+
readonly secure?: boolean;
|
|
28
|
+
readonly sameSite?: S;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `options` with the secure defaults filled in for every attribute
|
|
32
|
+
* the caller left `undefined`.
|
|
33
|
+
*
|
|
34
|
+
* @param options - The caller's cookie options.
|
|
35
|
+
* @param sameSite - The default `SameSite` value in the option type's own
|
|
36
|
+
* spelling (`"lax"` or `"Lax"`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function withSecureCookieDefaults<S extends string, T extends DefaultableCookieOptions<S>>(options: T | undefined, sameSite: S): T;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=httpCookies.defaults.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure cookie defaults.
|
|
3
|
+
*
|
|
4
|
+
* Both cookie serializers (`serializeCookie` in this module and the
|
|
5
|
+
* `serializeResponseCookie` the adapters use for `response.cookie()`) start
|
|
6
|
+
* from these defaults, so a cookie set "the obvious way" is `HttpOnly`,
|
|
7
|
+
* `Secure`, `SameSite=Lax` and `Path=/`, matching the defaults of
|
|
8
|
+
* `@zudojs/security`'s `serializeCookie` and the AGENTS.md secure-defaults
|
|
9
|
+
* rule. Every default can be overridden explicitly (`httpOnly: false`,
|
|
10
|
+
* `secure: false`, `sameSite: "none"`, `path: "/app"`); an option left
|
|
11
|
+
* `undefined` keeps the default.
|
|
12
|
+
*
|
|
13
|
+
* @module httpCookies/defaults
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The attributes every cookie gets unless the caller overrides them.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_COOKIE_ATTRIBUTES = Object.freeze({
|
|
19
|
+
path: "/",
|
|
20
|
+
httpOnly: true,
|
|
21
|
+
secure: true,
|
|
22
|
+
sameSite: "lax",
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Returns `options` with the secure defaults filled in for every attribute
|
|
26
|
+
* the caller left `undefined`.
|
|
27
|
+
*
|
|
28
|
+
* @param options - The caller's cookie options.
|
|
29
|
+
* @param sameSite - The default `SameSite` value in the option type's own
|
|
30
|
+
* spelling (`"lax"` or `"Lax"`).
|
|
31
|
+
*/
|
|
32
|
+
export function withSecureCookieDefaults(options, sameSite) {
|
|
33
|
+
const source = (options ?? {});
|
|
34
|
+
return {
|
|
35
|
+
...source,
|
|
36
|
+
path: source.path ?? DEFAULT_COOKIE_ATTRIBUTES.path,
|
|
37
|
+
httpOnly: source.httpOnly ?? DEFAULT_COOKIE_ATTRIBUTES.httpOnly,
|
|
38
|
+
secure: source.secure ?? DEFAULT_COOKIE_ATTRIBUTES.secure,
|
|
39
|
+
sameSite: source.sameSite ?? sameSite,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=httpCookies.defaults.js.map
|
|
@@ -7,7 +7,28 @@ import type { HttpMiddleware, HttpMiddlewareContext } from "../../httpMiddleware
|
|
|
7
7
|
import type { HttpResponseContext as ResponseContext } from "../../../httpResponse/httpResponse.context.js";
|
|
8
8
|
export declare function createAsyncMiddleware(factory: (context: HttpMiddlewareContext) => Promise<HttpMiddleware>): HttpMiddleware;
|
|
9
9
|
export declare function createConditionalMiddleware(predicate: (context: HttpMiddlewareContext) => boolean | Promise<boolean>, middleware: HttpMiddleware): HttpMiddleware;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Options for {@link createPathMiddleware}.
|
|
12
|
+
*/
|
|
13
|
+
export interface PathMiddlewareOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Match the path case-sensitively. Defaults to `false`, the router's
|
|
16
|
+
* default, so a guard scoped to `/admin` also covers `/Admin`, which the
|
|
17
|
+
* router would dispatch to the same route.
|
|
18
|
+
*/
|
|
19
|
+
readonly caseSensitive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs `middleware` only for requests addressed to `path`.
|
|
23
|
+
*
|
|
24
|
+
* The request path is normalised exactly as the router normalises it before
|
|
25
|
+
* matching: it is read with the canonical request-target parser, repeated
|
|
26
|
+
* slashes are collapsed, a trailing slash is ignored and (by default) case is
|
|
27
|
+
* ignored. An exact, case-sensitive comparison let `/Admin`, `/admin/` and
|
|
28
|
+
* `/admin//` skip a guard on `/admin` while the router still served the
|
|
29
|
+
* protected route.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createPathMiddleware(path: string, middleware: HttpMiddleware, options?: PathMiddlewareOptions): HttpMiddleware;
|
|
11
32
|
export declare function createMethodMiddleware(method: string, middleware: HttpMiddleware): HttpMiddleware;
|
|
12
33
|
export declare function createResponseMiddleware(response: Response | ResponseContext): HttpMiddleware;
|
|
13
34
|
export declare function createShortCircuitMiddleware(predicate: (context: HttpMiddlewareContext) => boolean | Promise<boolean>, response: Response | ResponseContext): HttpMiddleware;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module httpMiddleware/builtin/conditional
|
|
5
5
|
*/
|
|
6
6
|
import { getRequestMethod, getRequestUrl, extractPathname, } from "../helpers/index.js";
|
|
7
|
+
import { normalizePath } from "../../../httpRouter/core/util/httpRoute.util.js";
|
|
7
8
|
import { isWebResponse, bufferWebResponse, } from "../../../httpResponse/httpResponse.fromWeb.js";
|
|
8
9
|
export function createAsyncMiddleware(factory) {
|
|
9
10
|
return async (context, next) => {
|
|
@@ -19,12 +20,26 @@ export function createConditionalMiddleware(predicate, middleware) {
|
|
|
19
20
|
return next();
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Runs `middleware` only for requests addressed to `path`.
|
|
25
|
+
*
|
|
26
|
+
* The request path is normalised exactly as the router normalises it before
|
|
27
|
+
* matching: it is read with the canonical request-target parser, repeated
|
|
28
|
+
* slashes are collapsed, a trailing slash is ignored and (by default) case is
|
|
29
|
+
* ignored. An exact, case-sensitive comparison let `/Admin`, `/admin/` and
|
|
30
|
+
* `/admin//` skip a guard on `/admin` while the router still served the
|
|
31
|
+
* protected route.
|
|
32
|
+
*/
|
|
33
|
+
export function createPathMiddleware(path, middleware, options = {}) {
|
|
34
|
+
const caseSensitive = options.caseSensitive === true;
|
|
35
|
+
const canonical = (value) => {
|
|
36
|
+
const normalized = normalizePath(value);
|
|
37
|
+
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
38
|
+
};
|
|
39
|
+
const targetPath = canonical(extractPathname(path));
|
|
24
40
|
return createConditionalMiddleware((context) => {
|
|
25
41
|
const url = getRequestUrl(context.request);
|
|
26
|
-
|
|
27
|
-
return pathname === targetPath;
|
|
42
|
+
return canonical(extractPathname(url)) === targetPath;
|
|
28
43
|
}, middleware);
|
|
29
44
|
}
|
|
30
45
|
export function createMethodMiddleware(method, middleware) {
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
* @module httpMiddleware/builtin/conditional
|
|
5
5
|
*/
|
|
6
6
|
export { createAsyncMiddleware, createConditionalMiddleware, createPathMiddleware, createMethodMiddleware, createResponseMiddleware, createShortCircuitMiddleware, } from "./httpMiddleware.conditional.js";
|
|
7
|
+
export type { PathMiddlewareOptions } from "./httpMiddleware.conditional.js";
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|