@zudojs/http 1.1.0 → 1.3.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.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +24 -45
- 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/httpAgent/http.agent.d.ts +22 -3
- package/dist/httpAgent/http.agent.js +52 -14
- package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
- package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -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/httpKeepAlive/httpKeepAlive.core.js +14 -2
- 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/logging/httpMiddleware.logging.d.ts +18 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
- 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/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- 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/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpProxy/http.proxy.d.ts +16 -0
- package/dist/httpProxy/http.proxy.js +44 -3
- 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/httpQuery/index.d.ts +11 -2
- package/dist/httpQuery/index.js +11 -2
- package/dist/httpQuery/queryParse/index.d.ts +10 -0
- package/dist/httpQuery/queryParse/index.js +10 -0
- package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
- package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
- package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
- package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
- package/dist/httpQuery/queryRequest/index.d.ts +9 -0
- package/dist/httpQuery/queryRequest/index.js +9 -0
- package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
- package/dist/httpQuery/queryRequest/query.request.js +96 -0
- package/dist/httpQuery/querySerialize/index.d.ts +10 -0
- package/dist/httpQuery/querySerialize/index.js +10 -0
- package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
- package/dist/httpQuery/querySerialize/query.util.js +67 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
- package/dist/httpQuery/queryTypes/index.d.ts +11 -0
- package/dist/httpQuery/queryTypes/index.js +9 -0
- package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
- package/dist/httpQuery/queryTypes/query.container.js +51 -0
- package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
- package/dist/httpQuery/queryTypes/query.limit.js +32 -0
- package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
- package/dist/httpQuery/queryTypes/query.type.js +2 -0
- package/dist/httpRedirect/http.redirect.d.ts +6 -0
- package/dist/httpRedirect/http.redirect.js +53 -2
- package/dist/httpRequest/http.request.d.ts +61 -2
- package/dist/httpRequest/http.request.js +86 -35
- package/dist/httpRequest/httpRequest.context.d.ts +7 -0
- package/dist/httpRequest/httpRequest.context.js +26 -34
- 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 +111 -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/factory/httpRoute.factory.base.d.ts +18 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
- package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
- package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
- package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
- package/dist/httpRouter/pattern/index.d.ts +1 -1
- package/dist/httpRouter/pattern/index.js +1 -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 +20 -9
- 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
|
@@ -20,6 +20,7 @@ export interface CompiledRoutePath {
|
|
|
20
20
|
readonly segments: readonly CompiledSegment[];
|
|
21
21
|
readonly score: number;
|
|
22
22
|
readonly strictTrailingSlash: boolean;
|
|
23
|
+
readonly expectsTrailingSlash: boolean;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Compiles a route path into its segments.
|
|
@@ -35,4 +36,19 @@ export declare function compileRoute(path: string, strictTrailingSlash: boolean)
|
|
|
35
36
|
* Scores compiled segments by specificity.
|
|
36
37
|
*/
|
|
37
38
|
export declare function scoreSegments(segments: readonly CompiledSegment[]): number;
|
|
39
|
+
/**
|
|
40
|
+
* Compares two compiled patterns by specificity, most specific first.
|
|
41
|
+
*
|
|
42
|
+
* Segments are compared left to right by kind (literal, then parameter, then
|
|
43
|
+
* wildcard), which is how a router is expected to rank patterns. Summing the
|
|
44
|
+
* kinds into one scalar — as this used to — let a longer but entirely
|
|
45
|
+
* parameterised pattern such as `/:p/:q/:r/:s` outrank a literal-anchored
|
|
46
|
+
* `/admin/*rest`, so a request to `/admin/a/b/c` bypassed the admin route and
|
|
47
|
+
* every guard registered on it.
|
|
48
|
+
*
|
|
49
|
+
* @param left - The first pattern's segments.
|
|
50
|
+
* @param right - The second pattern's segments.
|
|
51
|
+
* @returns A negative number when `left` is more specific.
|
|
52
|
+
*/
|
|
53
|
+
export declare function compareSegmentSpecificity(left: readonly CompiledSegment[], right: readonly CompiledSegment[]): number;
|
|
38
54
|
//# sourceMappingURL=httpRoute.pattern.parse.d.ts.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - `/assets/*path` trailing wildcard
|
|
14
14
|
*/
|
|
15
15
|
import { InvalidRoutePatternError } from "../core/error/httpRouter.error.js";
|
|
16
|
-
import {
|
|
16
|
+
import { hasTrailingSlash, splitRoutePattern, validateParameterName, } from "../core/util/httpRoute.util.js";
|
|
17
17
|
const SEGMENT_SCORE_LITERAL = 3;
|
|
18
18
|
const SEGMENT_SCORE_PARAMETER = 2;
|
|
19
19
|
const SEGMENT_SCORE_WILDCARD = 1;
|
|
@@ -22,7 +22,7 @@ const SEGMENT_SCORE_WILDCARD = 1;
|
|
|
22
22
|
*/
|
|
23
23
|
export function compileRouteSegments(path) {
|
|
24
24
|
const segments = [];
|
|
25
|
-
const parts =
|
|
25
|
+
const parts = splitRoutePattern(path);
|
|
26
26
|
for (const part of parts) {
|
|
27
27
|
if (part.startsWith(":")) {
|
|
28
28
|
segments.push(parseParameter(part, path));
|
|
@@ -56,6 +56,7 @@ export function compileRoute(path, strictTrailingSlash) {
|
|
|
56
56
|
segments,
|
|
57
57
|
score: scoreSegments(segments),
|
|
58
58
|
strictTrailingSlash,
|
|
59
|
+
expectsTrailingSlash: hasTrailingSlash(path),
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
@@ -64,18 +65,46 @@ export function compileRoute(path, strictTrailingSlash) {
|
|
|
64
65
|
export function scoreSegments(segments) {
|
|
65
66
|
let score = 0;
|
|
66
67
|
for (const segment of segments) {
|
|
67
|
-
|
|
68
|
-
score += SEGMENT_SCORE_LITERAL;
|
|
69
|
-
}
|
|
70
|
-
else if (segment.type === "parameter") {
|
|
71
|
-
score += SEGMENT_SCORE_PARAMETER;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
score += SEGMENT_SCORE_WILDCARD;
|
|
75
|
-
}
|
|
68
|
+
score += segmentScore(segment);
|
|
76
69
|
}
|
|
77
70
|
return score;
|
|
78
71
|
}
|
|
72
|
+
function segmentScore(segment) {
|
|
73
|
+
if (segment === undefined) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (segment.type === "literal") {
|
|
77
|
+
return SEGMENT_SCORE_LITERAL;
|
|
78
|
+
}
|
|
79
|
+
if (segment.type === "parameter") {
|
|
80
|
+
return SEGMENT_SCORE_PARAMETER;
|
|
81
|
+
}
|
|
82
|
+
return SEGMENT_SCORE_WILDCARD;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compares two compiled patterns by specificity, most specific first.
|
|
86
|
+
*
|
|
87
|
+
* Segments are compared left to right by kind (literal, then parameter, then
|
|
88
|
+
* wildcard), which is how a router is expected to rank patterns. Summing the
|
|
89
|
+
* kinds into one scalar — as this used to — let a longer but entirely
|
|
90
|
+
* parameterised pattern such as `/:p/:q/:r/:s` outrank a literal-anchored
|
|
91
|
+
* `/admin/*rest`, so a request to `/admin/a/b/c` bypassed the admin route and
|
|
92
|
+
* every guard registered on it.
|
|
93
|
+
*
|
|
94
|
+
* @param left - The first pattern's segments.
|
|
95
|
+
* @param right - The second pattern's segments.
|
|
96
|
+
* @returns A negative number when `left` is more specific.
|
|
97
|
+
*/
|
|
98
|
+
export function compareSegmentSpecificity(left, right) {
|
|
99
|
+
const length = Math.max(left.length, right.length);
|
|
100
|
+
for (let index = 0; index < length; index += 1) {
|
|
101
|
+
const difference = segmentScore(right[index]) - segmentScore(left[index]);
|
|
102
|
+
if (difference !== 0) {
|
|
103
|
+
return difference;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
79
108
|
function parseParameter(segment, path) {
|
|
80
109
|
let value = segment.slice(1);
|
|
81
110
|
let optional = false;
|
|
@@ -14,6 +14,6 @@ export { parseSegments } from "./core/httpPattern.segmentParsing.js";
|
|
|
14
14
|
export { buildRegex } from "./core/httpPattern.regexBuilding.js";
|
|
15
15
|
export { testRoutePattern, matchRoutePattern, matchRoutePatterns, testRoutePatterns, } from "./core/httpPattern.matching.js";
|
|
16
16
|
export { createRoutePattern, createStrictRoutePattern, createCaseInsensitiveRoutePattern, } from "./httpPattern.factory.js";
|
|
17
|
-
export { compileRoute, compileRouteSegments, scoreSegments, } from "./httpRoute.pattern.parse.js";
|
|
17
|
+
export { compileRoute, compileRouteSegments, scoreSegments, compareSegmentSpecificity, } from "./httpRoute.pattern.parse.js";
|
|
18
18
|
export type { CompiledRoutePath } from "./httpRoute.pattern.parse.js";
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,5 +13,5 @@ export { parseSegments } from "./core/httpPattern.segmentParsing.js";
|
|
|
13
13
|
export { buildRegex } from "./core/httpPattern.regexBuilding.js";
|
|
14
14
|
export { testRoutePattern, matchRoutePattern, matchRoutePatterns, testRoutePatterns, } from "./core/httpPattern.matching.js";
|
|
15
15
|
export { createRoutePattern, createStrictRoutePattern, createCaseInsensitiveRoutePattern, } from "./httpPattern.factory.js";
|
|
16
|
-
export { compileRoute, compileRouteSegments, scoreSegments, } from "./httpRoute.pattern.parse.js";
|
|
16
|
+
export { compileRoute, compileRouteSegments, scoreSegments, compareSegmentSpecificity, } from "./httpRoute.pattern.parse.js";
|
|
17
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -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
|
|
@@ -20,14 +20,23 @@ export function validateHeaders(headers, config) {
|
|
|
20
20
|
if (entries.length > cfg.maxHeaders) {
|
|
21
21
|
errors.push(`Too many headers: ${entries.length} > ${cfg.maxHeaders}`);
|
|
22
22
|
}
|
|
23
|
+
/*
|
|
24
|
+
* `IncomingMessage.headers` returns an array for `set-cookie` and for any
|
|
25
|
+
* header a non-Node caller supplies as a list. Checking only `typeof value
|
|
26
|
+
* === "string"` let every one of those values past the size cap and the
|
|
27
|
+
* CRLF filter while the guard still reported `allowed: true`.
|
|
28
|
+
*/
|
|
23
29
|
for (const [key, value] of entries) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
for (const element of Array.isArray(value) ? value : [value]) {
|
|
31
|
+
if (typeof element !== "string") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (element.length > cfg.maxHeaderValueSize) {
|
|
35
|
+
errors.push(`Header "${key}" value too large: ${element.length} > ${cfg.maxHeaderValueSize}`);
|
|
36
|
+
}
|
|
37
|
+
if (cfg.enableCrlfProtection && /[\r\n]/.test(element)) {
|
|
38
|
+
errors.push(`Header "${key}" contains CRLF characters`);
|
|
39
|
+
}
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
return { valid: errors.length === 0, errors };
|
|
@@ -36,10 +45,12 @@ export function validateHeaders(headers, config) {
|
|
|
36
45
|
* Validate the Host header against allowed hosts.
|
|
37
46
|
*/
|
|
38
47
|
export function validateHost(host, config) {
|
|
48
|
+
const cfg = resolveConfig(config);
|
|
39
49
|
if (!host) {
|
|
40
|
-
return
|
|
50
|
+
return cfg.requireHost
|
|
51
|
+
? { valid: false, errors: ["Missing Host header"] }
|
|
52
|
+
: { valid: true, errors: [] };
|
|
41
53
|
}
|
|
42
|
-
const cfg = resolveConfig(config);
|
|
43
54
|
if (cfg.allowedHosts.length === 0) {
|
|
44
55
|
return { valid: true, errors: [] };
|
|
45
56
|
}
|
|
@@ -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
|
|
@@ -88,10 +88,13 @@ function buildListPredicate(trustProxy) {
|
|
|
88
88
|
* - `"loopback"`, `"linklocal"`, `"uniquelocal"` / `"private"` — named ranges.
|
|
89
89
|
* - an IP address, a CIDR range, a comma-separated list of either, or an array
|
|
90
90
|
* of the same.
|
|
91
|
+
* - a hop count `n`: trust the `n` hops nearest the server (the socket peer
|
|
92
|
+
* is hop `0`), as Express/`proxy-addr` do. `0` trusts nothing.
|
|
91
93
|
* - a custom predicate `(address, hopIndexFromPeer) => boolean`.
|
|
92
94
|
*
|
|
93
|
-
* Throws `TypeError` on a string that is none of the above
|
|
94
|
-
*
|
|
95
|
+
* Throws `TypeError` on a string that is none of the above, or on a hop
|
|
96
|
+
* count that is not a non-negative integer, rather than returning a
|
|
97
|
+
* predicate that can never match.
|
|
95
98
|
*/
|
|
96
99
|
export function compileTrustProxy(trustProxy) {
|
|
97
100
|
if (typeof trustProxy === "function") {
|
|
@@ -100,6 +103,9 @@ export function compileTrustProxy(trustProxy) {
|
|
|
100
103
|
if (trustProxy === true) {
|
|
101
104
|
return () => true;
|
|
102
105
|
}
|
|
106
|
+
if (typeof trustProxy === "number") {
|
|
107
|
+
return compileHopCount(trustProxy);
|
|
108
|
+
}
|
|
103
109
|
/* Anything that is not a string or a list (including `false`, `undefined`
|
|
104
110
|
* and any malformed runtime value) trusts nothing. */
|
|
105
111
|
if (typeof trustProxy !== "string" && !Array.isArray(trustProxy)) {
|
|
@@ -116,4 +122,15 @@ export function compileTrustProxy(trustProxy) {
|
|
|
116
122
|
compilationCache.set(trustProxy, predicate);
|
|
117
123
|
return predicate;
|
|
118
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Hop-count trust. The numeric form was typed and documented ("trust N
|
|
127
|
+
* proxies") but fell through to "trust nothing", so behind a load balancer
|
|
128
|
+
* every client shared the balancer's address.
|
|
129
|
+
*/
|
|
130
|
+
function compileHopCount(hops) {
|
|
131
|
+
if (!Number.isSafeInteger(hops) || hops < 0) {
|
|
132
|
+
throw new TypeError(`trustProxy hop count must be a non-negative integer, got ${hops}.`);
|
|
133
|
+
}
|
|
134
|
+
return (_address, index) => index < hops;
|
|
135
|
+
}
|
|
119
136
|
//# sourceMappingURL=httpTrustProxy.compilation.js.map
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpTrustProxy/types
|
|
5
5
|
*/
|
|
6
|
-
export type TrustProxy = boolean
|
|
6
|
+
export type TrustProxy = boolean
|
|
7
|
+
/** Hop count: trust the `n` hops nearest the server (the peer is hop 0). */
|
|
8
|
+
| number | "all" | "linklocal" | "loopback" | string | readonly string[] | ((value: string, index: number) => boolean);
|
|
7
9
|
export interface TrustProxyOptions {
|
|
8
10
|
readonly trustProxy?: TrustProxy;
|
|
9
11
|
readonly maxDepth?: number;
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* TypeScript reports TS2308 (ambiguous re-exports) for these duplicates.
|
|
8
8
|
* This is intentional — consumers can import from either path.
|
|
9
9
|
*/
|
|
10
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
11
10
|
export * from "./httpProtocol/index.js";
|
|
12
11
|
export * from "./httpTypes/index.js";
|
|
13
12
|
export * from "./httpConstants/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/http",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "HTTP primitives, request handling, routing, middleware, and server infrastructure for Zudojs applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"!dist/.tsbuildinfo"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zudojs/
|
|
28
|
-
"@zudojs/errors": "1.0
|
|
29
|
-
"@zudojs/logger": "1.
|
|
30
|
-
"@zudojs/security": "1.0
|
|
27
|
+
"@zudojs/crypto": "1.3.0",
|
|
28
|
+
"@zudojs/errors": "1.2.0",
|
|
29
|
+
"@zudojs/logger": "1.3.0",
|
|
30
|
+
"@zudojs/security": "1.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^26.4.1",
|