@zudojs/http 1.1.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 +53 -3
- package/dist/httpAdapter/http.adapter.d.ts +34 -3
- package/dist/httpAdapter/http.adapter.js +22 -6
- package/dist/httpAdapter/http.adapters.js +4 -34
- package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
- package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
- 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 +12 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
- package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
- package/dist/httpAdapter/node/httpNode.request.js +17 -27
- 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/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 +18 -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.helper.js +2 -1
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +14 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
- 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 +5 -5
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy request-path containment.
|
|
3
|
+
*
|
|
4
|
+
* A gateway mounts an upstream under `target.pathname` (for example
|
|
5
|
+
* `/public-api`) to expose only that subtree. The path builders used to
|
|
6
|
+
* concatenate the client's path onto the base and collapse `//` only, so
|
|
7
|
+
* `/../admin/keys` or `/%2e%2e/admin/keys` was forwarded as
|
|
8
|
+
* `/public-api/../admin/keys`, which the upstream resolves to `/admin/keys`.
|
|
9
|
+
*
|
|
10
|
+
* @module httpProxy/pathGuard
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Whether a request path contains a `.` / `..` segment once percent-encoded
|
|
14
|
+
* dots and separators (`%2e`, `%2f`, `%5c`) and backslashes are taken into
|
|
15
|
+
* account, i.e. whether an upstream might resolve it outside its base.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasProxyDotSegment(path: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Throws a `400 Bad Request` `HttpError` when the client-supplied path could
|
|
20
|
+
* escape the proxy target's base path.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertProxyPathContained(path: string): void;
|
|
23
|
+
//# sourceMappingURL=httpProxy.pathGuard.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy request-path containment.
|
|
3
|
+
*
|
|
4
|
+
* A gateway mounts an upstream under `target.pathname` (for example
|
|
5
|
+
* `/public-api`) to expose only that subtree. The path builders used to
|
|
6
|
+
* concatenate the client's path onto the base and collapse `//` only, so
|
|
7
|
+
* `/../admin/keys` or `/%2e%2e/admin/keys` was forwarded as
|
|
8
|
+
* `/public-api/../admin/keys`, which the upstream resolves to `/admin/keys`.
|
|
9
|
+
*
|
|
10
|
+
* @module httpProxy/pathGuard
|
|
11
|
+
*/
|
|
12
|
+
import { badRequest } from "../httpErrors/factories/httpError.clientError.js";
|
|
13
|
+
const ENCODED_SEPARATORS = /%2e|%2f|%5c/gi;
|
|
14
|
+
function decodeSeparator(match) {
|
|
15
|
+
const code = match.toLowerCase();
|
|
16
|
+
if (code === "%2e") {
|
|
17
|
+
return ".";
|
|
18
|
+
}
|
|
19
|
+
return code === "%2f" ? "/" : "\\";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether a request path contains a `.` / `..` segment once percent-encoded
|
|
23
|
+
* dots and separators (`%2e`, `%2f`, `%5c`) and backslashes are taken into
|
|
24
|
+
* account, i.e. whether an upstream might resolve it outside its base.
|
|
25
|
+
*/
|
|
26
|
+
export function hasProxyDotSegment(path) {
|
|
27
|
+
const end = path.search(/[?#]/);
|
|
28
|
+
const pathOnly = end === -1 ? path : path.slice(0, end);
|
|
29
|
+
const decoded = pathOnly.replace(ENCODED_SEPARATORS, decodeSeparator);
|
|
30
|
+
return decoded
|
|
31
|
+
.split(/[\\/]/)
|
|
32
|
+
.some((segment) => segment === "." || segment === "..");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Throws a `400 Bad Request` `HttpError` when the client-supplied path could
|
|
36
|
+
* escape the proxy target's base path.
|
|
37
|
+
*/
|
|
38
|
+
export function assertProxyPathContained(path) {
|
|
39
|
+
if (hasProxyDotSegment(path)) {
|
|
40
|
+
throw badRequest("Proxy request path contains a dot segment.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=httpProxy.pathGuard.js.map
|
package/dist/httpProxy/index.js
CHANGED
|
@@ -117,6 +117,13 @@ export declare function runWithRequestContext<T>(context: HttpRequestContext, ca
|
|
|
117
117
|
export declare function getCurrentRequestContext(): HttpRequestContext | undefined;
|
|
118
118
|
export declare function assertRequestContext(context: HttpRequestContext | undefined | null): asserts context is HttpRequestContext;
|
|
119
119
|
export declare function isRequestContext(value: unknown): value is HttpRequestContext;
|
|
120
|
+
/**
|
|
121
|
+
* Returns the canonical path of a request-target. An origin-form target such
|
|
122
|
+
* as `//host/admin` stays a path; it is never parsed as an authority.
|
|
123
|
+
*/
|
|
120
124
|
export declare function getPathname(url: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* Returns the search parameters of a request-target.
|
|
127
|
+
*/
|
|
121
128
|
export declare function getSearchParams(url: string): URLSearchParams;
|
|
122
129
|
//# sourceMappingURL=httpRequest.context.d.ts.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* The context is intentionally framework-agnostic so adapters can populate it
|
|
8
8
|
* from Node.js, Bun, Deno, or another HTTP runtime.
|
|
9
9
|
*/
|
|
10
|
+
import { parseRequestTarget } from "./target/httpRequest.target.js";
|
|
10
11
|
/* -------------------------------------------------------------------------- */
|
|
11
12
|
/* Constants */
|
|
12
13
|
/* -------------------------------------------------------------------------- */
|
|
@@ -98,7 +99,11 @@ export class HttpRequestContext {
|
|
|
98
99
|
/* Query */
|
|
99
100
|
/* ------------------------------------------------------------------------ */
|
|
100
101
|
get query() {
|
|
101
|
-
|
|
102
|
+
const query = Object.create(null);
|
|
103
|
+
for (const [name, value] of this.queryMap) {
|
|
104
|
+
query[name] = value;
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze(query);
|
|
102
107
|
}
|
|
103
108
|
hasQuery(name) {
|
|
104
109
|
return this.queryMap.has(name);
|
|
@@ -326,24 +331,18 @@ export function isRequestContext(value) {
|
|
|
326
331
|
/* -------------------------------------------------------------------------- */
|
|
327
332
|
/* URL Helpers */
|
|
328
333
|
/* -------------------------------------------------------------------------- */
|
|
334
|
+
/**
|
|
335
|
+
* Returns the canonical path of a request-target. An origin-form target such
|
|
336
|
+
* as `//host/admin` stays a path; it is never parsed as an authority.
|
|
337
|
+
*/
|
|
329
338
|
export function getPathname(url) {
|
|
330
|
-
|
|
331
|
-
const parsed = new URL(url, "http://zudojs.invalid");
|
|
332
|
-
return parsed.pathname || "/";
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
const pathname = url.split("?", 1)[0];
|
|
336
|
-
return pathname || "/";
|
|
337
|
-
}
|
|
339
|
+
return parseRequestTarget(url).pathname || "/";
|
|
338
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Returns the search parameters of a request-target.
|
|
343
|
+
*/
|
|
339
344
|
export function getSearchParams(url) {
|
|
340
|
-
|
|
341
|
-
return new URL(url, "http://zudojs.invalid").searchParams;
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
345
|
-
return new URLSearchParams(query);
|
|
346
|
-
}
|
|
345
|
+
return parseRequestTarget(url).searchParams;
|
|
347
346
|
}
|
|
348
347
|
/* -------------------------------------------------------------------------- */
|
|
349
348
|
/* Internal Helpers */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical request-target parsing.
|
|
3
|
+
*
|
|
4
|
+
* Every place that derives a path or query from `request.url` (the request
|
|
5
|
+
* context, the router, path-scoped middleware, static files) parses it here,
|
|
6
|
+
* so routing and middleware can never disagree about which path a request
|
|
7
|
+
* addresses.
|
|
8
|
+
*
|
|
9
|
+
* `new URL(target, base)` is the wrong tool for an origin-form target:
|
|
10
|
+
* `//evil/admin` is read as a scheme-relative URL with authority `evil`, so
|
|
11
|
+
* the path silently became `/admin` while `request.url` still said
|
|
12
|
+
* `//evil/admin`. Prefixing the base as a string keeps an origin-form target
|
|
13
|
+
* a path.
|
|
14
|
+
*
|
|
15
|
+
* @module httpRequest/target
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Parses a request-target (origin-form, absolute-form or `*`) into a URL.
|
|
19
|
+
*
|
|
20
|
+
* An origin-form target is never parsed as an authority. Unparseable input
|
|
21
|
+
* yields the root URL.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseRequestTarget(target: string): URL;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the canonical path of a request-target.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getCanonicalPath(target: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Explains why a request-target is refused, or returns `undefined` when it is
|
|
30
|
+
* acceptable.
|
|
31
|
+
*
|
|
32
|
+
* Refused: anything that is not origin-form, absolute-form (`http(s)://`) or
|
|
33
|
+
* the asterisk-form `*`; a backslash (WHATWG URL parsing treats it as a
|
|
34
|
+
* separator); and any `.` / `..` segment, plain or percent-encoded
|
|
35
|
+
* (`%2e%2e`). Those segments are resolved away by URL parsing, so a front
|
|
36
|
+
* proxy matching `/admin*` on the raw target and the router dispatching on
|
|
37
|
+
* the resolved path would otherwise disagree about the same request.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findRequestTargetViolation(target: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a request-target is acceptable (see
|
|
42
|
+
* {@link findRequestTargetViolation}).
|
|
43
|
+
*/
|
|
44
|
+
export declare function isCanonicalRequestTarget(target: string): boolean;
|
|
45
|
+
//# sourceMappingURL=httpRequest.target.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical request-target parsing.
|
|
3
|
+
*
|
|
4
|
+
* Every place that derives a path or query from `request.url` (the request
|
|
5
|
+
* context, the router, path-scoped middleware, static files) parses it here,
|
|
6
|
+
* so routing and middleware can never disagree about which path a request
|
|
7
|
+
* addresses.
|
|
8
|
+
*
|
|
9
|
+
* `new URL(target, base)` is the wrong tool for an origin-form target:
|
|
10
|
+
* `//evil/admin` is read as a scheme-relative URL with authority `evil`, so
|
|
11
|
+
* the path silently became `/admin` while `request.url` still said
|
|
12
|
+
* `//evil/admin`. Prefixing the base as a string keeps an origin-form target
|
|
13
|
+
* a path.
|
|
14
|
+
*
|
|
15
|
+
* @module httpRequest/target
|
|
16
|
+
*/
|
|
17
|
+
const TARGET_BASE = "http://zudojs.invalid";
|
|
18
|
+
const ABSOLUTE_FORM = /^https?:\/\//i;
|
|
19
|
+
const ENCODED_DOT = /%2e/gi;
|
|
20
|
+
/**
|
|
21
|
+
* Parses a request-target (origin-form, absolute-form or `*`) into a URL.
|
|
22
|
+
*
|
|
23
|
+
* An origin-form target is never parsed as an authority. Unparseable input
|
|
24
|
+
* yields the root URL.
|
|
25
|
+
*/
|
|
26
|
+
export function parseRequestTarget(target) {
|
|
27
|
+
try {
|
|
28
|
+
if (target.startsWith("/")) {
|
|
29
|
+
return new URL(`${TARGET_BASE}${target}`);
|
|
30
|
+
}
|
|
31
|
+
if (ABSOLUTE_FORM.test(target)) {
|
|
32
|
+
return new URL(target);
|
|
33
|
+
}
|
|
34
|
+
return new URL(`${TARGET_BASE}/${target === "*" ? "" : target}`);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return new URL(`${TARGET_BASE}/`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the canonical path of a request-target.
|
|
42
|
+
*/
|
|
43
|
+
export function getCanonicalPath(target) {
|
|
44
|
+
return parseRequestTarget(target).pathname || "/";
|
|
45
|
+
}
|
|
46
|
+
function rawPathOf(target) {
|
|
47
|
+
let path = target;
|
|
48
|
+
if (ABSOLUTE_FORM.test(path)) {
|
|
49
|
+
const afterScheme = path.indexOf("//") + 2;
|
|
50
|
+
const slash = path.indexOf("/", afterScheme);
|
|
51
|
+
path = slash === -1 ? "/" : path.slice(slash);
|
|
52
|
+
}
|
|
53
|
+
const end = path.search(/[?#]/);
|
|
54
|
+
return end === -1 ? path : path.slice(0, end);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Explains why a request-target is refused, or returns `undefined` when it is
|
|
58
|
+
* acceptable.
|
|
59
|
+
*
|
|
60
|
+
* Refused: anything that is not origin-form, absolute-form (`http(s)://`) or
|
|
61
|
+
* the asterisk-form `*`; a backslash (WHATWG URL parsing treats it as a
|
|
62
|
+
* separator); and any `.` / `..` segment, plain or percent-encoded
|
|
63
|
+
* (`%2e%2e`). Those segments are resolved away by URL parsing, so a front
|
|
64
|
+
* proxy matching `/admin*` on the raw target and the router dispatching on
|
|
65
|
+
* the resolved path would otherwise disagree about the same request.
|
|
66
|
+
*/
|
|
67
|
+
export function findRequestTargetViolation(target) {
|
|
68
|
+
if (target === "*") {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (!target.startsWith("/") && !ABSOLUTE_FORM.test(target)) {
|
|
72
|
+
return "Request target is not in origin-form or absolute-form.";
|
|
73
|
+
}
|
|
74
|
+
const path = rawPathOf(target);
|
|
75
|
+
if (path.includes("\\")) {
|
|
76
|
+
return "Request target contains a backslash.";
|
|
77
|
+
}
|
|
78
|
+
for (const segment of path.split("/")) {
|
|
79
|
+
const decoded = segment.replace(ENCODED_DOT, ".");
|
|
80
|
+
if (decoded === "." || decoded === "..") {
|
|
81
|
+
return "Request target contains a dot segment.";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a request-target is acceptable (see
|
|
88
|
+
* {@link findRequestTargetViolation}).
|
|
89
|
+
*/
|
|
90
|
+
export function isCanonicalRequestTarget(target) {
|
|
91
|
+
return findRequestTargetViolation(target) === undefined;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=httpRequest.target.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpRequest/target
|
|
3
|
+
*
|
|
4
|
+
* Canonical request-target parsing shared by the request context, router and
|
|
5
|
+
* path-scoped middleware, plus the dot-segment / non-origin-form check the
|
|
6
|
+
* Node adapter applies before a request is dispatched.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./httpRequest.target.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpRequest/target
|
|
3
|
+
*
|
|
4
|
+
* Canonical request-target parsing shared by the request context, router and
|
|
5
|
+
* path-scoped middleware, plus the dot-segment / non-origin-form check the
|
|
6
|
+
* Node adapter applies before a request is dispatched.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./httpRequest.target.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpResponse/helpers
|
|
5
5
|
*/
|
|
6
|
+
import { withSecureCookieDefaults } from "../httpCookies/httpCookies.defaults.js";
|
|
6
7
|
import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
|
|
7
8
|
/**
|
|
8
9
|
* Creates a JSON response.
|
|
@@ -110,7 +111,7 @@ export function serializeResponseCookie(cookie) {
|
|
|
110
111
|
if (!name || !COOKIE_NAME_TOKEN.test(name)) {
|
|
111
112
|
throw new TypeError(`Invalid cookie name: ${JSON.stringify(name)}`);
|
|
112
113
|
}
|
|
113
|
-
const opts = cookie.options
|
|
114
|
+
const opts = withSecureCookieDefaults(cookie.options, "Lax");
|
|
114
115
|
if (name.startsWith("__Host-")) {
|
|
115
116
|
if (!opts.secure) {
|
|
116
117
|
throw new TypeError("A __Host- cookie requires the Secure attribute.");
|
|
@@ -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 */
|
|
@@ -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
|
}
|