@blamejs/core 0.8.89 → 0.9.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/CHANGELOG.md +847 -845
- package/index.js +6 -0
- package/lib/ai-pref.js +8 -2
- package/lib/auth/step-up.js +14 -5
- package/lib/cdn-cache-control.js +473 -0
- package/lib/client-hints.js +318 -0
- package/lib/http-client-cache.js +15 -8
- package/lib/http-client-cookie-jar.js +18 -7
- package/lib/http-message-signature.js +18 -11
- package/lib/log-stream.js +25 -1
- package/lib/mail-auth.js +3 -2
- package/lib/mail-require-tls.js +198 -0
- package/lib/mail.js +3 -1
- package/lib/middleware/body-parser.js +24 -12
- package/lib/middleware/scim-server.js +2 -2
- package/lib/middleware/tus-upload.js +12 -7
- package/lib/network-dns.js +178 -0
- package/lib/network-smtp-policy.js +2 -2
- package/lib/request-helpers.js +15 -0
- package/lib/security-assert.js +23 -1
- package/lib/structured-fields.js +244 -0
- package/lib/websocket.js +15 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.clientHints
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Sec-CH-UA Client Hints
|
|
6
|
+
* @order 316
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* User-Agent Client Hints parser. Browsers replacing the
|
|
10
|
+
* freeform `User-Agent` string send a family of `Sec-CH-UA-*`
|
|
11
|
+
* request headers carrying structured-fields data per RFC 8941:
|
|
12
|
+
*
|
|
13
|
+
* Sec-CH-UA: "Chromium";v="124", "Not-A.Brand";v="99", "Google Chrome";v="124"
|
|
14
|
+
* Sec-CH-UA-Mobile: ?0
|
|
15
|
+
* Sec-CH-UA-Platform: "Windows"
|
|
16
|
+
* Sec-CH-UA-Platform-Version: "15.0.0"
|
|
17
|
+
* Sec-CH-UA-Arch: "x86"
|
|
18
|
+
* Sec-CH-UA-Bitness: "64"
|
|
19
|
+
* Sec-CH-UA-Model: ""
|
|
20
|
+
* Sec-CH-UA-Full-Version-List: "Chromium";v="124.0.6367.91", ...
|
|
21
|
+
* Sec-CH-UA-WoW64: ?0
|
|
22
|
+
* Sec-CH-UA-Form-Factors: "Desktop"
|
|
23
|
+
*
|
|
24
|
+
* `parse(headers)` walks an HTTP request's headers map and returns
|
|
25
|
+
* a normalized object — brand list with versions, mobile boolean,
|
|
26
|
+
* platform / platform-version / arch / bitness / model / form-
|
|
27
|
+
* factors strings — plus the raw RFC 8941 parsed shape for any
|
|
28
|
+
* header the operator wants to inspect verbatim.
|
|
29
|
+
*
|
|
30
|
+
* Operators use it to:
|
|
31
|
+
* - Replace freeform UA-string parsing (deprecated; brittle).
|
|
32
|
+
* - Negotiate per-platform CSS / JS bundles (Sec-CH-UA-Platform).
|
|
33
|
+
* - Detect mobile-class clients without UA-sniffing.
|
|
34
|
+
* - Audit fingerprinting-style header negotiation.
|
|
35
|
+
*
|
|
36
|
+
* The primitive treats every header as defensive request-shape
|
|
37
|
+
* input — returns `null` for absent / malformed headers, throws
|
|
38
|
+
* only on explicit control-character / header-injection-shape
|
|
39
|
+
* input. Operators upstream of this primitive (proxies, framework
|
|
40
|
+
* middleware) already split CRLF; the in-string control-byte
|
|
41
|
+
* check is a defense-in-depth layer.
|
|
42
|
+
*
|
|
43
|
+
* `acceptList()` builds the `Accept-CH` response header so the
|
|
44
|
+
* operator advertises which client-hint headers the page wants.
|
|
45
|
+
*
|
|
46
|
+
* @card
|
|
47
|
+
* RFC 8941 / W3C Client Hints — parse `Sec-CH-UA*` request headers
|
|
48
|
+
* (brand list, mobile, platform, arch, model, form-factors) and
|
|
49
|
+
* build the `Accept-CH` response header for hint negotiation.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
var structuredFields = require("./structured-fields");
|
|
53
|
+
var { defineClass } = require("./framework-error");
|
|
54
|
+
|
|
55
|
+
var ClientHintsError = defineClass("ClientHintsError",
|
|
56
|
+
{ alwaysPermanent: true });
|
|
57
|
+
|
|
58
|
+
// Well-known Sec-CH-UA-* header names (W3C UA-CH spec + IETF
|
|
59
|
+
// draft-davidben-http-client-hint-reliability). Operators looking up
|
|
60
|
+
// the canonical name for an Accept-CH response use this constant; we
|
|
61
|
+
// keep it as a frozen array so a future hint addition is one-line.
|
|
62
|
+
var KNOWN_HINTS = Object.freeze([
|
|
63
|
+
"Sec-CH-UA",
|
|
64
|
+
"Sec-CH-UA-Mobile",
|
|
65
|
+
"Sec-CH-UA-Platform",
|
|
66
|
+
"Sec-CH-UA-Platform-Version",
|
|
67
|
+
"Sec-CH-UA-Arch",
|
|
68
|
+
"Sec-CH-UA-Bitness",
|
|
69
|
+
"Sec-CH-UA-Model",
|
|
70
|
+
"Sec-CH-UA-Full-Version-List",
|
|
71
|
+
"Sec-CH-UA-WoW64",
|
|
72
|
+
"Sec-CH-UA-Form-Factors",
|
|
73
|
+
"Sec-CH-Prefers-Reduced-Motion",
|
|
74
|
+
"Sec-CH-Prefers-Reduced-Transparency",
|
|
75
|
+
"Sec-CH-Prefers-Color-Scheme",
|
|
76
|
+
"Sec-CH-Prefers-Contrast",
|
|
77
|
+
"Sec-CH-Save-Data",
|
|
78
|
+
"Sec-CH-Viewport-Width",
|
|
79
|
+
"Sec-CH-Viewport-Height",
|
|
80
|
+
"Sec-CH-DPR",
|
|
81
|
+
"Sec-CH-Width",
|
|
82
|
+
"Sec-CH-Downlink",
|
|
83
|
+
"Sec-CH-RTT",
|
|
84
|
+
"Sec-CH-ECT",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
var KNOWN_HINTS_LC = {};
|
|
88
|
+
for (var _h = 0; _h < KNOWN_HINTS.length; _h += 1) {
|
|
89
|
+
KNOWN_HINTS_LC[KNOWN_HINTS[_h].toLowerCase()] = KNOWN_HINTS[_h];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _scanControlBytes(s, headerName) {
|
|
93
|
+
structuredFields.refuseControlBytes(s, {
|
|
94
|
+
ErrorClass: ClientHintsError,
|
|
95
|
+
code: "client-hints/bad-header-value",
|
|
96
|
+
label: "parse: " + headerName,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// RFC 8941 §3.3.3 sf-string — quoted-string with backslash-escape for
|
|
101
|
+
// `"` and `\`. Defensive parser that tolerates unquoted bare tokens
|
|
102
|
+
// (some operators forward a header whose quote-shape was already
|
|
103
|
+
// stripped by an upstream proxy).
|
|
104
|
+
function _parseSfString(s) {
|
|
105
|
+
var t = s.trim();
|
|
106
|
+
if (t.length === 0) return "";
|
|
107
|
+
if (t.charAt(0) === "\"") {
|
|
108
|
+
if (t.charAt(t.length - 1) !== "\"") return null;
|
|
109
|
+
return t.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
110
|
+
}
|
|
111
|
+
return t;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// RFC 8941 §3.3.6 sf-boolean — `?1` (true) / `?0` (false). Returns
|
|
115
|
+
// null for any other shape so callers can detect malformed values.
|
|
116
|
+
function _parseSfBoolean(s) {
|
|
117
|
+
var t = s.trim();
|
|
118
|
+
if (t === "?1") return true;
|
|
119
|
+
if (t === "?0") return false;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// RFC 8941 §3.1.1 sf-list — comma-separated members, each potentially
|
|
124
|
+
// carrying `;key=value;flag` parameters. For Sec-CH-UA the members are
|
|
125
|
+
// sf-strings with a `;v="<version>"` parameter giving the brand
|
|
126
|
+
// version. Returns `[ { brand, version, params } ]` or `null` for
|
|
127
|
+
// malformed input.
|
|
128
|
+
function _parseSfBrandList(s) {
|
|
129
|
+
var t = s.trim();
|
|
130
|
+
if (t.length === 0) return [];
|
|
131
|
+
// Walk via the shared quote-aware top-level `,` splitter. RFC 8941
|
|
132
|
+
// sf-list members don't allow parenthesized inner-list values in
|
|
133
|
+
// the Sec-CH-UA grammar (only sf-string + parameters), so the
|
|
134
|
+
// simple top-level comma split suffices — no `depth` tracking
|
|
135
|
+
// needed (the earlier inline shape carried defensive paren
|
|
136
|
+
// tracking left over from a generic sf-list walker prototype).
|
|
137
|
+
var pieces = structuredFields.splitTopLevel(t, ",");
|
|
138
|
+
var out = [];
|
|
139
|
+
for (var i = 0; i < pieces.length; i += 1) {
|
|
140
|
+
var piece = pieces[i].trim();
|
|
141
|
+
if (piece.length === 0) continue;
|
|
142
|
+
var parsed = _parseBrandMember(piece);
|
|
143
|
+
if (parsed !== null) out.push(parsed);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _parseBrandMember(piece) {
|
|
149
|
+
// member ::= sf-string ( ';' parameter )*
|
|
150
|
+
// RFC 8941 §4.1.1.4 — parameter values can be sf-string. A bare
|
|
151
|
+
// `piece.split(";")` would slice through `;v="1.2; 3"` and corrupt
|
|
152
|
+
// the parameter value. Use the shared quote-aware splitter so the
|
|
153
|
+
// tracking shape stays consistent across every framework parser.
|
|
154
|
+
var params = structuredFields.splitTopLevel(piece, ";");
|
|
155
|
+
if (params.length === 0) return null;
|
|
156
|
+
var brand = _parseSfString(params[0].trim());
|
|
157
|
+
if (brand === null) return null;
|
|
158
|
+
var member = { brand: brand, version: null, params: {} };
|
|
159
|
+
for (var i = 1; i < params.length; i += 1) {
|
|
160
|
+
var kv = params[i].trim();
|
|
161
|
+
if (kv.length === 0) continue;
|
|
162
|
+
var eq = kv.indexOf("=");
|
|
163
|
+
if (eq === -1) { member.params[kv.toLowerCase()] = true; continue; }
|
|
164
|
+
var k = kv.slice(0, eq).trim().toLowerCase();
|
|
165
|
+
var v = _parseSfString(kv.slice(eq + 1));
|
|
166
|
+
member.params[k] = v;
|
|
167
|
+
if (k === "v" && v !== null) member.version = v;
|
|
168
|
+
}
|
|
169
|
+
return member;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @primitive b.clientHints.parse
|
|
174
|
+
* @signature b.clientHints.parse(headers)
|
|
175
|
+
* @since 0.8.91
|
|
176
|
+
* @status stable
|
|
177
|
+
* @related b.clientHints.acceptList, b.clientHints.isKnownHint
|
|
178
|
+
*
|
|
179
|
+
* Parse the Sec-CH-UA-* family from an HTTP request's headers object.
|
|
180
|
+
* `headers` is the Node `req.headers` shape (header names are
|
|
181
|
+
* already lowercased per Node convention). Returns a normalized
|
|
182
|
+
* `{ brands, mobile, platform, platformVersion, arch, bitness, model,
|
|
183
|
+
* fullVersionList, wow64, formFactors, raw }` shape:
|
|
184
|
+
*
|
|
185
|
+
* - `brands`: `[ { brand, version, params } ]` from `Sec-CH-UA`
|
|
186
|
+
* - `mobile`: boolean from `Sec-CH-UA-Mobile` (?1/?0) — null if absent / malformed
|
|
187
|
+
* - `platform`: sf-string from `Sec-CH-UA-Platform`
|
|
188
|
+
* - `platformVersion`: sf-string from `Sec-CH-UA-Platform-Version`
|
|
189
|
+
* - `arch`: sf-string from `Sec-CH-UA-Arch`
|
|
190
|
+
* - `bitness`: sf-string from `Sec-CH-UA-Bitness`
|
|
191
|
+
* - `model`: sf-string from `Sec-CH-UA-Model`
|
|
192
|
+
* - `fullVersionList`: brand-list from `Sec-CH-UA-Full-Version-List`
|
|
193
|
+
* - `wow64`: boolean from `Sec-CH-UA-WoW64`
|
|
194
|
+
* - `formFactors`: brand-list from `Sec-CH-UA-Form-Factors`
|
|
195
|
+
* - `raw`: `{ "<header-name-lc>": "<raw-value>" }` for every Sec-CH-*
|
|
196
|
+
* header in the input, so operators can audit the full set without
|
|
197
|
+
* re-walking `req.headers`.
|
|
198
|
+
*
|
|
199
|
+
* Returns `null` when `headers` is not an object. Individual fields
|
|
200
|
+
* are `null` when the corresponding header is absent or malformed
|
|
201
|
+
* (defensive request-shape reader). Refuses control characters in
|
|
202
|
+
* any present Sec-CH-* value (header-injection defense).
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* var ch = b.clientHints.parse(req.headers);
|
|
206
|
+
* if (ch && ch.mobile === true) renderMobilePage(req, res);
|
|
207
|
+
* else if (ch && ch.platform === "Windows") renderWindowsPage(req, res);
|
|
208
|
+
* else renderDefaultPage(req, res);
|
|
209
|
+
*/
|
|
210
|
+
function parse(headers) {
|
|
211
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return null;
|
|
212
|
+
var raw = {};
|
|
213
|
+
var keys = Object.keys(headers);
|
|
214
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
215
|
+
var name = keys[k].toLowerCase();
|
|
216
|
+
if (name.indexOf("sec-ch-") !== 0) continue;
|
|
217
|
+
var val = headers[keys[k]];
|
|
218
|
+
if (val === undefined || val === null) continue;
|
|
219
|
+
if (typeof val !== "string") val = String(val);
|
|
220
|
+
_scanControlBytes(val, name);
|
|
221
|
+
raw[name] = val;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
brands: raw["sec-ch-ua"] ? _parseSfBrandList(raw["sec-ch-ua"]) : null,
|
|
226
|
+
mobile: raw["sec-ch-ua-mobile"] !== undefined ? _parseSfBoolean(raw["sec-ch-ua-mobile"]) : null,
|
|
227
|
+
platform: raw["sec-ch-ua-platform"] !== undefined ? _parseSfString(raw["sec-ch-ua-platform"]) : null,
|
|
228
|
+
platformVersion: raw["sec-ch-ua-platform-version"] !== undefined ? _parseSfString(raw["sec-ch-ua-platform-version"]) : null,
|
|
229
|
+
arch: raw["sec-ch-ua-arch"] !== undefined ? _parseSfString(raw["sec-ch-ua-arch"]) : null,
|
|
230
|
+
bitness: raw["sec-ch-ua-bitness"] !== undefined ? _parseSfString(raw["sec-ch-ua-bitness"]) : null,
|
|
231
|
+
model: raw["sec-ch-ua-model"] !== undefined ? _parseSfString(raw["sec-ch-ua-model"]) : null,
|
|
232
|
+
fullVersionList: raw["sec-ch-ua-full-version-list"] ? _parseSfBrandList(raw["sec-ch-ua-full-version-list"]) : null,
|
|
233
|
+
wow64: raw["sec-ch-ua-wow64"] !== undefined ? _parseSfBoolean(raw["sec-ch-ua-wow64"]) : null,
|
|
234
|
+
formFactors: raw["sec-ch-ua-form-factors"] ? _parseSfBrandList(raw["sec-ch-ua-form-factors"]) : null,
|
|
235
|
+
raw: raw,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @primitive b.clientHints.acceptList
|
|
241
|
+
* @signature b.clientHints.acceptList(hintNames)
|
|
242
|
+
* @since 0.8.91
|
|
243
|
+
* @status stable
|
|
244
|
+
* @related b.clientHints.parse, b.clientHints.isKnownHint
|
|
245
|
+
*
|
|
246
|
+
* Build the `Accept-CH` response header value advertising which
|
|
247
|
+
* client-hint request headers the operator wants the browser to
|
|
248
|
+
* include on subsequent requests. `hintNames` is an array of
|
|
249
|
+
* canonical Sec-CH-* header names; the primitive refuses unknown
|
|
250
|
+
* hint names (typo defense — `accept-ch: Sec-CH-UA-Plateform`
|
|
251
|
+
* silently neuters the negotiation).
|
|
252
|
+
*
|
|
253
|
+
* Operators set `Accept-CH` on the HTML document response. The
|
|
254
|
+
* browser sends the listed hints on every subsequent same-origin
|
|
255
|
+
* navigation / sub-resource request.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* res.setHeader("Accept-CH", b.clientHints.acceptList([
|
|
259
|
+
* "Sec-CH-UA-Platform",
|
|
260
|
+
* "Sec-CH-UA-Platform-Version",
|
|
261
|
+
* "Sec-CH-UA-Mobile",
|
|
262
|
+
* ]));
|
|
263
|
+
* // → "Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA-Mobile"
|
|
264
|
+
*/
|
|
265
|
+
function acceptList(hintNames) {
|
|
266
|
+
if (!Array.isArray(hintNames) || hintNames.length === 0) {
|
|
267
|
+
throw new ClientHintsError("client-hints/bad-hint-list",
|
|
268
|
+
"acceptList: hintNames must be a non-empty array of Sec-CH-* names");
|
|
269
|
+
}
|
|
270
|
+
var seen = {};
|
|
271
|
+
var out = [];
|
|
272
|
+
for (var i = 0; i < hintNames.length; i += 1) {
|
|
273
|
+
var n = hintNames[i];
|
|
274
|
+
if (typeof n !== "string" || n.length === 0) {
|
|
275
|
+
throw new ClientHintsError("client-hints/bad-hint-name",
|
|
276
|
+
"acceptList: hintNames[" + i + "] must be a non-empty string");
|
|
277
|
+
}
|
|
278
|
+
var canonical = KNOWN_HINTS_LC[n.toLowerCase()];
|
|
279
|
+
if (!canonical) {
|
|
280
|
+
throw new ClientHintsError("client-hints/unknown-hint",
|
|
281
|
+
"acceptList: '" + n + "' is not a known client-hint header " +
|
|
282
|
+
"(see b.clientHints.KNOWN_HINTS for the list)");
|
|
283
|
+
}
|
|
284
|
+
if (seen[canonical]) continue;
|
|
285
|
+
seen[canonical] = true;
|
|
286
|
+
out.push(canonical);
|
|
287
|
+
}
|
|
288
|
+
return out.join(", ");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @primitive b.clientHints.isKnownHint
|
|
293
|
+
* @signature b.clientHints.isKnownHint(headerName)
|
|
294
|
+
* @since 0.8.91
|
|
295
|
+
* @status stable
|
|
296
|
+
*
|
|
297
|
+
* Returns `true` when `headerName` matches one of the well-known
|
|
298
|
+
* Sec-CH-* hint headers (case-insensitive). Operators auditing
|
|
299
|
+
* inbound headers walk the request and call this to identify
|
|
300
|
+
* negotiation-related hints without keyword-matching.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* b.clientHints.isKnownHint("Sec-CH-UA-Mobile"); // → true
|
|
304
|
+
* b.clientHints.isKnownHint("sec-ch-ua-platform"); // → true
|
|
305
|
+
* b.clientHints.isKnownHint("X-Custom"); // → false
|
|
306
|
+
*/
|
|
307
|
+
function isKnownHint(headerName) {
|
|
308
|
+
if (typeof headerName !== "string" || headerName.length === 0) return false;
|
|
309
|
+
return Object.prototype.hasOwnProperty.call(KNOWN_HINTS_LC, headerName.toLowerCase());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
parse: parse,
|
|
314
|
+
acceptList: acceptList,
|
|
315
|
+
isKnownHint: isKnownHint,
|
|
316
|
+
KNOWN_HINTS: KNOWN_HINTS,
|
|
317
|
+
ClientHintsError: ClientHintsError,
|
|
318
|
+
};
|
package/lib/http-client-cache.js
CHANGED
|
@@ -42,10 +42,11 @@
|
|
|
42
42
|
* stale-while-revalidate / stale-if-error.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
var C
|
|
46
|
-
var canonicalJson
|
|
47
|
-
var safeUrl
|
|
48
|
-
var
|
|
45
|
+
var C = require("./constants");
|
|
46
|
+
var canonicalJson = require("./canonical-json");
|
|
47
|
+
var safeUrl = require("./safe-url");
|
|
48
|
+
var structuredFields = require("./structured-fields");
|
|
49
|
+
var validateOpts = require("./validate-opts");
|
|
49
50
|
var { HttpClientError } = require("./framework-error");
|
|
50
51
|
|
|
51
52
|
// ---- Tunables ----------------------------------------------------------
|
|
@@ -90,7 +91,13 @@ function _hcErr(code, message) {
|
|
|
90
91
|
function _parseCacheControl(value) {
|
|
91
92
|
var out = Object.create(null);
|
|
92
93
|
if (typeof value !== "string" || value.length === 0) return out;
|
|
93
|
-
|
|
94
|
+
// RFC 9111 §5.2 + RFC 9110 §5.6.4 — directive arguments may be
|
|
95
|
+
// quoted-string. A bare `value.split(",")` would slice through
|
|
96
|
+
// `no-cache="Authorization, Cookie"` and `private="set-cookie,
|
|
97
|
+
// x-foo"` and emit fake directives. Quote-aware splitter mirrors
|
|
98
|
+
// cdn-cache-control's _splitTopLevelCommas (RFC 8941 §3.3.3 with
|
|
99
|
+
// backslash-escape).
|
|
100
|
+
var parts = structuredFields.splitTopLevel(value, ",");
|
|
94
101
|
for (var i = 0; i < parts.length; i++) {
|
|
95
102
|
var p = parts[i].trim();
|
|
96
103
|
if (!p) continue;
|
|
@@ -100,7 +107,7 @@ function _parseCacheControl(value) {
|
|
|
100
107
|
else { k = p.slice(0, eq).trim(); v = p.slice(eq + 1).trim(); }
|
|
101
108
|
// Strip surrounding quotes from value.
|
|
102
109
|
if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
|
|
103
|
-
v = v.slice(1, v.length - 1);
|
|
110
|
+
v = v.slice(1, v.length - 1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
104
111
|
}
|
|
105
112
|
out[k.toLowerCase()] = v;
|
|
106
113
|
}
|
|
@@ -202,7 +209,7 @@ function _buildCacheKey(method, url, varyHeaderValues) {
|
|
|
202
209
|
// uncacheable.
|
|
203
210
|
function _extractVaryValues(varyHeader, requestHeaders) {
|
|
204
211
|
if (typeof varyHeader !== "string" || varyHeader.length === 0) return [];
|
|
205
|
-
var names = varyHeader.split(",").map(function (s) {
|
|
212
|
+
var names = varyHeader.split(",").map(function (s) { // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary is a comma-list of field-names (token grammar); no quoted-string
|
|
206
213
|
return s.trim().toLowerCase();
|
|
207
214
|
}).filter(function (s) { return s.length > 0; });
|
|
208
215
|
if (names.indexOf("*") !== -1) return null; // sentinel: "uncacheable"
|
|
@@ -252,7 +259,7 @@ function _evaluateStorage(method, statusCode, responseHeaders, sharedCache) {
|
|
|
252
259
|
|
|
253
260
|
// Vary: * is uncacheable per RFC 9110 §12.5.5.
|
|
254
261
|
if (typeof varyHeader === "string" && varyHeader.indexOf("*") !== -1) {
|
|
255
|
-
var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); });
|
|
262
|
+
var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); }); // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary field-names; token grammar only
|
|
256
263
|
if (trimmed.indexOf("*") !== -1) {
|
|
257
264
|
return { cacheable: false, reason: "vary-star", freshnessMs: -1, directives: directives, varyHeader: varyHeader };
|
|
258
265
|
}
|
|
@@ -62,12 +62,13 @@
|
|
|
62
62
|
|
|
63
63
|
var fs = require("node:fs");
|
|
64
64
|
var path = require("node:path");
|
|
65
|
-
var C
|
|
66
|
-
var numericBounds
|
|
67
|
-
var safeAsync
|
|
68
|
-
var safeJson
|
|
69
|
-
var safeUrl
|
|
70
|
-
var
|
|
65
|
+
var C = require("./constants");
|
|
66
|
+
var numericBounds = require("./numeric-bounds");
|
|
67
|
+
var safeAsync = require("./safe-async");
|
|
68
|
+
var safeJson = require("./safe-json");
|
|
69
|
+
var safeUrl = require("./safe-url");
|
|
70
|
+
var structuredFields = require("./structured-fields");
|
|
71
|
+
var validateOpts = require("./validate-opts");
|
|
71
72
|
var { defineClass } = require("./framework-error");
|
|
72
73
|
|
|
73
74
|
var CookieJarError = defineClass("CookieJarError", { alwaysPermanent: true });
|
|
@@ -102,7 +103,12 @@ function _parseSetCookie(line) {
|
|
|
102
103
|
var attrs = {};
|
|
103
104
|
if (semi !== -1) {
|
|
104
105
|
var rest = line.slice(semi + 1);
|
|
105
|
-
|
|
106
|
+
// RFC 6265 §4.1 attribute values are token-only by spec, but
|
|
107
|
+
// interop reality is that some servers emit quoted attr values
|
|
108
|
+
// (e.g. `; SameSite="Strict"` from older middleware). Quote-aware
|
|
109
|
+
// split preserves a quoted `;` inside an attr value if anyone
|
|
110
|
+
// ever sends one — defensive, not bug-fixing.
|
|
111
|
+
var parts = structuredFields.splitTopLevel(rest, ";");
|
|
106
112
|
for (var i = 0; i < parts.length; i++) {
|
|
107
113
|
var p = parts[i].trim();
|
|
108
114
|
if (!p) continue;
|
|
@@ -110,6 +116,11 @@ function _parseSetCookie(line) {
|
|
|
110
116
|
var k, v;
|
|
111
117
|
if (pi === -1) { k = p; v = ""; }
|
|
112
118
|
else { k = p.slice(0, pi).trim(); v = p.slice(pi + 1).trim(); }
|
|
119
|
+
// Strip surrounding quotes from attribute value when present
|
|
120
|
+
// (defensive against interop). RFC 6265 §4.1 does not require
|
|
121
|
+
// this, but doesn't forbid the operator's parser absorbing it.
|
|
122
|
+
var _unq = structuredFields.unquoteSfString(v);
|
|
123
|
+
if (_unq !== null) v = _unq;
|
|
113
124
|
attrs[k.toLowerCase()] = v;
|
|
114
125
|
}
|
|
115
126
|
}
|
|
@@ -59,12 +59,13 @@
|
|
|
59
59
|
* // → { valid, label, keyid, alg, covered, reason? }
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
var nodeCrypto
|
|
63
|
-
var safeUrl
|
|
64
|
-
var safeBuffer
|
|
65
|
-
var C
|
|
66
|
-
var lazyRequire
|
|
67
|
-
var
|
|
62
|
+
var nodeCrypto = require("crypto");
|
|
63
|
+
var safeUrl = require("./safe-url");
|
|
64
|
+
var safeBuffer = require("./safe-buffer");
|
|
65
|
+
var C = require("./constants");
|
|
66
|
+
var lazyRequire = require("./lazy-require");
|
|
67
|
+
var structuredFields = require("./structured-fields");
|
|
68
|
+
var validateOpts = require("./validate-opts");
|
|
68
69
|
var { HttpSigError } = require("./framework-error");
|
|
69
70
|
|
|
70
71
|
var _err = HttpSigError.factory;
|
|
@@ -317,7 +318,7 @@ function sign(msg, opts) {
|
|
|
317
318
|
// header isn't already supplied. Operators wanting to use the
|
|
318
319
|
// RFC 9530 "sha-512" identifier (SHA-512 instead of SHA3-512) supply
|
|
319
320
|
// the header themselves; the framework emits SHA3-512.
|
|
320
|
-
var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
|
|
321
|
+
var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — opts.covered is operator-supplied component-id list (e.g. "content-digest;sf"); component identifiers are RFC 9421 §2.1 derived-field names with token-only grammar; no quoted-string
|
|
321
322
|
if (coveredLower.indexOf("content-digest") !== -1 &&
|
|
322
323
|
_resolveHeader(m.headers, "content-digest") === null) {
|
|
323
324
|
if (m.body == null) {
|
|
@@ -416,7 +417,12 @@ function _parseSignatureInput(headerValue) {
|
|
|
416
417
|
|
|
417
418
|
var params = {};
|
|
418
419
|
if (paramsRaw.length > 0) {
|
|
419
|
-
|
|
420
|
+
// RFC 9421 §2.3 + RFC 8941 §3.1.2 — parameter values may be
|
|
421
|
+
// sf-string. A bare `paramsRaw.split(";")` would slice through a
|
|
422
|
+
// legitimate `;tag="x;y"` parameter. Quote-aware splitter
|
|
423
|
+
// mirrors cdn-cache-control._splitTopLevelCommas (RFC 8941
|
|
424
|
+
// §3.3.3 quoted-string state with backslash-escape).
|
|
425
|
+
var paramParts = structuredFields.splitTopLevel(paramsRaw, ";");
|
|
420
426
|
for (var j = 0; j < paramParts.length; j++) {
|
|
421
427
|
var part = paramParts[j].trim();
|
|
422
428
|
if (part.length === 0) continue;
|
|
@@ -425,7 +431,8 @@ function _parseSignatureInput(headerValue) {
|
|
|
425
431
|
var k = part.slice(0, pEq).trim();
|
|
426
432
|
var vv = part.slice(pEq + 1).trim();
|
|
427
433
|
if (vv.charAt(0) === "\"" && vv.charAt(vv.length - 1) === "\"") {
|
|
428
|
-
|
|
434
|
+
var _unq = structuredFields.unquoteSfString(vv);
|
|
435
|
+
params[k] = _unq === null ? vv : _unq;
|
|
429
436
|
} else {
|
|
430
437
|
var num = Number(vv);
|
|
431
438
|
params[k] = isFinite(num) ? num : vv;
|
|
@@ -441,7 +448,7 @@ function _parseSignature(headerValue, label) {
|
|
|
441
448
|
if (headerValue.indexOf(prefix) !== 0) {
|
|
442
449
|
// Multiple signature labels can appear; comma-separated. Find the
|
|
443
450
|
// matching label.
|
|
444
|
-
var parts = headerValue.split(",");
|
|
451
|
+
var parts = headerValue.split(","); // allow:bare-split-on-quoted-header — RFC 9421 §2.4 Signature header values are `label=:b64:` form; base64 alphabet excludes `,` and the label tokens are RFC 8941 §3.3.4 sf-token (no DQUOTE in practice)
|
|
445
452
|
for (var i = 0; i < parts.length; i++) {
|
|
446
453
|
var p = parts[i].trim();
|
|
447
454
|
if (p.indexOf(prefix) === 0) {
|
|
@@ -509,7 +516,7 @@ function verify(msg, opts) {
|
|
|
509
516
|
// If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
|
|
510
517
|
// mandates that verifiers re-run the digest over the body — a stale
|
|
511
518
|
// header from a proxy would otherwise verify trivially.
|
|
512
|
-
var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
|
|
519
|
+
var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — same as sign() above: covered items are RFC 9421 §2.1 component-ids, token grammar
|
|
513
520
|
if (coveredLower.indexOf("content-digest") !== -1) {
|
|
514
521
|
if (m.body == null) {
|
|
515
522
|
return { valid: false, reason: "content-digest-no-body" };
|
package/lib/log-stream.js
CHANGED
|
@@ -133,15 +133,39 @@ function init(opts) {
|
|
|
133
133
|
if (initialized) return;
|
|
134
134
|
if (!opts || !opts.sinks) throw new Error("logStream.init({ sinks }) is required");
|
|
135
135
|
|
|
136
|
+
// Validate top-level minLevel at config time so a typo (`"infos"`)
|
|
137
|
+
// doesn't silently produce `LEVEL_PRIORITY["infos"] === undefined`
|
|
138
|
+
// and drop every record at runtime (an `X >= undefined` compare
|
|
139
|
+
// is always false). Throw rather than fall back to a default —
|
|
140
|
+
// operators want a loud failure at boot, not silent log loss.
|
|
141
|
+
if (opts.minLevel !== undefined && opts.minLevel !== null) {
|
|
142
|
+
var topLevel = String(opts.minLevel).toLowerCase();
|
|
143
|
+
if (LEVELS.indexOf(topLevel) === -1) {
|
|
144
|
+
throw _err("INVALID_LEVEL",
|
|
145
|
+
"logStream.init: opts.minLevel '" + opts.minLevel +
|
|
146
|
+
"' must be one of " + LEVELS.join(", "), true);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
sinks = {};
|
|
137
151
|
for (var name in opts.sinks) {
|
|
138
152
|
var cfg = opts.sinks[name];
|
|
139
153
|
var proto = dispatcher.resolve(cfg.protocol);
|
|
154
|
+
// Same gate per-sink so a misconfigured filter doesn't silently
|
|
155
|
+
// drop records from a single sink while every other sink works.
|
|
156
|
+
if (cfg.minLevel !== undefined && cfg.minLevel !== null) {
|
|
157
|
+
var sinkLvl = String(cfg.minLevel).toLowerCase();
|
|
158
|
+
if (LEVELS.indexOf(sinkLvl) === -1) {
|
|
159
|
+
throw _err("INVALID_LEVEL",
|
|
160
|
+
"logStream.init: sink '" + name + "' minLevel '" + cfg.minLevel +
|
|
161
|
+
"' must be one of " + LEVELS.join(", "), true);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
140
164
|
sinks[name] = {
|
|
141
165
|
name: name,
|
|
142
166
|
protocol: cfg.protocol,
|
|
143
167
|
raw: proto.create(cfg),
|
|
144
|
-
levelFilter: cfg.minLevel
|
|
168
|
+
levelFilter: cfg.minLevel ? String(cfg.minLevel).toLowerCase() : null,
|
|
145
169
|
};
|
|
146
170
|
}
|
|
147
171
|
|
package/lib/mail-auth.js
CHANGED
|
@@ -379,7 +379,7 @@ var DMARCBIS_VALID_PSD = { y: 1, n: 1, u: 1 };
|
|
|
379
379
|
function _parseDmarcRecord(text) {
|
|
380
380
|
var policy = { v: null, p: null, sp: null, np: null, psd: null,
|
|
381
381
|
pct: 100, adkim: "r", aspf: "r" }; // allow:raw-byte-literal — RFC 7489 default pct
|
|
382
|
-
var pairs = text.split(";");
|
|
382
|
+
var pairs = text.split(";"); // allow:bare-split-on-quoted-header — RFC 7489 §6.4 DMARC tag-list grammar: `tag-spec *( ";" tag-spec )` with tag-value = 0*( tval *( WSP / FWS ) ); NO quoted-string allowed
|
|
383
383
|
for (var i = 0; i < pairs.length; i += 1) {
|
|
384
384
|
var kv = pairs[i].trim();
|
|
385
385
|
if (kv.length === 0) continue;
|
|
@@ -950,7 +950,8 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
|
|
|
950
950
|
|
|
951
951
|
function _parseArcTagList(value) {
|
|
952
952
|
var tags = {};
|
|
953
|
-
var parts = String(value).split(";");
|
|
953
|
+
var parts = String(value).split(";"); // allow:bare-split-on-quoted-header — allow:raw-byte-literal — RFC 8617 §4 ARC tag-list grammar (same as the DKIM RFC's): `tag-spec *( ";" tag-spec )`, tag-value contains no DQUOTE
|
|
954
|
+
|
|
954
955
|
for (var i = 0; i < parts.length; i += 1) {
|
|
955
956
|
var p = parts[i].trim();
|
|
956
957
|
if (p.length === 0) continue;
|