@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { appendHeader, deleteHeader, getHeader, setHeader, } from "../httpProtocol/http.protocol.js";
|
|
11
11
|
import { isValidHTTPURL, isValidHeaderValue } from "../httpValidation/index.js";
|
|
12
12
|
import { isLinkLocalAddress, isLoopbackAddress, isUniqueLocalAddress, parseIpAddress, } from "../httpTrustProxy/httpTrustProxy.ip.js";
|
|
13
|
-
import { assertSafeHeaderValue } from "../httpHeaders/security/index.js";
|
|
13
|
+
import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
|
|
14
14
|
import { assertProxyPathContained } from "./httpProxy.pathGuard.js";
|
|
15
15
|
/* -------------------------------------------------------------------------- */
|
|
16
16
|
/* Target */
|
|
@@ -72,6 +72,16 @@ function isBlockedLiteralAddress(hostname) {
|
|
|
72
72
|
(address.bytes[1] ?? 0) <= 127) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
|
+
/* 192.0.0.0/24 IETF protocol assignments (192.0.0.8, 192.0.0.170, …). */
|
|
76
|
+
if (first === 192 &&
|
|
77
|
+
(address.bytes[1] ?? 0) === 0 &&
|
|
78
|
+
(address.bytes[2] ?? 0) === 0) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/* 198.18.0.0/15 benchmarking. */
|
|
82
|
+
if (first === 198 && ((address.bytes[1] ?? 0) & 0xfe) === 18) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
75
85
|
return false;
|
|
76
86
|
}
|
|
77
87
|
const first = address.bytes[0] ?? 0;
|
|
@@ -361,6 +371,15 @@ function appendForwardedValue(headers, name, value) {
|
|
|
361
371
|
/* -------------------------------------------------------------------------- */
|
|
362
372
|
/* Standard Forwarded Header */
|
|
363
373
|
/* -------------------------------------------------------------------------- */
|
|
374
|
+
/**
|
|
375
|
+
* Builds an RFC 7239 `Forwarded` field value.
|
|
376
|
+
*
|
|
377
|
+
* Every parameter is escaped as a `quoted-string` when it is not a bare
|
|
378
|
+
* token, and the finished value is checked the same way `setForwardedHeaders`
|
|
379
|
+
* checks the values it writes.
|
|
380
|
+
*
|
|
381
|
+
* @throws {TypeError} If any parameter contains a control character.
|
|
382
|
+
*/
|
|
364
383
|
export function createForwardedHeader(address) {
|
|
365
384
|
const parts = [];
|
|
366
385
|
if (address.for) {
|
|
@@ -375,7 +394,9 @@ export function createForwardedHeader(address) {
|
|
|
375
394
|
if (address.protocol) {
|
|
376
395
|
parts.push(`proto=${formatForwardedValue(address.protocol)}`);
|
|
377
396
|
}
|
|
378
|
-
|
|
397
|
+
const value = parts.join("; ");
|
|
398
|
+
assertSafeHeaderValue(value);
|
|
399
|
+
return value;
|
|
379
400
|
}
|
|
380
401
|
export function parseForwardedHeader(value) {
|
|
381
402
|
if (!value || value.trim().length === 0) {
|
|
@@ -534,11 +555,22 @@ function formatForwardedIdentifier(value) {
|
|
|
534
555
|
}
|
|
535
556
|
return formatForwardedValue(value);
|
|
536
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Emits an RFC 7239 parameter value.
|
|
560
|
+
*
|
|
561
|
+
* Wrapping a value in quotes does not neutralise a CR or LF — the control
|
|
562
|
+
* character survives into the field value and an attacker-chosen header
|
|
563
|
+
* follows it on the wire. `escapeHeaderQuotedString` rejects those characters
|
|
564
|
+
* rather than escaping them, which is what every other quoted-parameter
|
|
565
|
+
* emitter in this package already uses.
|
|
566
|
+
*
|
|
567
|
+
* @throws {TypeError} If the value contains a forbidden control character.
|
|
568
|
+
*/
|
|
537
569
|
function formatForwardedValue(value) {
|
|
538
570
|
if (/^[A-Za-z0-9._:-]+$/.test(value)) {
|
|
539
571
|
return value;
|
|
540
572
|
}
|
|
541
|
-
return `"${value
|
|
573
|
+
return `"${escapeHeaderQuotedString(value)}"`;
|
|
542
574
|
}
|
|
543
575
|
function unquoteForwardedValue(value) {
|
|
544
576
|
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/http/httpQuery
|
|
3
3
|
*
|
|
4
|
-
* HTTP query string parsing and
|
|
4
|
+
* HTTP query string parsing, request accessors, and serialization.
|
|
5
|
+
*
|
|
6
|
+
* Every parser in this module enforces the same four limits
|
|
7
|
+
* ({@link QueryLimitOptions}) and drops `__proto__`, `constructor` and
|
|
8
|
+
* `prototype`, on both the string and the `URLSearchParams` entry points.
|
|
9
|
+
* Exceeding a limit throws {@link HTTPQueryLimitError}, which carries a 414
|
|
10
|
+
* status code.
|
|
5
11
|
*/
|
|
6
|
-
export * from "./
|
|
12
|
+
export * from "./queryTypes/index.js";
|
|
13
|
+
export * from "./queryParse/index.js";
|
|
14
|
+
export * from "./queryRequest/index.js";
|
|
15
|
+
export * from "./querySerialize/index.js";
|
|
7
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/httpQuery/index.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/http/httpQuery
|
|
3
3
|
*
|
|
4
|
-
* HTTP query string parsing and
|
|
4
|
+
* HTTP query string parsing, request accessors, and serialization.
|
|
5
|
+
*
|
|
6
|
+
* Every parser in this module enforces the same four limits
|
|
7
|
+
* ({@link QueryLimitOptions}) and drops `__proto__`, `constructor` and
|
|
8
|
+
* `prototype`, on both the string and the `URLSearchParams` entry points.
|
|
9
|
+
* Exceeding a limit throws {@link HTTPQueryLimitError}, which carries a 414
|
|
10
|
+
* status code.
|
|
5
11
|
*/
|
|
6
|
-
export * from "./
|
|
12
|
+
export * from "./queryTypes/index.js";
|
|
13
|
+
export * from "./queryParse/index.js";
|
|
14
|
+
export * from "./queryRequest/index.js";
|
|
15
|
+
export * from "./querySerialize/index.js";
|
|
7
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/queryParse
|
|
3
|
+
*
|
|
4
|
+
* The query-string tokenizer and the two parsers built on it: a nested,
|
|
5
|
+
* bracket-path parser and a flat one. Both enforce the same limits.
|
|
6
|
+
*/
|
|
7
|
+
export { decodeQueryComponent, stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
|
|
8
|
+
export { normalizeQueryValue, parseQuery, parseQueryKey, } from "./queryParse.nested.js";
|
|
9
|
+
export { parseQueryString } from "./queryParse.flat.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/queryParse
|
|
3
|
+
*
|
|
4
|
+
* The query-string tokenizer and the two parsers built on it: a nested,
|
|
5
|
+
* bracket-path parser and a flat one. Both enforce the same limits.
|
|
6
|
+
*/
|
|
7
|
+
export { decodeQueryComponent, stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
|
|
8
|
+
export { normalizeQueryValue, parseQuery, parseQueryKey, } from "./queryParse.nested.js";
|
|
9
|
+
export { parseQueryString } from "./queryParse.flat.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { QueryStringParseOptions } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parses a query string into a flat record of decoded values.
|
|
4
|
+
*
|
|
5
|
+
* Unlike {@link parseQuery} this applies no bracket-path nesting and no value
|
|
6
|
+
* coercion: every value is the decoded string, and a repeated name yields an
|
|
7
|
+
* array. Both functions share one tokenizer and one set of limits.
|
|
8
|
+
*
|
|
9
|
+
* The record has a `null` prototype and `__proto__` / `constructor` /
|
|
10
|
+
* `prototype` are dropped, so no parameter name can reach a prototype chain.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseQueryString(input: string | undefined | null, options?: QueryStringParseOptions): Record<string, string | string[]>;
|
|
13
|
+
//# sourceMappingURL=queryParse.flat.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createQueryContainer, isForbiddenQueryKey, ownValue, resolveLimits, } from "../queryTypes/index.js";
|
|
2
|
+
import { stripQueryPrefix, tokenizeQuery } from "./queryParse.tokenizer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parses a query string into a flat record of decoded values.
|
|
5
|
+
*
|
|
6
|
+
* Unlike {@link parseQuery} this applies no bracket-path nesting and no value
|
|
7
|
+
* coercion: every value is the decoded string, and a repeated name yields an
|
|
8
|
+
* array. Both functions share one tokenizer and one set of limits.
|
|
9
|
+
*
|
|
10
|
+
* The record has a `null` prototype and `__proto__` / `constructor` /
|
|
11
|
+
* `prototype` are dropped, so no parameter name can reach a prototype chain.
|
|
12
|
+
*/
|
|
13
|
+
export function parseQueryString(input, options = {}) {
|
|
14
|
+
const result = createQueryContainer();
|
|
15
|
+
if (input === undefined || input === null || input === "") {
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
const value = stripQueryPrefix(input);
|
|
19
|
+
if (value.length === 0) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
const allowEmptyKeys = options.allowEmptyKeys ?? true;
|
|
23
|
+
const entries = tokenizeQuery(value, resolveLimits(options), true, options.decodePlusAsSpace !== false, false);
|
|
24
|
+
for (const [key, item] of entries) {
|
|
25
|
+
if (!allowEmptyKeys && key.length === 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (isForbiddenQueryKey(key)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const existing = ownValue(result, key);
|
|
32
|
+
if (existing === undefined) {
|
|
33
|
+
result[key] = item;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(existing)) {
|
|
37
|
+
existing.push(item);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
result[key] = [existing, item];
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=queryParse.flat.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { QueryObject, QueryParseOptions, QueryPrimitive } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parses a query string into a nested object.
|
|
4
|
+
*
|
|
5
|
+
* The returned objects have a `null` prototype: a parameter named
|
|
6
|
+
* `__proto__` can never reach `Object.prototype`, and a lookup of an
|
|
7
|
+
* unrelated key can never return an inherited member.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseQuery(query: string | URLSearchParams | undefined, options?: QueryParseOptions): QueryObject;
|
|
10
|
+
/**
|
|
11
|
+
* Splits a bracket/dot path such as `a[b][c]` into its segments.
|
|
12
|
+
*
|
|
13
|
+
* A path containing `__proto__`, `constructor` or `prototype` yields an empty
|
|
14
|
+
* array, which callers treat as "drop this parameter entirely".
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseQueryKey(key: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Coerces the well-known literals and numbers that round-trip exactly.
|
|
19
|
+
*
|
|
20
|
+
* A numeric literal is only converted when `String(Number(value)) === value`,
|
|
21
|
+
* so a 20-digit identifier stays the string it arrived as instead of being
|
|
22
|
+
* silently rewritten to a different number.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeQueryValue(value: string): QueryPrimitive;
|
|
25
|
+
//# sourceMappingURL=queryParse.nested.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createQueryContainer, DEFAULT_QUERY_MAX_DEPTH, HTTPQueryLimitError, isForbiddenQueryKey, isQueryObject, ownValue, resolveLimits, } from "../queryTypes/index.js";
|
|
2
|
+
import { stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parses a query string into a nested object.
|
|
5
|
+
*
|
|
6
|
+
* The returned objects have a `null` prototype: a parameter named
|
|
7
|
+
* `__proto__` can never reach `Object.prototype`, and a lookup of an
|
|
8
|
+
* unrelated key can never return an inherited member.
|
|
9
|
+
*/
|
|
10
|
+
export function parseQuery(query, options = {}) {
|
|
11
|
+
if (query === undefined) {
|
|
12
|
+
return createQueryContainer();
|
|
13
|
+
}
|
|
14
|
+
const limits = resolveLimits(options);
|
|
15
|
+
const maxDepth = options.maxDepth ?? DEFAULT_QUERY_MAX_DEPTH;
|
|
16
|
+
const commaSeparated = options.commaSeparated === true;
|
|
17
|
+
const entries = query instanceof URLSearchParams
|
|
18
|
+
? tokenizeSearchParams(query, limits, commaSeparated)
|
|
19
|
+
: tokenizeQuery(stripQueryPrefix(query), limits, options.decode !== false, options.plusAsSpace !== false, commaSeparated);
|
|
20
|
+
const result = createQueryContainer();
|
|
21
|
+
for (const [key, value] of entries) {
|
|
22
|
+
appendQueryValue(result, parseQueryKey(key), value, maxDepth);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits a bracket/dot path such as `a[b][c]` into its segments.
|
|
28
|
+
*
|
|
29
|
+
* A path containing `__proto__`, `constructor` or `prototype` yields an empty
|
|
30
|
+
* array, which callers treat as "drop this parameter entirely".
|
|
31
|
+
*/
|
|
32
|
+
export function parseQueryKey(key) {
|
|
33
|
+
if (!key) {
|
|
34
|
+
return [""];
|
|
35
|
+
}
|
|
36
|
+
const normalized = key.replace(/\]/g, "");
|
|
37
|
+
const segments = normalized.split(/[.[\]]+/).filter((part) => part.length > 0);
|
|
38
|
+
if (segments.some(isForbiddenQueryKey)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return segments;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes one value into the nested result object.
|
|
45
|
+
*
|
|
46
|
+
* Iterative rather than recursive so a deeply nested key cannot overflow the
|
|
47
|
+
* stack, depth-capped so it cannot allocate without bound, and every level is
|
|
48
|
+
* a null-prototype container read through `hasOwnProperty`.
|
|
49
|
+
*/
|
|
50
|
+
function appendQueryValue(target, path, value, maxDepth) {
|
|
51
|
+
if (path.length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (path.length > maxDepth) {
|
|
55
|
+
throw new HTTPQueryLimitError("Query parameter nesting exceeds the maximum allowed depth.");
|
|
56
|
+
}
|
|
57
|
+
let current = target;
|
|
58
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
59
|
+
const segment = path[index];
|
|
60
|
+
if (segment === undefined) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const existing = ownValue(current, segment);
|
|
64
|
+
if (isQueryObject(existing)) {
|
|
65
|
+
current = existing;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const next = createQueryContainer();
|
|
69
|
+
current[segment] = next;
|
|
70
|
+
current = next;
|
|
71
|
+
}
|
|
72
|
+
const leaf = path[path.length - 1];
|
|
73
|
+
if (leaf === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const existing = ownValue(current, leaf);
|
|
77
|
+
if (existing === undefined) {
|
|
78
|
+
current[leaf] = normalizeQueryValue(value);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(existing)) {
|
|
82
|
+
existing.push(normalizeQueryValue(value));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
current[leaf] = [existing, normalizeQueryValue(value)];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Coerces the well-known literals and numbers that round-trip exactly.
|
|
89
|
+
*
|
|
90
|
+
* A numeric literal is only converted when `String(Number(value)) === value`,
|
|
91
|
+
* so a 20-digit identifier stays the string it arrived as instead of being
|
|
92
|
+
* silently rewritten to a different number.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeQueryValue(value) {
|
|
95
|
+
if (value === "null") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (value === "true") {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (value === "false") {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
|
|
105
|
+
const number = Number(value);
|
|
106
|
+
if (Number.isFinite(number) && String(number) === value) {
|
|
107
|
+
return number;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=queryParse.nested.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { QueryLimits } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Percent-decodes a query component.
|
|
4
|
+
*
|
|
5
|
+
* A malformed sequence such as `%ZZ` makes `decodeURIComponent` throw a
|
|
6
|
+
* `URIError`. Any client can send one, so the raw component is returned
|
|
7
|
+
* instead of propagating an unhandled error onto the request path. The raw
|
|
8
|
+
* bytes are never interpolated into an error message.
|
|
9
|
+
*/
|
|
10
|
+
export declare function decodeQueryComponent(value: string, decode?: boolean, plusAsSpace?: boolean): string;
|
|
11
|
+
export declare function stripQueryPrefix(input: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Splits a raw query string into decoded `[name, value]` pairs, enforcing
|
|
14
|
+
* every configured limit as it goes.
|
|
15
|
+
*
|
|
16
|
+
* This is the single tokenizer for the module: {@link parseQuery} and
|
|
17
|
+
* {@link parseQueryString} are both built on it — including their
|
|
18
|
+
* `URLSearchParams` entry points — so the limits and the decoding rules
|
|
19
|
+
* cannot drift apart.
|
|
20
|
+
*
|
|
21
|
+
* `maxKeys` counts *emitted* pairs, not `&`-separated segments. Counting
|
|
22
|
+
* segments let `?a=1,2,3,...` expand past the cap under `commaSeparated`,
|
|
23
|
+
* because the split happened after the check: a single parameter could yield
|
|
24
|
+
* thousands of entries with `maxKeys` set to 1.
|
|
25
|
+
*/
|
|
26
|
+
export declare function tokenizeQuery(query: string, limits: QueryLimits, decode: boolean, plusAsSpace: boolean, commaSeparated: boolean): Array<readonly [string, string]>;
|
|
27
|
+
/**
|
|
28
|
+
* Tokenizes a `URLSearchParams` through the same path as a raw string.
|
|
29
|
+
*
|
|
30
|
+
* `toString()` re-encodes, and the tokenizer decodes again, so the round-trip
|
|
31
|
+
* is lossless. Routing it through the tokenizer is what makes `maxTotalLength`
|
|
32
|
+
* and `commaSeparated` apply here at all: the previous bespoke loop checked
|
|
33
|
+
* only the per-key and per-value caps, so `parseQuery(new URLSearchParams(…))`
|
|
34
|
+
* silently ignored two of the four documented limits and the comma option.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tokenizeSearchParams(params: URLSearchParams, limits: QueryLimits, commaSeparated: boolean): Array<readonly [string, string]>;
|
|
37
|
+
//# sourceMappingURL=queryParse.tokenizer.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HTTPQueryLimitError } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Percent-decodes a query component.
|
|
4
|
+
*
|
|
5
|
+
* A malformed sequence such as `%ZZ` makes `decodeURIComponent` throw a
|
|
6
|
+
* `URIError`. Any client can send one, so the raw component is returned
|
|
7
|
+
* instead of propagating an unhandled error onto the request path. The raw
|
|
8
|
+
* bytes are never interpolated into an error message.
|
|
9
|
+
*/
|
|
10
|
+
export function decodeQueryComponent(value, decode = true, plusAsSpace = true) {
|
|
11
|
+
const input = plusAsSpace ? value.replace(/\+/g, " ") : value;
|
|
12
|
+
if (!decode) {
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(input);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function stripQueryPrefix(input) {
|
|
23
|
+
let value = input;
|
|
24
|
+
if (value.startsWith("?")) {
|
|
25
|
+
value = value.slice(1);
|
|
26
|
+
}
|
|
27
|
+
const hashIndex = value.indexOf("#");
|
|
28
|
+
if (hashIndex !== -1) {
|
|
29
|
+
value = value.slice(0, hashIndex);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Splits a raw query string into decoded `[name, value]` pairs, enforcing
|
|
35
|
+
* every configured limit as it goes.
|
|
36
|
+
*
|
|
37
|
+
* This is the single tokenizer for the module: {@link parseQuery} and
|
|
38
|
+
* {@link parseQueryString} are both built on it — including their
|
|
39
|
+
* `URLSearchParams` entry points — so the limits and the decoding rules
|
|
40
|
+
* cannot drift apart.
|
|
41
|
+
*
|
|
42
|
+
* `maxKeys` counts *emitted* pairs, not `&`-separated segments. Counting
|
|
43
|
+
* segments let `?a=1,2,3,...` expand past the cap under `commaSeparated`,
|
|
44
|
+
* because the split happened after the check: a single parameter could yield
|
|
45
|
+
* thousands of entries with `maxKeys` set to 1.
|
|
46
|
+
*/
|
|
47
|
+
export function tokenizeQuery(query, limits, decode, plusAsSpace, commaSeparated) {
|
|
48
|
+
if (query.length > limits.maxTotalLength) {
|
|
49
|
+
throw new HTTPQueryLimitError("Query string exceeds the maximum allowed length.");
|
|
50
|
+
}
|
|
51
|
+
const entries = [];
|
|
52
|
+
const push = (key, value) => {
|
|
53
|
+
if (entries.length >= limits.maxKeys) {
|
|
54
|
+
throw new HTTPQueryLimitError("Query string contains too many parameters.");
|
|
55
|
+
}
|
|
56
|
+
entries.push([key, value]);
|
|
57
|
+
};
|
|
58
|
+
for (const part of query.split("&")) {
|
|
59
|
+
if (part === "") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const separator = part.indexOf("=");
|
|
63
|
+
const rawKey = separator === -1 ? part : part.slice(0, separator);
|
|
64
|
+
const rawValue = separator === -1 ? "" : part.slice(separator + 1);
|
|
65
|
+
const key = decodeQueryComponent(rawKey, decode, plusAsSpace);
|
|
66
|
+
if (key.length > limits.maxKeyLength) {
|
|
67
|
+
throw new HTTPQueryLimitError("Query parameter name exceeds the maximum allowed length.");
|
|
68
|
+
}
|
|
69
|
+
const value = decodeQueryComponent(rawValue, decode, plusAsSpace);
|
|
70
|
+
if (value.length > limits.maxValueLength) {
|
|
71
|
+
throw new HTTPQueryLimitError("Query parameter value exceeds the maximum allowed length.");
|
|
72
|
+
}
|
|
73
|
+
if (commaSeparated && value.includes(",")) {
|
|
74
|
+
for (const item of value.split(",")) {
|
|
75
|
+
push(key, item);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
push(key, value);
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Tokenizes a `URLSearchParams` through the same path as a raw string.
|
|
85
|
+
*
|
|
86
|
+
* `toString()` re-encodes, and the tokenizer decodes again, so the round-trip
|
|
87
|
+
* is lossless. Routing it through the tokenizer is what makes `maxTotalLength`
|
|
88
|
+
* and `commaSeparated` apply here at all: the previous bespoke loop checked
|
|
89
|
+
* only the per-key and per-value caps, so `parseQuery(new URLSearchParams(…))`
|
|
90
|
+
* silently ignored two of the four documented limits and the comma option.
|
|
91
|
+
*/
|
|
92
|
+
export function tokenizeSearchParams(params, limits, commaSeparated) {
|
|
93
|
+
return tokenizeQuery(params.toString(), limits, true, true, commaSeparated);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=queryParse.tokenizer.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/queryRequest
|
|
3
|
+
*
|
|
4
|
+
* Accessors that read the query component of an {@link HTTPRequest}. Every
|
|
5
|
+
* accessor here answers about the same parsed object, so middleware and a
|
|
6
|
+
* handler asking the same question get the same answer.
|
|
7
|
+
*/
|
|
8
|
+
export { getQuery, getQueryString, getQueryStrings, getQueryValue, getSearchParams, hasQuery, querySize, } from "./query.request.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/queryRequest
|
|
3
|
+
*
|
|
4
|
+
* Accessors that read the query component of an {@link HTTPRequest}. Every
|
|
5
|
+
* accessor here answers about the same parsed object, so middleware and a
|
|
6
|
+
* handler asking the same question get the same answer.
|
|
7
|
+
*/
|
|
8
|
+
export { getQuery, getQueryString, getQueryStrings, getQueryValue, getSearchParams, hasQuery, querySize, } from "./query.request.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HTTPRequest } from "../../httpTypes/http.types.js";
|
|
2
|
+
import type { QueryObject, QueryParseOptions, QueryValue } from "../queryTypes/index.js";
|
|
3
|
+
export declare function getQuery(request: HTTPRequest, options?: QueryParseOptions): QueryObject;
|
|
4
|
+
export declare function getQueryValue(request: HTTPRequest, key: string): QueryValue | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Returns a single parameter as a string.
|
|
7
|
+
*
|
|
8
|
+
* A parameter that appeared more than once, or as a bracket path, has no
|
|
9
|
+
* single string form and yields `undefined`; use {@link getQueryStrings} for
|
|
10
|
+
* those. A literal `?a=null` yields `"null"` — the text the client sent.
|
|
11
|
+
* This used to return `null` while declaring `string | undefined`, so a
|
|
12
|
+
* caller doing `value.length` on a non-`undefined` result crashed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getQueryString(request: HTTPRequest, key: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Returns every value a parameter carries, as strings.
|
|
17
|
+
*
|
|
18
|
+
* The parsed shape is attacker-chosen: `?a[b]=1&a=2` puts a null-prototype
|
|
19
|
+
* object inside the array for `a`, and `String()` on such an object throws
|
|
20
|
+
* `TypeError: Cannot convert object to primitive value`. Stringifying through
|
|
21
|
+
* {@link queryValueToString} keeps this total for every reachable input.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getQueryStrings(request: HTTPRequest, key: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Returns the raw search parameters of a request-target.
|
|
26
|
+
*
|
|
27
|
+
* This is the undecoded, unnested view. It applies none of the parser's
|
|
28
|
+
* limits and drops no key, so prefer {@link getQuery} for anything that
|
|
29
|
+
* reaches application code.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getSearchParams(request: HTTPRequest): URLSearchParams;
|
|
32
|
+
/**
|
|
33
|
+
* Reports whether a parameter is present in the parsed query.
|
|
34
|
+
*
|
|
35
|
+
* Answers about the same object {@link getQuery} returns, not about the raw
|
|
36
|
+
* search params. The two used to disagree: `hasQuery(req, "a")` was `false`
|
|
37
|
+
* for `?a[b]=1` (which `getQuery` exposes as `a`), and
|
|
38
|
+
* `hasQuery(req, "__proto__")` was `true` for a key the parser drops.
|
|
39
|
+
*/
|
|
40
|
+
export declare function hasQuery(request: HTTPRequest, key: string): boolean;
|
|
41
|
+
/** Number of distinct parameter names in the parsed query. */
|
|
42
|
+
export declare function querySize(request: HTTPRequest): number;
|
|
43
|
+
//# sourceMappingURL=query.request.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createQueryContainer, ownValue, queryValueToString, } from "../queryTypes/index.js";
|
|
2
|
+
import { parseQuery } from "../queryParse/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the query component of a request-target.
|
|
5
|
+
*
|
|
6
|
+
* The fragment is stripped: a proxy or a client that leaves `#…` on the
|
|
7
|
+
* request line must not turn it into a parameter.
|
|
8
|
+
*/
|
|
9
|
+
function queryComponent(url) {
|
|
10
|
+
if (!url) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const queryIndex = url.indexOf("?");
|
|
14
|
+
if (queryIndex === -1) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const hashIndex = url.indexOf("#", queryIndex + 1);
|
|
18
|
+
return url.slice(queryIndex + 1, hashIndex === -1 ? undefined : hashIndex);
|
|
19
|
+
}
|
|
20
|
+
export function getQuery(request, options = {}) {
|
|
21
|
+
const query = queryComponent(request.url);
|
|
22
|
+
if (query === undefined) {
|
|
23
|
+
return createQueryContainer();
|
|
24
|
+
}
|
|
25
|
+
return parseQuery(query, options);
|
|
26
|
+
}
|
|
27
|
+
export function getQueryValue(request, key) {
|
|
28
|
+
return ownValue(getQuery(request), key);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns a single parameter as a string.
|
|
32
|
+
*
|
|
33
|
+
* A parameter that appeared more than once, or as a bracket path, has no
|
|
34
|
+
* single string form and yields `undefined`; use {@link getQueryStrings} for
|
|
35
|
+
* those. A literal `?a=null` yields `"null"` — the text the client sent.
|
|
36
|
+
* This used to return `null` while declaring `string | undefined`, so a
|
|
37
|
+
* caller doing `value.length` on a non-`undefined` result crashed.
|
|
38
|
+
*/
|
|
39
|
+
export function getQueryString(request, key) {
|
|
40
|
+
const value = getQueryValue(request, key);
|
|
41
|
+
if (value === undefined || Array.isArray(value)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "object" && value !== null) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return queryValueToString(value);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns every value a parameter carries, as strings.
|
|
51
|
+
*
|
|
52
|
+
* The parsed shape is attacker-chosen: `?a[b]=1&a=2` puts a null-prototype
|
|
53
|
+
* object inside the array for `a`, and `String()` on such an object throws
|
|
54
|
+
* `TypeError: Cannot convert object to primitive value`. Stringifying through
|
|
55
|
+
* {@link queryValueToString} keeps this total for every reachable input.
|
|
56
|
+
*/
|
|
57
|
+
export function getQueryStrings(request, key) {
|
|
58
|
+
const value = getQueryValue(request, key);
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map(queryValueToString);
|
|
64
|
+
}
|
|
65
|
+
return [queryValueToString(value)];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns the raw search parameters of a request-target.
|
|
69
|
+
*
|
|
70
|
+
* This is the undecoded, unnested view. It applies none of the parser's
|
|
71
|
+
* limits and drops no key, so prefer {@link getQuery} for anything that
|
|
72
|
+
* reaches application code.
|
|
73
|
+
*/
|
|
74
|
+
export function getSearchParams(request) {
|
|
75
|
+
const query = queryComponent(request.url ?? "");
|
|
76
|
+
if (query === undefined) {
|
|
77
|
+
return new URLSearchParams();
|
|
78
|
+
}
|
|
79
|
+
return new URLSearchParams(query);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reports whether a parameter is present in the parsed query.
|
|
83
|
+
*
|
|
84
|
+
* Answers about the same object {@link getQuery} returns, not about the raw
|
|
85
|
+
* search params. The two used to disagree: `hasQuery(req, "a")` was `false`
|
|
86
|
+
* for `?a[b]=1` (which `getQuery` exposes as `a`), and
|
|
87
|
+
* `hasQuery(req, "__proto__")` was `true` for a key the parser drops.
|
|
88
|
+
*/
|
|
89
|
+
export function hasQuery(request, key) {
|
|
90
|
+
return getQueryValue(request, key) !== undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Number of distinct parameter names in the parsed query. */
|
|
93
|
+
export function querySize(request) {
|
|
94
|
+
return Object.keys(getQuery(request)).length;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=query.request.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/querySerialize
|
|
3
|
+
*
|
|
4
|
+
* Serializing an object back into a query string, and the clone/merge
|
|
5
|
+
* helpers that operate on an already-parsed query. All three are bounded the
|
|
6
|
+
* same way the parser is.
|
|
7
|
+
*/
|
|
8
|
+
export { buildQueryString, stringifyQuery, } from "./querySerialize.core.js";
|
|
9
|
+
export { cloneQuery, mergeQuery } from "./query.util.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpQuery/querySerialize
|
|
3
|
+
*
|
|
4
|
+
* Serializing an object back into a query string, and the clone/merge
|
|
5
|
+
* helpers that operate on an already-parsed query. All three are bounded the
|
|
6
|
+
* same way the parser is.
|
|
7
|
+
*/
|
|
8
|
+
export { buildQueryString, stringifyQuery, } from "./querySerialize.core.js";
|
|
9
|
+
export { cloneQuery, mergeQuery } from "./query.util.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|