@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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULT_RESPONSE_STATUS } from "./core/httpResponse.type.js";
|
|
7
7
|
import { getStatusText } from "./core/httpResponse.statusText.js";
|
|
8
|
+
import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
|
|
8
9
|
export class HttpResponseContext {
|
|
9
10
|
_status;
|
|
10
11
|
_statusText;
|
|
@@ -86,10 +87,21 @@ export class HttpResponseContext {
|
|
|
86
87
|
this._cookies = this._cookies.filter((c) => c.name !== name);
|
|
87
88
|
return this;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Redirects to `url`.
|
|
92
|
+
*
|
|
93
|
+
* The destination goes through `assertSafeRedirect`: a `javascript:` or
|
|
94
|
+
* `data:` URL, a scheme-relative `//evil.com`, or a value carrying a
|
|
95
|
+
* control character throws instead of being emitted as `Location`. An
|
|
96
|
+
* absolute `http(s)` URL and a same-origin path reference are accepted.
|
|
97
|
+
*
|
|
98
|
+
* @throws {TypeError} If the destination is not a safe redirect target.
|
|
99
|
+
*/
|
|
89
100
|
redirect(url, status = 302) {
|
|
101
|
+
const location = assertSafeRedirect(url);
|
|
90
102
|
this._status = status;
|
|
91
103
|
this._statusText = getStatusText(status);
|
|
92
|
-
this._headers["location"] =
|
|
104
|
+
this._headers["location"] = location;
|
|
93
105
|
return this;
|
|
94
106
|
}
|
|
95
107
|
markSent() {
|
|
@@ -30,6 +30,12 @@ export declare function htmlResponse(html: string, status?: number, headers?: Re
|
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Creates a redirect response.
|
|
33
|
+
*
|
|
34
|
+
* The destination goes through {@link assertSafeRedirect}: a `javascript:` or
|
|
35
|
+
* `data:` URL, a scheme-relative `//evil.com`, or a value carrying a control
|
|
36
|
+
* character throws instead of being emitted as `Location`.
|
|
37
|
+
*
|
|
38
|
+
* @throws {TypeError} If the destination is not a safe redirect target.
|
|
33
39
|
*/
|
|
34
40
|
export declare function redirectResponse(url: string, status?: number, headers?: ResponseHeaders): {
|
|
35
41
|
readonly status: number;
|
|
@@ -46,6 +52,22 @@ export declare function emptyResponse(status?: number, headers?: ResponseHeaders
|
|
|
46
52
|
};
|
|
47
53
|
/**
|
|
48
54
|
* Serializes a response cookie into a Set-Cookie header string.
|
|
55
|
+
*
|
|
56
|
+
* This is the serializer the adapters use for `response.cookie()` and it
|
|
57
|
+
* previously concatenated every field verbatim, so a value such as
|
|
58
|
+
* `x; Domain=evil.com` became a second attribute and a `__Host-` cookie
|
|
59
|
+
* missing `Secure` was emitted (and silently dropped by the browser). It now
|
|
60
|
+
* applies the same rules as `serializeCookie()` in the cookies module:
|
|
61
|
+
*
|
|
62
|
+
* - the name must be an RFC 6265 token;
|
|
63
|
+
* - `__Host-` / `__Secure-` prefix constraints are enforced;
|
|
64
|
+
* - `Domain` and `Path` may not contain `;` or a control character;
|
|
65
|
+
* - a value that is not made only of `cookie-octet` characters is
|
|
66
|
+
* percent-encoded, so it can neither inject an attribute nor split the
|
|
67
|
+
* header (a value that is already valid is emitted unchanged);
|
|
68
|
+
* - `maxAge` must be finite and `expires` must be a valid `Date`.
|
|
69
|
+
*
|
|
70
|
+
* @throws {TypeError} If the name, a prefix rule or an attribute is invalid.
|
|
49
71
|
*/
|
|
50
72
|
export declare function serializeResponseCookie(cookie: ResponseCookie): string;
|
|
51
73
|
//# sourceMappingURL=httpResponse.helper.d.ts.map
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpResponse/helpers
|
|
5
5
|
*/
|
|
6
|
+
import { withSecureCookieDefaults } from "../httpCookies/httpCookies.defaults.js";
|
|
7
|
+
import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
|
|
6
8
|
/**
|
|
7
9
|
* Creates a JSON response.
|
|
8
10
|
*/
|
|
@@ -44,12 +46,18 @@ export function htmlResponse(html, status = 200, headers = {}) {
|
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Creates a redirect response.
|
|
49
|
+
*
|
|
50
|
+
* The destination goes through {@link assertSafeRedirect}: a `javascript:` or
|
|
51
|
+
* `data:` URL, a scheme-relative `//evil.com`, or a value carrying a control
|
|
52
|
+
* character throws instead of being emitted as `Location`.
|
|
53
|
+
*
|
|
54
|
+
* @throws {TypeError} If the destination is not a safe redirect target.
|
|
47
55
|
*/
|
|
48
56
|
export function redirectResponse(url, status = 302, headers = {}) {
|
|
49
57
|
return {
|
|
50
58
|
status,
|
|
51
59
|
headers: {
|
|
52
|
-
location: url,
|
|
60
|
+
location: assertSafeRedirect(url),
|
|
53
61
|
...headers,
|
|
54
62
|
},
|
|
55
63
|
body: undefined,
|
|
@@ -65,40 +73,102 @@ export function emptyResponse(status = 204, headers = {}) {
|
|
|
65
73
|
body: undefined,
|
|
66
74
|
};
|
|
67
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* RFC 6265 `cookie-name`, which is an RFC 9110 `token`.
|
|
78
|
+
*/
|
|
79
|
+
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
80
|
+
/**
|
|
81
|
+
* RFC 6265 `cookie-octet`: printable US-ASCII minus CTLs, SP, DQUOTE, comma,
|
|
82
|
+
* semicolon and backslash.
|
|
83
|
+
*/
|
|
84
|
+
const COOKIE_OCTETS = /^[\u0021\u0023-\u002b\u002d-\u003a\u003c-\u005b\u005d-\u007e]*$/;
|
|
85
|
+
/**
|
|
86
|
+
* Characters that must never appear in a cookie attribute value: `;` starts
|
|
87
|
+
* the next attribute, and any control character can split the header.
|
|
88
|
+
*/
|
|
89
|
+
const COOKIE_ATTRIBUTE_FORBIDDEN = /[;\u0000-\u001f\u007f]/;
|
|
68
90
|
/**
|
|
69
91
|
* Serializes a response cookie into a Set-Cookie header string.
|
|
92
|
+
*
|
|
93
|
+
* This is the serializer the adapters use for `response.cookie()` and it
|
|
94
|
+
* previously concatenated every field verbatim, so a value such as
|
|
95
|
+
* `x; Domain=evil.com` became a second attribute and a `__Host-` cookie
|
|
96
|
+
* missing `Secure` was emitted (and silently dropped by the browser). It now
|
|
97
|
+
* applies the same rules as `serializeCookie()` in the cookies module:
|
|
98
|
+
*
|
|
99
|
+
* - the name must be an RFC 6265 token;
|
|
100
|
+
* - `__Host-` / `__Secure-` prefix constraints are enforced;
|
|
101
|
+
* - `Domain` and `Path` may not contain `;` or a control character;
|
|
102
|
+
* - a value that is not made only of `cookie-octet` characters is
|
|
103
|
+
* percent-encoded, so it can neither inject an attribute nor split the
|
|
104
|
+
* header (a value that is already valid is emitted unchanged);
|
|
105
|
+
* - `maxAge` must be finite and `expires` must be a valid `Date`.
|
|
106
|
+
*
|
|
107
|
+
* @throws {TypeError} If the name, a prefix rule or an attribute is invalid.
|
|
70
108
|
*/
|
|
71
109
|
export function serializeResponseCookie(cookie) {
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (opts.
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (opts.expires instanceof Date) {
|
|
82
|
-
str += `; Expires=${opts.expires.toUTCString()}`;
|
|
110
|
+
const name = cookie.name;
|
|
111
|
+
if (!name || !COOKIE_NAME_TOKEN.test(name)) {
|
|
112
|
+
throw new TypeError(`Invalid cookie name: ${JSON.stringify(name)}`);
|
|
113
|
+
}
|
|
114
|
+
const opts = withSecureCookieDefaults(cookie.options, "Lax");
|
|
115
|
+
if (name.startsWith("__Host-")) {
|
|
116
|
+
if (!opts.secure) {
|
|
117
|
+
throw new TypeError("A __Host- cookie requires the Secure attribute.");
|
|
83
118
|
}
|
|
84
|
-
if (
|
|
85
|
-
|
|
119
|
+
if (opts.domain) {
|
|
120
|
+
throw new TypeError("A __Host- cookie must not set a Domain attribute.");
|
|
86
121
|
}
|
|
87
|
-
if (opts.
|
|
88
|
-
|
|
122
|
+
if ((opts.path ?? "/") !== "/") {
|
|
123
|
+
throw new TypeError("A __Host- cookie requires Path=/.");
|
|
89
124
|
}
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
}
|
|
126
|
+
else if (name.startsWith("__Secure-") && !opts.secure) {
|
|
127
|
+
throw new TypeError("A __Secure- cookie requires the Secure attribute.");
|
|
128
|
+
}
|
|
129
|
+
const rawValue = String(cookie.value);
|
|
130
|
+
const value = COOKIE_OCTETS.test(rawValue)
|
|
131
|
+
? rawValue
|
|
132
|
+
: encodeURIComponent(rawValue);
|
|
133
|
+
let str = `${name}=${value}`;
|
|
134
|
+
if (opts.domain) {
|
|
135
|
+
if (COOKIE_ATTRIBUTE_FORBIDDEN.test(opts.domain)) {
|
|
136
|
+
throw new TypeError("Invalid cookie domain.");
|
|
92
137
|
}
|
|
93
|
-
|
|
94
|
-
|
|
138
|
+
str += `; Domain=${opts.domain}`;
|
|
139
|
+
}
|
|
140
|
+
if (opts.path) {
|
|
141
|
+
if (COOKIE_ATTRIBUTE_FORBIDDEN.test(opts.path)) {
|
|
142
|
+
throw new TypeError("Invalid cookie path.");
|
|
95
143
|
}
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
str += `; Path=${opts.path}`;
|
|
145
|
+
}
|
|
146
|
+
if (opts.expires instanceof Date) {
|
|
147
|
+
if (Number.isNaN(opts.expires.getTime())) {
|
|
148
|
+
throw new TypeError("Cookie expires must be a valid Date.");
|
|
98
149
|
}
|
|
99
|
-
|
|
100
|
-
|
|
150
|
+
str += `; Expires=${opts.expires.toUTCString()}`;
|
|
151
|
+
}
|
|
152
|
+
if (typeof opts.maxAge === "number") {
|
|
153
|
+
if (!Number.isFinite(opts.maxAge)) {
|
|
154
|
+
throw new TypeError("Cookie maxAge must be a finite number.");
|
|
101
155
|
}
|
|
156
|
+
str += `; Max-Age=${Math.floor(opts.maxAge)}`;
|
|
157
|
+
}
|
|
158
|
+
if (opts.httpOnly) {
|
|
159
|
+
str += "; HttpOnly";
|
|
160
|
+
}
|
|
161
|
+
if (opts.secure) {
|
|
162
|
+
str += "; Secure";
|
|
163
|
+
}
|
|
164
|
+
if (opts.sameSite) {
|
|
165
|
+
str += `; SameSite=${opts.sameSite}`;
|
|
166
|
+
}
|
|
167
|
+
if (opts.priority) {
|
|
168
|
+
str += `; Priority=${opts.priority}`;
|
|
169
|
+
}
|
|
170
|
+
if (opts.partitioned) {
|
|
171
|
+
str += "; Partitioned";
|
|
102
172
|
}
|
|
103
173
|
return str;
|
|
104
174
|
}
|
|
@@ -42,7 +42,7 @@ export function normalizePath(path, options) {
|
|
|
42
42
|
normalized = normalized.toLowerCase();
|
|
43
43
|
}
|
|
44
44
|
if (!options.strict) {
|
|
45
|
-
normalized = normalized.replace(
|
|
45
|
+
normalized = normalized.replace(/(?<!\/)\/+$/, "") || "/";
|
|
46
46
|
}
|
|
47
47
|
return normalized;
|
|
48
48
|
}
|
|
@@ -8,7 +8,21 @@ import type { HttpRequestContext as RequestContext } from "../../../httpRequest/
|
|
|
8
8
|
export declare function getRequestMethod(request: RequestContext): string;
|
|
9
9
|
export declare function getRequestUrl(request: RequestContext): string;
|
|
10
10
|
export declare function getRequestSignal(request: RequestContext): AbortSignal | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Parses a request-target with the canonical parser shared by the request
|
|
13
|
+
* context and path-scoped middleware.
|
|
14
|
+
*/
|
|
11
15
|
export declare function parseUrl(value: string): URL;
|
|
16
|
+
/**
|
|
17
|
+
* Builds the router's query record.
|
|
18
|
+
*
|
|
19
|
+
* Uses the same parser as the Node adapter's `request.query`
|
|
20
|
+
* (`parseQueryString`): a repeated name is an array, the record has a `null`
|
|
21
|
+
* prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
|
|
22
|
+
* two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
|
|
23
|
+
* the last value), which let middleware and handlers read different values
|
|
24
|
+
* for the same parameter.
|
|
25
|
+
*/
|
|
12
26
|
export declare function parseQuery(params: URLSearchParams): Readonly<Record<string, string | string[]>>;
|
|
13
27
|
export declare function normalizePath(path: string): string;
|
|
14
28
|
export declare function splitPath(path: string): string[];
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Request accessors, URL/query parsing, and path normalization shared by the
|
|
5
5
|
* router core.
|
|
6
6
|
*/
|
|
7
|
+
import { parseRequestTarget } from "../../../httpRequest/target/httpRequest.target.js";
|
|
8
|
+
import { parseQueryString } from "../../../httpQuery/http.query.js";
|
|
7
9
|
import { InvalidRoutePatternError } from "../error/httpRouter.error.js";
|
|
8
10
|
export function getRequestMethod(request) {
|
|
9
11
|
const value = request.method;
|
|
@@ -19,23 +21,25 @@ export function getRequestUrl(request) {
|
|
|
19
21
|
export function getRequestSignal(request) {
|
|
20
22
|
return request.signal;
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Parses a request-target with the canonical parser shared by the request
|
|
26
|
+
* context and path-scoped middleware.
|
|
27
|
+
*/
|
|
22
28
|
export function parseUrl(value) {
|
|
23
|
-
|
|
24
|
-
return new URL(value, "http://zudojs.local");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return new URL("/", "http://zudojs.local");
|
|
28
|
-
}
|
|
29
|
+
return parseRequestTarget(value);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds the router's query record.
|
|
33
|
+
*
|
|
34
|
+
* Uses the same parser as the Node adapter's `request.query`
|
|
35
|
+
* (`parseQueryString`): a repeated name is an array, the record has a `null`
|
|
36
|
+
* prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
|
|
37
|
+
* two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
|
|
38
|
+
* the last value), which let middleware and handlers read different values
|
|
39
|
+
* for the same parameter.
|
|
40
|
+
*/
|
|
30
41
|
export function parseQuery(params) {
|
|
31
|
-
|
|
32
|
-
for (const key of new Set(Array.from(params.keys()))) {
|
|
33
|
-
const values = params.getAll(key);
|
|
34
|
-
result[key] = values.length > 1 ? values : (values[0] ?? "");
|
|
35
|
-
}
|
|
36
|
-
return Object.freeze({
|
|
37
|
-
...result,
|
|
38
|
-
});
|
|
42
|
+
return Object.freeze(parseQueryString(params.toString()));
|
|
39
43
|
}
|
|
40
44
|
/* -------------------------------------------------------------------------- */
|
|
41
45
|
/* Path Helpers */
|
|
@@ -229,8 +229,19 @@ function toDispatchHandler(handler, context) {
|
|
|
229
229
|
signal: getRequestSignal(request),
|
|
230
230
|
}));
|
|
231
231
|
if (result instanceof HttpResponseContext && result !== response) {
|
|
232
|
-
|
|
232
|
+
/*
|
|
233
|
+
* Everything the handler put on its response must survive the merge.
|
|
234
|
+
* Copying only status, headers and body dropped every cookie the
|
|
235
|
+
* handler set, its custom status text and its metadata.
|
|
236
|
+
*/
|
|
237
|
+
response.setStatus(result.status, result.statusText);
|
|
233
238
|
response.headers_obj(result.headers);
|
|
239
|
+
for (const cookie of result.cookies) {
|
|
240
|
+
response.setCookie(cookie);
|
|
241
|
+
}
|
|
242
|
+
for (const [key, value] of Object.entries(result.metadata)) {
|
|
243
|
+
response.setMetadata(key, value);
|
|
244
|
+
}
|
|
234
245
|
response.setBody(result.body);
|
|
235
246
|
}
|
|
236
247
|
};
|
|
@@ -26,6 +26,12 @@ export interface HTTPSecurityConfig {
|
|
|
26
26
|
readonly maxQueryLength?: number;
|
|
27
27
|
/** Allowed Host header values. Empty = allow all. */
|
|
28
28
|
readonly allowedHosts?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether a request without a Host header is rejected (default: true).
|
|
31
|
+
* The Node adapter relaxes this for HTTP/1.0 requests, where Host is
|
|
32
|
+
* optional and some load-balancer health checks omit it.
|
|
33
|
+
*/
|
|
34
|
+
readonly requireHost?: boolean;
|
|
29
35
|
/**
|
|
30
36
|
* Whether `X-Forwarded-*` headers may be trusted (default: `false`).
|
|
31
37
|
*
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* first security boundary. Rejects malformed, oversized, or
|
|
6
6
|
* suspicious requests before they reach the router.
|
|
7
7
|
*/
|
|
8
|
+
import { HttpRequestGuardError } from "@zudojs/errors";
|
|
8
9
|
import type { HTTPSecurityConfig } from "./httpSecurity.config.js";
|
|
9
10
|
/** A request-like object for validation (keeps the guard decoupled from HTTP types). */
|
|
10
11
|
export interface GuardableRequest {
|
|
@@ -39,16 +40,13 @@ export declare function guardRequest(request: GuardableRequest, config?: Partial
|
|
|
39
40
|
*/
|
|
40
41
|
export declare function createRequestGuard(config?: Partial<HTTPSecurityConfig>): (request: GuardableRequest) => GuardResult;
|
|
41
42
|
/**
|
|
42
|
-
* Error thrown by {@link assertRequestAllowed}
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* failures, so a caller can log
|
|
43
|
+
* Error thrown by {@link assertRequestAllowed}: the shared
|
|
44
|
+
* `HttpRequestGuardError` from `@zudojs/errors` (a `BaseError`, code
|
|
45
|
+
* `HTTP_REQUEST_REJECTED`, not exposed). It carries the status the response
|
|
46
|
+
* should use and the individual validation failures, so a caller can log
|
|
47
|
+
* the detail without returning it to the client.
|
|
46
48
|
*/
|
|
47
|
-
export
|
|
48
|
-
readonly statusCode: number;
|
|
49
|
-
readonly errors: readonly string[];
|
|
50
|
-
constructor(result: GuardResult);
|
|
51
|
-
}
|
|
49
|
+
export { HttpRequestGuardError };
|
|
52
50
|
/**
|
|
53
51
|
* Runs the guard and throws {@link HttpRequestGuardError} when the request is
|
|
54
52
|
* rejected.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* first security boundary. Rejects malformed, oversized, or
|
|
6
6
|
* suspicious requests before they reach the router.
|
|
7
7
|
*/
|
|
8
|
+
import { HttpRequestGuardError } from "@zudojs/errors";
|
|
8
9
|
import { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
|
|
9
10
|
/**
|
|
10
11
|
* Run all security validations against an incoming request.
|
|
@@ -52,21 +53,13 @@ export function createRequestGuard(config) {
|
|
|
52
53
|
return (request) => guardRequest(request, config);
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
|
-
* Error thrown by {@link assertRequestAllowed}
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* failures, so a caller can log
|
|
56
|
+
* Error thrown by {@link assertRequestAllowed}: the shared
|
|
57
|
+
* `HttpRequestGuardError` from `@zudojs/errors` (a `BaseError`, code
|
|
58
|
+
* `HTTP_REQUEST_REJECTED`, not exposed). It carries the status the response
|
|
59
|
+
* should use and the individual validation failures, so a caller can log
|
|
60
|
+
* the detail without returning it to the client.
|
|
59
61
|
*/
|
|
60
|
-
export
|
|
61
|
-
statusCode;
|
|
62
|
-
errors;
|
|
63
|
-
constructor(result) {
|
|
64
|
-
super(`Request rejected by security guard: ${result.errors.join("; ")}`);
|
|
65
|
-
this.name = "HttpRequestGuardError";
|
|
66
|
-
this.statusCode = result.statusCode;
|
|
67
|
-
this.errors = result.errors;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
62
|
+
export { HttpRequestGuardError };
|
|
70
63
|
/**
|
|
71
64
|
* Runs the guard and throws {@link HttpRequestGuardError} when the request is
|
|
72
65
|
* rejected.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request guard wiring for the Node adapter.
|
|
3
|
+
*
|
|
4
|
+
* `guardRequest` (Host, request-ID, header-count, header-size, URL/query
|
|
5
|
+
* length and `Transfer-Encoding`/`Content-Length` checks) had no call site,
|
|
6
|
+
* so every one of those checks was inert unless an application wired it by
|
|
7
|
+
* hand. The Node adapter now runs it on every request before the request
|
|
8
|
+
* context is built or the body is read, and answers `400` when it refuses.
|
|
9
|
+
*
|
|
10
|
+
* @module httpSecurity/nodeGuard
|
|
11
|
+
*/
|
|
12
|
+
import type { IncomingMessage } from "node:http";
|
|
13
|
+
import type { HTTPSecurityConfig } from "./httpSecurity.config.js";
|
|
14
|
+
import { type GuardResult } from "./httpSecurity.guard.js";
|
|
15
|
+
/**
|
|
16
|
+
* The adapter's `security` option.
|
|
17
|
+
*
|
|
18
|
+
* - `true` / omitted (default): run the guard with its defaults.
|
|
19
|
+
* - an object: run the guard with that configuration.
|
|
20
|
+
* - `false`: do not run the guard.
|
|
21
|
+
*/
|
|
22
|
+
export type NodeAdapterSecurityOption = boolean | Partial<HTTPSecurityConfig>;
|
|
23
|
+
/**
|
|
24
|
+
* Adapter options contributed by the request guard.
|
|
25
|
+
*/
|
|
26
|
+
export interface NodeAdapterSecurityOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Request guard applied before dispatch (on by default). See
|
|
29
|
+
* {@link NodeAdapterSecurityOption}.
|
|
30
|
+
*
|
|
31
|
+
* The guard's `maxBodySize` defaults to "no limit" here so the adapter's own
|
|
32
|
+
* `maxBodySize` keeps answering oversized bodies with `413`; set it
|
|
33
|
+
* explicitly to have the guard refuse a too-large `Content-Length` with
|
|
34
|
+
* `400` before any body is read. `requireHost` defaults to `true` except for
|
|
35
|
+
* HTTP/1.0 requests, where Host is optional.
|
|
36
|
+
*/
|
|
37
|
+
readonly security?: NodeAdapterSecurityOption;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A per-request guard for Node requests.
|
|
41
|
+
*/
|
|
42
|
+
export type NodeRequestGuard = (request: IncomingMessage) => GuardResult;
|
|
43
|
+
/**
|
|
44
|
+
* Builds the guard the adapter runs on each request, or `undefined` when the
|
|
45
|
+
* guard is disabled with `security: false`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createNodeRequestGuard(option: NodeAdapterSecurityOption | undefined): NodeRequestGuard | undefined;
|
|
48
|
+
//# sourceMappingURL=httpSecurity.nodeGuard.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request guard wiring for the Node adapter.
|
|
3
|
+
*
|
|
4
|
+
* `guardRequest` (Host, request-ID, header-count, header-size, URL/query
|
|
5
|
+
* length and `Transfer-Encoding`/`Content-Length` checks) had no call site,
|
|
6
|
+
* so every one of those checks was inert unless an application wired it by
|
|
7
|
+
* hand. The Node adapter now runs it on every request before the request
|
|
8
|
+
* context is built or the body is read, and answers `400` when it refuses.
|
|
9
|
+
*
|
|
10
|
+
* @module httpSecurity/nodeGuard
|
|
11
|
+
*/
|
|
12
|
+
import { guardRequest, } from "./httpSecurity.guard.js";
|
|
13
|
+
/**
|
|
14
|
+
* Builds the guard the adapter runs on each request, or `undefined` when the
|
|
15
|
+
* guard is disabled with `security: false`.
|
|
16
|
+
*/
|
|
17
|
+
export function createNodeRequestGuard(option) {
|
|
18
|
+
if (option === false) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const configured = typeof option === "object" ? option : {};
|
|
22
|
+
return (request) => guardRequest({
|
|
23
|
+
method: request.method ?? "GET",
|
|
24
|
+
url: request.url ?? "/",
|
|
25
|
+
headers: request.headers,
|
|
26
|
+
}, {
|
|
27
|
+
maxBodySize: Number.MAX_SAFE_INTEGER,
|
|
28
|
+
requireHost: request.httpVersion !== "1.0",
|
|
29
|
+
...configured,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=httpSecurity.nodeGuard.js.map
|
|
@@ -36,10 +36,12 @@ export function validateHeaders(headers, config) {
|
|
|
36
36
|
* Validate the Host header against allowed hosts.
|
|
37
37
|
*/
|
|
38
38
|
export function validateHost(host, config) {
|
|
39
|
+
const cfg = resolveConfig(config);
|
|
39
40
|
if (!host) {
|
|
40
|
-
return
|
|
41
|
+
return cfg.requireHost
|
|
42
|
+
? { valid: false, errors: ["Missing Host header"] }
|
|
43
|
+
: { valid: true, errors: [] };
|
|
41
44
|
}
|
|
42
|
-
const cfg = resolveConfig(config);
|
|
43
45
|
if (cfg.allowedHosts.length === 0) {
|
|
44
46
|
return { valid: true, errors: [] };
|
|
45
47
|
}
|
|
@@ -11,4 +11,6 @@ export type { SecurityValidationResult } from "./httpSecurity.validator.js";
|
|
|
11
11
|
export { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
|
|
12
12
|
export type { GuardableRequest, GuardResult } from "./httpSecurity.guard.js";
|
|
13
13
|
export { guardRequest, createRequestGuard, assertRequestAllowed, HttpRequestGuardError, } from "./httpSecurity.guard.js";
|
|
14
|
+
export type { NodeAdapterSecurityOption, NodeAdapterSecurityOptions, NodeRequestGuard, } from "./httpSecurity.nodeGuard.js";
|
|
15
|
+
export { createNodeRequestGuard } from "./httpSecurity.nodeGuard.js";
|
|
14
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
export { DEFAULT_SECURITY_CONFIG } from "./httpSecurity.config.js";
|
|
9
9
|
export { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
|
|
10
10
|
export { guardRequest, createRequestGuard, assertRequestAllowed, HttpRequestGuardError, } from "./httpSecurity.guard.js";
|
|
11
|
+
export { createNodeRequestGuard } from "./httpSecurity.nodeGuard.js";
|
|
11
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -45,6 +45,12 @@ export declare class HttpServer {
|
|
|
45
45
|
close(): Promise<this>;
|
|
46
46
|
setHandler(handler: HttpHandler): this;
|
|
47
47
|
setErrorHandler(handler: HttpErrorHandler): this;
|
|
48
|
+
/**
|
|
49
|
+
* Installs `handler` on the adapter, wrapped so every dispatch is counted
|
|
50
|
+
* (`requests`, `onRequest`) and its completion reported (`onResponse`).
|
|
51
|
+
* Nothing called `recordRequest`/`recordResponse` before, so the counter
|
|
52
|
+
* and both events stayed at zero.
|
|
53
|
+
*/
|
|
48
54
|
private adapterHandler;
|
|
49
55
|
private adapterErrorHandler;
|
|
50
56
|
recordRequest(): void;
|
|
@@ -61,6 +67,11 @@ export declare class HttpServer {
|
|
|
61
67
|
snapshot(): HttpServerSnapshot;
|
|
62
68
|
toJSON(): HttpServerSnapshot;
|
|
63
69
|
private performStart;
|
|
70
|
+
/**
|
|
71
|
+
* The adapter is told the shutdown timeout as its grace period, so
|
|
72
|
+
* in-flight requests get the documented `gracefulShutdownTimeout` rather
|
|
73
|
+
* than the Node adapter's hidden 10 s default.
|
|
74
|
+
*/
|
|
64
75
|
private performStop;
|
|
65
76
|
private refreshAddress;
|
|
66
77
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { isHttpAdapter, startAdapter, stopAdapter, } from "../../httpAdapter/http.adapter.js";
|
|
7
7
|
import { HttpServerLifecycleError, InvalidHttpServerStateError, HttpServerStartError, HttpServerStopError, } from "@zudojs/errors";
|
|
8
8
|
import { withTimeout, validateShutdownTimeout, } from "../factory/httpServer.factory.js";
|
|
9
|
+
const SHUTDOWN_CLOSE_MARGIN_MS = 1_000;
|
|
9
10
|
export class HttpServer {
|
|
10
11
|
name;
|
|
11
12
|
adapter;
|
|
@@ -170,7 +171,22 @@ export class HttpServer {
|
|
|
170
171
|
this.adapterErrorHandler(handler);
|
|
171
172
|
return this;
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Installs `handler` on the adapter, wrapped so every dispatch is counted
|
|
176
|
+
* (`requests`, `onRequest`) and its completion reported (`onResponse`).
|
|
177
|
+
* Nothing called `recordRequest`/`recordResponse` before, so the counter
|
|
178
|
+
* and both events stayed at zero.
|
|
179
|
+
*/
|
|
180
|
+
adapterHandler(application) {
|
|
181
|
+
const handler = async (request) => {
|
|
182
|
+
this.recordRequest();
|
|
183
|
+
try {
|
|
184
|
+
return await application(request);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.recordResponse();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
174
190
|
const adapter = this.adapter;
|
|
175
191
|
if (typeof adapter.setHandler === "function") {
|
|
176
192
|
adapter.setHandler(handler);
|
|
@@ -262,13 +278,23 @@ export class HttpServer {
|
|
|
262
278
|
async performStart() {
|
|
263
279
|
await startAdapter(this.adapter);
|
|
264
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* The adapter is told the shutdown timeout as its grace period, so
|
|
283
|
+
* in-flight requests get the documented `gracefulShutdownTimeout` rather
|
|
284
|
+
* than the Node adapter's hidden 10 s default.
|
|
285
|
+
*/
|
|
286
|
+
/*
|
|
287
|
+
* The adapter gets the full timeout as its grace period and then destroys
|
|
288
|
+
* the remaining sockets; the margin only lets that close complete before
|
|
289
|
+
* the stop is reported as timed out.
|
|
290
|
+
*/
|
|
265
291
|
async performStop(force, timeout) {
|
|
266
292
|
if (force) {
|
|
267
|
-
await stopAdapter(this.adapter);
|
|
293
|
+
await stopAdapter(this.adapter, { graceMs: 0 });
|
|
268
294
|
return;
|
|
269
295
|
}
|
|
270
296
|
try {
|
|
271
|
-
await withTimeout(stopAdapter(this.adapter), timeout, "HTTP server shutdown timed out.");
|
|
297
|
+
await withTimeout(stopAdapter(this.adapter, { graceMs: timeout }), timeout + SHUTDOWN_CLOSE_MARGIN_MS, "HTTP server shutdown timed out.");
|
|
272
298
|
}
|
|
273
299
|
catch (error) {
|
|
274
300
|
/*
|
|
@@ -277,7 +303,7 @@ export class HttpServer {
|
|
|
277
303
|
* rebind the port. Escalate to a forced stop before rethrowing.
|
|
278
304
|
*/
|
|
279
305
|
try {
|
|
280
|
-
await stopAdapter(this.adapter);
|
|
306
|
+
await stopAdapter(this.adapter, { graceMs: 0 });
|
|
281
307
|
}
|
|
282
308
|
catch {
|
|
283
309
|
/* The original timeout is the more useful error. */
|
|
@@ -23,10 +23,13 @@ export type TrustProxyPredicate = (value: string, index: number) => boolean;
|
|
|
23
23
|
* - `"loopback"`, `"linklocal"`, `"uniquelocal"` / `"private"` — named ranges.
|
|
24
24
|
* - an IP address, a CIDR range, a comma-separated list of either, or an array
|
|
25
25
|
* of the same.
|
|
26
|
+
* - a hop count `n`: trust the `n` hops nearest the server (the socket peer
|
|
27
|
+
* is hop `0`), as Express/`proxy-addr` do. `0` trusts nothing.
|
|
26
28
|
* - a custom predicate `(address, hopIndexFromPeer) => boolean`.
|
|
27
29
|
*
|
|
28
|
-
* Throws `TypeError` on a string that is none of the above
|
|
29
|
-
*
|
|
30
|
+
* Throws `TypeError` on a string that is none of the above, or on a hop
|
|
31
|
+
* count that is not a non-negative integer, rather than returning a
|
|
32
|
+
* predicate that can never match.
|
|
30
33
|
*/
|
|
31
34
|
export declare function compileTrustProxy(trustProxy: TrustProxy): TrustProxyPredicate;
|
|
32
35
|
//# sourceMappingURL=httpTrustProxy.compilation.d.ts.map
|