@zudojs/http 1.2.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/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +20 -11
- package/dist/httpAdapter/node/httpNode.request.js +1 -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/httpKeepAlive/httpKeepAlive.core.js +14 -2
- 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/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpProxy/http.proxy.d.ts +9 -0
- package/dist/httpProxy/http.proxy.js +35 -3
- 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.js +11 -18
- package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
- package/dist/httpRequest/target/httpRequest.target.js +23 -5
- 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 +47 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +85 -4
- 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.validator.js +16 -7
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { HTTPContext, HTTPHandler, HTTPMiddleware, HTTPRequest, HTTPResponse, HTTPRouteMatch, HTTPState } from "../httpTypes/http.types.js";
|
|
3
|
+
import type { TrustProxy } from "../httpTrustProxy/httpTrustProxy.type.js";
|
|
3
4
|
export interface HTTPAdapter<State extends HTTPState = HTTPState> {
|
|
4
5
|
readonly name: string;
|
|
5
6
|
createRequest(request: IncomingMessage): HTTPRequest;
|
|
@@ -9,9 +10,21 @@ export interface HTTPAdapter<State extends HTTPState = HTTPState> {
|
|
|
9
10
|
readonly signal?: AbortSignal;
|
|
10
11
|
}): HTTPContext<State>;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Options every Node request built by this module accepts.
|
|
15
|
+
*
|
|
16
|
+
* `trustProxy` defaults to `false`, so `X-Forwarded-For` and
|
|
17
|
+
* `X-Forwarded-Proto` are ignored and the socket peer decides `request.ip`,
|
|
18
|
+
* `request.protocol` and `request.secure`.
|
|
19
|
+
*/
|
|
20
|
+
export interface NodeRequestTrustOptions {
|
|
21
|
+
readonly trustProxy?: TrustProxy;
|
|
22
|
+
}
|
|
12
23
|
export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implements HTTPAdapter<State> {
|
|
13
24
|
readonly name = "node";
|
|
14
|
-
|
|
25
|
+
private readonly trustProxy;
|
|
26
|
+
constructor(options?: NodeRequestTrustOptions);
|
|
27
|
+
createRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
|
|
15
28
|
createResponse(response: ServerResponse): HTTPResponse;
|
|
16
29
|
createContext(request: HTTPRequest, response: HTTPResponse, options?: {
|
|
17
30
|
readonly state?: State;
|
|
@@ -20,12 +33,19 @@ export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implem
|
|
|
20
33
|
}
|
|
21
34
|
export interface HTTPAdapterOptions<State extends HTTPState = HTTPState> {
|
|
22
35
|
readonly adapter?: HTTPAdapter<State>;
|
|
36
|
+
/**
|
|
37
|
+
* Which socket peers may speak for a client through `X-Forwarded-*`.
|
|
38
|
+
* Defaults to `false` — forwarded headers are ignored.
|
|
39
|
+
*/
|
|
40
|
+
readonly trustProxy?: TrustProxy;
|
|
23
41
|
}
|
|
24
|
-
export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(): NodeHTTPAdapter<State>;
|
|
42
|
+
export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(options?: NodeRequestTrustOptions): NodeHTTPAdapter<State>;
|
|
25
43
|
export interface RequestAdapter {
|
|
26
44
|
toHTTPRequest(request: IncomingMessage): HTTPRequest;
|
|
27
45
|
}
|
|
28
46
|
export declare class NodeRequestAdapter implements RequestAdapter {
|
|
47
|
+
private readonly trustProxy;
|
|
48
|
+
constructor(options?: NodeRequestTrustOptions);
|
|
29
49
|
toHTTPRequest(request: IncomingMessage): HTTPRequest;
|
|
30
50
|
}
|
|
31
51
|
export interface ResponseAdapter {
|
|
@@ -61,11 +81,12 @@ export declare class DefaultHTTPMiddlewareAdapter<State extends HTTPState = HTTP
|
|
|
61
81
|
export interface HTTPRouteAdapter<State extends HTTPState = HTTPState> {
|
|
62
82
|
match(method: string, path: string): HTTPRouteMatch<State> | undefined;
|
|
63
83
|
}
|
|
64
|
-
export declare function adaptNodeRequest(request: IncomingMessage): HTTPRequest;
|
|
84
|
+
export declare function adaptNodeRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
|
|
65
85
|
export declare function adaptNodeResponse(response: ServerResponse): HTTPResponse;
|
|
66
86
|
export declare function adaptNodeContext<State extends HTTPState = HTTPState>(request: IncomingMessage, response: ServerResponse, options?: {
|
|
67
87
|
readonly state?: State;
|
|
68
88
|
readonly signal?: AbortSignal;
|
|
89
|
+
readonly trustProxy?: TrustProxy;
|
|
69
90
|
}): HTTPContext<State>;
|
|
70
91
|
export declare function isHTTPAdapter(value: unknown): value is HTTPAdapter;
|
|
71
92
|
//# sourceMappingURL=http.adapters.d.ts.map
|
|
@@ -2,13 +2,16 @@ import { createHTTPContext } from "../httpContext/http.context.js";
|
|
|
2
2
|
import { createHTTPRequest } from "../httpRequest/http.request.js";
|
|
3
3
|
import { createHTTPResponse } from "../httpResponse/http.response.js";
|
|
4
4
|
import { createDefaultContextLogger } from "./httpAdapter.logger.js";
|
|
5
|
-
/* -------------------------------------------------------------------------- */
|
|
6
|
-
/* Node Adapter */
|
|
7
|
-
/* -------------------------------------------------------------------------- */
|
|
8
5
|
export class NodeHTTPAdapter {
|
|
9
6
|
name = "node";
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
trustProxy;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.trustProxy = options.trustProxy ?? false;
|
|
10
|
+
}
|
|
11
|
+
createRequest(request, options = {}) {
|
|
12
|
+
return createHTTPRequest(request, {
|
|
13
|
+
trustProxy: options.trustProxy ?? this.trustProxy,
|
|
14
|
+
});
|
|
12
15
|
}
|
|
13
16
|
createResponse(response) {
|
|
14
17
|
return createHTTPResponse(response);
|
|
@@ -26,12 +29,16 @@ export class NodeHTTPAdapter {
|
|
|
26
29
|
/* -------------------------------------------------------------------------- */
|
|
27
30
|
/* Adapter Factory */
|
|
28
31
|
/* -------------------------------------------------------------------------- */
|
|
29
|
-
export function createHTTPAdapter() {
|
|
30
|
-
return new NodeHTTPAdapter();
|
|
32
|
+
export function createHTTPAdapter(options = {}) {
|
|
33
|
+
return new NodeHTTPAdapter(options);
|
|
31
34
|
}
|
|
32
35
|
export class NodeRequestAdapter {
|
|
36
|
+
trustProxy;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.trustProxy = options.trustProxy ?? false;
|
|
39
|
+
}
|
|
33
40
|
toHTTPRequest(request) {
|
|
34
|
-
return createHTTPRequest(request);
|
|
41
|
+
return createHTTPRequest(request, { trustProxy: this.trustProxy });
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
export class NodeResponseAdapter {
|
|
@@ -76,15 +83,17 @@ export class DefaultHTTPMiddlewareAdapter {
|
|
|
76
83
|
/* -------------------------------------------------------------------------- */
|
|
77
84
|
/* Request Conversion Helpers */
|
|
78
85
|
/* -------------------------------------------------------------------------- */
|
|
79
|
-
export function adaptNodeRequest(request) {
|
|
80
|
-
return createHTTPRequest(request
|
|
86
|
+
export function adaptNodeRequest(request, options = {}) {
|
|
87
|
+
return createHTTPRequest(request, {
|
|
88
|
+
trustProxy: options.trustProxy ?? false,
|
|
89
|
+
});
|
|
81
90
|
}
|
|
82
91
|
export function adaptNodeResponse(response) {
|
|
83
92
|
return createHTTPResponse(response);
|
|
84
93
|
}
|
|
85
94
|
export function adaptNodeContext(request, response, options = {}) {
|
|
86
95
|
return createHTTPContext({
|
|
87
|
-
request: adaptNodeRequest(request),
|
|
96
|
+
request: adaptNodeRequest(request, { trustProxy: options.trustProxy }),
|
|
88
97
|
response: adaptNodeResponse(response),
|
|
89
98
|
state: options.state ?? {},
|
|
90
99
|
signal: options.signal,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { HttpRequestContext, createRequestContext, } from "../../httpRequest/httpRequest.context.js";
|
|
7
7
|
import { getClientIp, isTrustedProxy, } from "../../httpTrustProxy/httpTrustProxy.core.js";
|
|
8
8
|
import { removePort, extractPort } from "./httpNode.server.js";
|
|
9
|
-
import { parseQueryString } from "../../httpQuery/
|
|
9
|
+
import { parseQueryString } from "../../httpQuery/index.js";
|
|
10
10
|
import { findRequestTargetViolation } from "../../httpRequest/target/httpRequest.target.js";
|
|
11
11
|
/* -------------------------------------------------------------------------- */
|
|
12
12
|
/* Proxy Trust */
|
|
@@ -46,9 +46,28 @@ export interface AgentRegistryKey {
|
|
|
46
46
|
readonly name?: string;
|
|
47
47
|
}
|
|
48
48
|
export declare function getOrCreateAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Looks up a registered agent.
|
|
51
|
+
*
|
|
52
|
+
* `options` must be the same TLS-relevant options `getOrCreateAgent` was
|
|
53
|
+
* given, because they are part of the key; omitting them looks up the agent
|
|
54
|
+
* created with no options.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Whether an agent is registered for this key and TLS option set.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Removes registered agents for a key.
|
|
63
|
+
*
|
|
64
|
+
* With `options` the single matching agent is removed; without them every
|
|
65
|
+
* agent registered for that host is removed, whatever TLS options it was
|
|
66
|
+
* created with, so a per-host teardown releases all of its sockets.
|
|
67
|
+
*
|
|
68
|
+
* @returns Whether anything was removed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function removeAgent(key: string | AgentRegistryKey, destroy?: boolean, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
|
|
52
71
|
export declare function clearAgents(destroy?: boolean): void;
|
|
53
72
|
export declare function getRegisteredAgentKeys(): string[];
|
|
54
73
|
export declare function getDefaultHTTPAgent(): HTTPAgent;
|
|
@@ -113,7 +113,7 @@ export function getOrCreateAgent(key, options = {}) {
|
|
|
113
113
|
* passing `rejectUnauthorized: false` would have disabled certificate
|
|
114
114
|
* verification for a caller that had pinned a CA, with no way to detect it.
|
|
115
115
|
*/
|
|
116
|
-
const cacheKey =
|
|
116
|
+
const cacheKey = agentCacheKey(registryKey, options);
|
|
117
117
|
const existing = agentRegistry.get(cacheKey);
|
|
118
118
|
if (existing) {
|
|
119
119
|
return existing;
|
|
@@ -127,6 +127,18 @@ export function getOrCreateAgent(key, options = {}) {
|
|
|
127
127
|
agentRegistry.set(cacheKey, agent);
|
|
128
128
|
return agent;
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Builds the registry key an agent is stored under.
|
|
132
|
+
*
|
|
133
|
+
* Every read and write goes through this one helper. `getOrCreateAgent` used
|
|
134
|
+
* to append the TLS fingerprint while `getAgent`, `hasAgent` and
|
|
135
|
+
* `removeAgent` looked up the bare registry key, so every lookup missed and
|
|
136
|
+
* the documented per-host teardown silently leaked the agent and its
|
|
137
|
+
* keep-alive sockets.
|
|
138
|
+
*/
|
|
139
|
+
function agentCacheKey(registryKey, options) {
|
|
140
|
+
return `${registryKey}|${tlsFingerprint(options)}`;
|
|
141
|
+
}
|
|
130
142
|
/**
|
|
131
143
|
* Builds a stable fingerprint of the TLS-relevant fields of an agent config.
|
|
132
144
|
*/
|
|
@@ -156,23 +168,49 @@ function describeCredential(value) {
|
|
|
156
168
|
}
|
|
157
169
|
return "opaque";
|
|
158
170
|
}
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Looks up a registered agent.
|
|
173
|
+
*
|
|
174
|
+
* `options` must be the same TLS-relevant options `getOrCreateAgent` was
|
|
175
|
+
* given, because they are part of the key; omitting them looks up the agent
|
|
176
|
+
* created with no options.
|
|
177
|
+
*/
|
|
178
|
+
export function getAgent(key, options = {}) {
|
|
179
|
+
return agentRegistry.get(agentCacheKey(normalizeRegistryKey(key), options));
|
|
161
180
|
}
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Whether an agent is registered for this key and TLS option set.
|
|
183
|
+
*/
|
|
184
|
+
export function hasAgent(key, options = {}) {
|
|
185
|
+
return agentRegistry.has(agentCacheKey(normalizeRegistryKey(key), options));
|
|
164
186
|
}
|
|
165
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Removes registered agents for a key.
|
|
189
|
+
*
|
|
190
|
+
* With `options` the single matching agent is removed; without them every
|
|
191
|
+
* agent registered for that host is removed, whatever TLS options it was
|
|
192
|
+
* created with, so a per-host teardown releases all of its sockets.
|
|
193
|
+
*
|
|
194
|
+
* @returns Whether anything was removed.
|
|
195
|
+
*/
|
|
196
|
+
export function removeAgent(key, destroy = true, options) {
|
|
166
197
|
const registryKey = normalizeRegistryKey(key);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
agent
|
|
198
|
+
const cacheKeys = options === undefined
|
|
199
|
+
? Array.from(agentRegistry.keys()).filter((candidate) => candidate.startsWith(`${registryKey}|`))
|
|
200
|
+
: [agentCacheKey(registryKey, options)];
|
|
201
|
+
let removed = false;
|
|
202
|
+
for (const cacheKey of cacheKeys) {
|
|
203
|
+
const agent = agentRegistry.get(cacheKey);
|
|
204
|
+
if (!agent) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
agentRegistry.delete(cacheKey);
|
|
208
|
+
if (destroy) {
|
|
209
|
+
agent.destroy();
|
|
210
|
+
}
|
|
211
|
+
removed = true;
|
|
174
212
|
}
|
|
175
|
-
return
|
|
213
|
+
return removed;
|
|
176
214
|
}
|
|
177
215
|
export function clearAgents(destroy = true) {
|
|
178
216
|
if (destroy) {
|
|
@@ -12,11 +12,17 @@ import type { CacheFreshness } from "./core/httpCacheControl.type.js";
|
|
|
12
12
|
* the origin marked must-revalidate-before-reuse must never be reported
|
|
13
13
|
* fresh.
|
|
14
14
|
*
|
|
15
|
+
* The response's current age follows RFC 9111 section 4.2.3: the `Age`
|
|
16
|
+
* header plus the time elapsed since the response's `Date`. Reading `Age`
|
|
17
|
+
* alone — as this used to — left every response without that header aged
|
|
18
|
+
* `0` forever, so `isFresh()` answered `true` for a response of any age.
|
|
19
|
+
*
|
|
15
20
|
* @param responseHeaders - The cached response's headers.
|
|
16
21
|
* @param responseDate - The response's `Date`, if already parsed.
|
|
22
|
+
* @param now - The current time, defaulting to `Date.now()`.
|
|
17
23
|
* @returns The freshness calculation.
|
|
18
24
|
*/
|
|
19
|
-
export declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date): CacheFreshness;
|
|
25
|
+
export declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date, now?: Date): CacheFreshness;
|
|
20
26
|
/**
|
|
21
27
|
* Determines if a cached response is still fresh.
|
|
22
28
|
*/
|
|
@@ -12,24 +12,37 @@ import { parseCacheControl } from "./core/httpCacheControl.parse.js";
|
|
|
12
12
|
* the origin marked must-revalidate-before-reuse must never be reported
|
|
13
13
|
* fresh.
|
|
14
14
|
*
|
|
15
|
+
* The response's current age follows RFC 9111 section 4.2.3: the `Age`
|
|
16
|
+
* header plus the time elapsed since the response's `Date`. Reading `Age`
|
|
17
|
+
* alone — as this used to — left every response without that header aged
|
|
18
|
+
* `0` forever, so `isFresh()` answered `true` for a response of any age.
|
|
19
|
+
*
|
|
15
20
|
* @param responseHeaders - The cached response's headers.
|
|
16
21
|
* @param responseDate - The response's `Date`, if already parsed.
|
|
22
|
+
* @param now - The current time, defaulting to `Date.now()`.
|
|
17
23
|
* @returns The freshness calculation.
|
|
18
24
|
*/
|
|
19
|
-
export function calculateFreshness(responseHeaders, responseDate) {
|
|
25
|
+
export function calculateFreshness(responseHeaders, responseDate, now = new Date()) {
|
|
20
26
|
const cacheControl = responseHeaders["cache-control"];
|
|
21
27
|
const directives = parseCacheControl(cacheControl);
|
|
22
|
-
const date = responseDate ??
|
|
28
|
+
const date = responseDate ?? parseDateHeader(responseHeaders["date"], now);
|
|
23
29
|
/*
|
|
24
30
|
* RFC 9111 section 5.1: Age is a non-negative delta-seconds. A negative or
|
|
25
31
|
* suffixed value would otherwise inflate freshness without bound, which
|
|
26
32
|
* pins a poisoned response in cache far past the origin's TTL.
|
|
27
33
|
*/
|
|
28
34
|
const rawAge = (responseHeaders["age"] ?? "").trim();
|
|
29
|
-
const
|
|
35
|
+
const ageHeader = /^\d+$/.test(rawAge) ? Number(rawAge) : 0;
|
|
36
|
+
const residentSeconds = Math.max(0, Math.floor((now.getTime() - date.getTime()) / 1_000));
|
|
37
|
+
const age = ageHeader + residentSeconds;
|
|
30
38
|
const expiresHeader = responseHeaders["expires"];
|
|
31
39
|
const expires = expiresHeader ? parseExpires(expiresHeader) : undefined;
|
|
32
40
|
const effectiveMaxAge = resolveMaxAge(directives, expires, date);
|
|
41
|
+
/*
|
|
42
|
+
* With an `Expires`-derived lifetime this reduces to `expires - now`,
|
|
43
|
+
* because the lifetime is measured from `Date` and the age is measured to
|
|
44
|
+
* `now`.
|
|
45
|
+
*/
|
|
33
46
|
const remaining = Math.max(0, effectiveMaxAge - age);
|
|
34
47
|
const stale = remaining <= 0 ||
|
|
35
48
|
directives.noCache === true ||
|
|
@@ -43,6 +56,20 @@ export function calculateFreshness(responseHeaders, responseDate) {
|
|
|
43
56
|
remaining,
|
|
44
57
|
};
|
|
45
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Parses a `Date` header, falling back to the current time.
|
|
61
|
+
*
|
|
62
|
+
* @param value - The raw `Date` value.
|
|
63
|
+
* @param now - The fallback instant.
|
|
64
|
+
* @returns The parsed date.
|
|
65
|
+
*/
|
|
66
|
+
function parseDateHeader(value, now) {
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
return now;
|
|
69
|
+
}
|
|
70
|
+
const parsed = new Date(value);
|
|
71
|
+
return Number.isNaN(parsed.getTime()) ? now : parsed;
|
|
72
|
+
}
|
|
46
73
|
/**
|
|
47
74
|
* Parses an `Expires` header, treating an unparseable value as expired.
|
|
48
75
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Transport-specific socket management belongs to the server/client adapter
|
|
8
8
|
* layer. This module only handles HTTP-level semantics.
|
|
9
9
|
*/
|
|
10
|
+
import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
|
|
10
11
|
/* -------------------------------------------------------------------------- */
|
|
11
12
|
/* Constants */
|
|
12
13
|
/* -------------------------------------------------------------------------- */
|
|
@@ -121,7 +122,9 @@ export function formatKeepAliveHeader(parameters = {}) {
|
|
|
121
122
|
parts.push(`${key}=${quoteIfNeeded(value)}`);
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
+
const header = parts.join(", ");
|
|
126
|
+
assertSafeHeaderValue(header);
|
|
127
|
+
return header;
|
|
125
128
|
}
|
|
126
129
|
/* -------------------------------------------------------------------------- */
|
|
127
130
|
/* Connection Header */
|
|
@@ -348,10 +351,19 @@ function unquote(value) {
|
|
|
348
351
|
}
|
|
349
352
|
return trimmed;
|
|
350
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Emits a Keep-Alive parameter value.
|
|
356
|
+
*
|
|
357
|
+
* Quoting does not neutralise a CR or LF, so the control character used to
|
|
358
|
+
* survive into the field value. `escapeHeaderQuotedString` rejects it, the
|
|
359
|
+
* same helper every other quoted-parameter emitter in this package uses.
|
|
360
|
+
*
|
|
361
|
+
* @throws {TypeError} If the value contains a forbidden control character.
|
|
362
|
+
*/
|
|
351
363
|
function quoteIfNeeded(value) {
|
|
352
364
|
if (/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value)) {
|
|
353
365
|
return value;
|
|
354
366
|
}
|
|
355
|
-
return `"${value
|
|
367
|
+
return `"${escapeHeaderQuotedString(value)}"`;
|
|
356
368
|
}
|
|
357
369
|
//# sourceMappingURL=httpKeepAlive.core.js.map
|
|
@@ -11,6 +11,24 @@ export interface RequestLogger {
|
|
|
11
11
|
export interface LoggingMiddlewareOptions {
|
|
12
12
|
readonly logger?: RequestLogger;
|
|
13
13
|
readonly includeHeaders?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extra header names whose value must be replaced with `[REDACTED]`, on top
|
|
16
|
+
* of the credential-bearing names `@zudojs/logger` already recognises
|
|
17
|
+
* (`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, …).
|
|
18
|
+
*/
|
|
19
|
+
readonly redactHeaders?: readonly string[];
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates the request/response logging middleware.
|
|
23
|
+
*
|
|
24
|
+
* With `includeHeaders` the header record is redacted before it reaches the
|
|
25
|
+
* logger, using the same matcher `@zudojs/logger` applies to log metadata.
|
|
26
|
+
* It used to be copied verbatim, so a bearer token and the whole session
|
|
27
|
+
* cookie landed in the log store on every request.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Logger, header inclusion and extra redacted names.
|
|
30
|
+
* @returns A middleware that logs the start, completion and failure of a
|
|
31
|
+
* request.
|
|
32
|
+
*/
|
|
15
33
|
export declare function createLoggingMiddleware(options?: LoggingMiddlewareOptions): HttpMiddleware;
|
|
16
34
|
//# sourceMappingURL=httpMiddleware.logging.d.ts.map
|
|
@@ -3,8 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/builtin/logging
|
|
5
5
|
*/
|
|
6
|
+
import { createSecretMatcher, redactLogValue } from "@zudojs/logger";
|
|
6
7
|
import { getRequestMethod, getRequestUrl, getRequestHeaders, getResponseStatus, } from "../helpers/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Creates the request/response logging middleware.
|
|
10
|
+
*
|
|
11
|
+
* With `includeHeaders` the header record is redacted before it reaches the
|
|
12
|
+
* logger, using the same matcher `@zudojs/logger` applies to log metadata.
|
|
13
|
+
* It used to be copied verbatim, so a bearer token and the whole session
|
|
14
|
+
* cookie landed in the log store on every request.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Logger, header inclusion and extra redacted names.
|
|
17
|
+
* @returns A middleware that logs the start, completion and failure of a
|
|
18
|
+
* request.
|
|
19
|
+
*/
|
|
7
20
|
export function createLoggingMiddleware(options = {}) {
|
|
21
|
+
const isSecret = createSecretMatcher({
|
|
22
|
+
keys: options.redactHeaders ? [...options.redactHeaders] : undefined,
|
|
23
|
+
});
|
|
8
24
|
return async (context, next) => {
|
|
9
25
|
const startedAt = Date.now();
|
|
10
26
|
const request = context.request;
|
|
@@ -14,7 +30,7 @@ export function createLoggingMiddleware(options = {}) {
|
|
|
14
30
|
url: getRequestUrl(request),
|
|
15
31
|
...(options.includeHeaders
|
|
16
32
|
? {
|
|
17
|
-
headers: getRequestHeaders(request),
|
|
33
|
+
headers: redactLogValue(getRequestHeaders(request), isSecret),
|
|
18
34
|
}
|
|
19
35
|
: {}),
|
|
20
36
|
});
|
|
@@ -17,5 +17,16 @@ export interface SecurityMiddlewareOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
readonly useDefaults?: boolean;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates the response-hardening middleware.
|
|
22
|
+
*
|
|
23
|
+
* Called with no options it emits the package's default security header set
|
|
24
|
+
* ({@link createDefaultSecurityHeaderOptions}); the options below layer over
|
|
25
|
+
* that set, and `useDefaults: false` drops it entirely.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Explicit header values, layered over the baseline.
|
|
28
|
+
* @returns A middleware that adds the headers to the downstream response.
|
|
29
|
+
* @throws {TypeError} If a configured value contains a control character.
|
|
30
|
+
*/
|
|
20
31
|
export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): HttpMiddleware;
|
|
21
32
|
//# sourceMappingURL=httpMiddleware.security.d.ts.map
|
|
@@ -4,19 +4,34 @@
|
|
|
4
4
|
* @module httpMiddleware/builtin/security
|
|
5
5
|
*/
|
|
6
6
|
import { isValidHeaderFieldValue } from "../../../httpHeaders/security/index.js";
|
|
7
|
+
import { createSecurityHeaders } from "../../../httpSecurityHeaders/httpSecurityHeader.factory.js";
|
|
8
|
+
import { createDefaultSecurityHeaderOptions } from "../../../httpSecurityHeaders/httpSecurityHeader.recommended.js";
|
|
7
9
|
import { withResponseHeaders } from "../helpers/index.js";
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* The headers applied when the caller does not override them.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
13
|
+
* This is the package's declared safe baseline — the very set
|
|
14
|
+
* `createDefaultSecurityHeaderOptions` was written for and, until now, was
|
|
15
|
+
* never called for. The middleware used to re-derive its own three-entry
|
|
16
|
+
* list, so `pipeline.use(createSecurityMiddleware())` emitted no
|
|
17
|
+
* `Content-Security-Policy`, no `Strict-Transport-Security`, no
|
|
18
|
+
* `Permissions-Policy` and none of the cross-origin isolation headers while
|
|
19
|
+
* reading as evidence that the control was in force.
|
|
20
|
+
*/
|
|
21
|
+
function defaultSecurityHeaders() {
|
|
22
|
+
return createSecurityHeaders(createDefaultSecurityHeaderOptions());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates the response-hardening middleware.
|
|
26
|
+
*
|
|
27
|
+
* Called with no options it emits the package's default security header set
|
|
28
|
+
* ({@link createDefaultSecurityHeaderOptions}); the options below layer over
|
|
29
|
+
* that set, and `useDefaults: false` drops it entirely.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Explicit header values, layered over the baseline.
|
|
32
|
+
* @returns A middleware that adds the headers to the downstream response.
|
|
33
|
+
* @throws {TypeError} If a configured value contains a control character.
|
|
14
34
|
*/
|
|
15
|
-
const DEFAULT_SECURITY_HEADERS = Object.freeze({
|
|
16
|
-
"x-content-type-options": "nosniff",
|
|
17
|
-
"x-frame-options": "DENY",
|
|
18
|
-
"referrer-policy": "strict-origin-when-cross-origin",
|
|
19
|
-
});
|
|
20
35
|
export function createSecurityMiddleware(options = {}) {
|
|
21
36
|
const configured = {
|
|
22
37
|
"strict-transport-security": options.strictTransportSecurity,
|
|
@@ -26,9 +41,7 @@ export function createSecurityMiddleware(options = {}) {
|
|
|
26
41
|
"content-security-policy": options.contentSecurityPolicy,
|
|
27
42
|
"referrer-policy": options.referrerPolicy,
|
|
28
43
|
};
|
|
29
|
-
const resolved = {
|
|
30
|
-
...(options.useDefaults === false ? {} : DEFAULT_SECURITY_HEADERS),
|
|
31
|
-
};
|
|
44
|
+
const resolved = options.useDefaults === false ? {} : defaultSecurityHeaders();
|
|
32
45
|
for (const [name, value] of Object.entries(configured)) {
|
|
33
46
|
if (value === undefined) {
|
|
34
47
|
continue;
|
|
@@ -99,6 +99,19 @@ export declare function negotiateCharset(header: string | undefined | null, avai
|
|
|
99
99
|
* @returns The selected alternative, or `undefined` if none is acceptable.
|
|
100
100
|
*/
|
|
101
101
|
export declare function negotiate<T>(preferences: readonly NegotiationPreference[], available: readonly T[], matcher: (accepted: string, available: T) => boolean): T | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Returns the weight a preference list assigns to one value.
|
|
104
|
+
*
|
|
105
|
+
* The **most specific** match wins, and only then the highest weight, per
|
|
106
|
+
* RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
|
|
107
|
+
* explicit `gzip;q=0`, so a coding the client had refused came back with
|
|
108
|
+
* full quality.
|
|
109
|
+
*
|
|
110
|
+
* @param preferences - The parsed preferences.
|
|
111
|
+
* @param value - The alternative to weigh.
|
|
112
|
+
* @param matcher - Matches a preference value against an alternative.
|
|
113
|
+
* @returns The quality in `[0, 1]`, or `0` when nothing matches.
|
|
114
|
+
*/
|
|
102
115
|
export declare function getPreferenceQuality<T>(preferences: readonly NegotiationPreference[], value: T, matcher: (accepted: string, available: T) => boolean): number;
|
|
103
116
|
export declare function normalizeMediaType(value: string): string;
|
|
104
117
|
export declare function splitMediaType(value: string): [string, string] | undefined;
|
|
@@ -214,7 +214,36 @@ export function negotiateEncoding(header, available) {
|
|
|
214
214
|
if (preferences.length === 0) {
|
|
215
215
|
return available[0];
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
const selected = negotiate(preferences, available, matchesEncoding);
|
|
218
|
+
if (selected !== undefined) {
|
|
219
|
+
return selected;
|
|
220
|
+
}
|
|
221
|
+
/*
|
|
222
|
+
* RFC 9110 section 12.5.3: a representation with no content coding is
|
|
223
|
+
* acceptable unless the field explicitly excludes it with `identity;q=0`
|
|
224
|
+
* or a `*;q=0` that no identity entry overrides. Returning `undefined`
|
|
225
|
+
* here made `Accept-Encoding: zstd` look like "nothing is acceptable", so
|
|
226
|
+
* a caller answered 406 for a request it could have served uncompressed.
|
|
227
|
+
*/
|
|
228
|
+
const identity = available.find((value) => isIdentityEncoding(value));
|
|
229
|
+
if (identity === undefined || isIdentityRejected(preferences)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return identity;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Reports whether an `Accept-Encoding` field rejects the identity coding.
|
|
236
|
+
*
|
|
237
|
+
* @param preferences - The parsed preferences.
|
|
238
|
+
* @returns `true` when identity must not be served.
|
|
239
|
+
*/
|
|
240
|
+
function isIdentityRejected(preferences) {
|
|
241
|
+
const explicit = preferences.find((preference) => isIdentityEncoding(preference.value));
|
|
242
|
+
if (explicit) {
|
|
243
|
+
return !isAcceptableQuality(explicit.quality);
|
|
244
|
+
}
|
|
245
|
+
const wildcard = preferences.find((preference) => isWildcardEncoding(preference.value));
|
|
246
|
+
return wildcard !== undefined && !isAcceptableQuality(wildcard.quality);
|
|
218
247
|
}
|
|
219
248
|
export function getEncodingQuality(header, encoding) {
|
|
220
249
|
const preferences = parseAcceptEncoding(header);
|
|
@@ -319,20 +348,43 @@ function isExcluded(candidate, selected, rejections, matcher) {
|
|
|
319
348
|
return rejections.some((rejection) => rejection.specificity >= selected.specificity &&
|
|
320
349
|
matcher(rejection.value, candidate));
|
|
321
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Returns the weight a preference list assigns to one value.
|
|
353
|
+
*
|
|
354
|
+
* The **most specific** match wins, and only then the highest weight, per
|
|
355
|
+
* RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
|
|
356
|
+
* explicit `gzip;q=0`, so a coding the client had refused came back with
|
|
357
|
+
* full quality.
|
|
358
|
+
*
|
|
359
|
+
* @param preferences - The parsed preferences.
|
|
360
|
+
* @param value - The alternative to weigh.
|
|
361
|
+
* @param matcher - Matches a preference value against an alternative.
|
|
362
|
+
* @returns The quality in `[0, 1]`, or `0` when nothing matches.
|
|
363
|
+
*/
|
|
322
364
|
export function getPreferenceQuality(preferences, value, matcher) {
|
|
323
365
|
let best;
|
|
324
366
|
for (const preference of preferences) {
|
|
325
|
-
if (matcher(preference.value, value)) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
best = preference;
|
|
331
|
-
}
|
|
367
|
+
if (!matcher(preference.value, value)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!best || isMoreRelevant(preference, best)) {
|
|
371
|
+
best = preference;
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
374
|
return best?.quality ?? 0;
|
|
335
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Orders two matching preferences: specificity, then weight, then position.
|
|
378
|
+
*/
|
|
379
|
+
function isMoreRelevant(candidate, best) {
|
|
380
|
+
if (candidate.specificity !== best.specificity) {
|
|
381
|
+
return candidate.specificity > best.specificity;
|
|
382
|
+
}
|
|
383
|
+
if (candidate.quality !== best.quality) {
|
|
384
|
+
return candidate.quality > best.quality;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
336
388
|
/* -------------------------------------------------------------------------- */
|
|
337
389
|
/* Media Type Helpers */
|
|
338
390
|
/* -------------------------------------------------------------------------- */
|
|
@@ -167,6 +167,15 @@ export declare function applyProxyHeaders(headers: readonly HTTPHeader[], additi
|
|
|
167
167
|
* with `httpTrustProxy.getClientIp` rather than copying the raw header.
|
|
168
168
|
*/
|
|
169
169
|
export declare function setForwardedHeaders(headers: readonly HTTPHeader[], target: ProxyTarget, client?: ProxyClientContext): HTTPHeader[];
|
|
170
|
+
/**
|
|
171
|
+
* Builds an RFC 7239 `Forwarded` field value.
|
|
172
|
+
*
|
|
173
|
+
* Every parameter is escaped as a `quoted-string` when it is not a bare
|
|
174
|
+
* token, and the finished value is checked the same way `setForwardedHeaders`
|
|
175
|
+
* checks the values it writes.
|
|
176
|
+
*
|
|
177
|
+
* @throws {TypeError} If any parameter contains a control character.
|
|
178
|
+
*/
|
|
170
179
|
export declare function createForwardedHeader(address: ForwardedAddress): string;
|
|
171
180
|
export declare function parseForwardedHeader(value: string | undefined | null): ForwardedAddress[];
|
|
172
181
|
/**
|