@blamejs/core 0.8.52 → 0.8.58
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 +6 -0
- package/index.js +8 -0
- package/lib/audit.js +4 -0
- package/lib/auth/fido-mds3.js +624 -0
- package/lib/auth/passkey.js +214 -2
- package/lib/auth-bot-challenge.js +1 -1
- package/lib/credential-hash.js +2 -2
- package/lib/db-collection.js +290 -0
- package/lib/db-query.js +245 -0
- package/lib/db.js +173 -67
- package/lib/framework-error.js +55 -0
- package/lib/guard-cidr.js +2 -1
- package/lib/guard-jwt.js +2 -2
- package/lib/guard-oauth.js +2 -2
- package/lib/http-client-cache.js +916 -0
- package/lib/http-client.js +242 -0
- package/lib/mail-arf.js +343 -0
- package/lib/mail-auth.js +265 -40
- package/lib/mail-bimi.js +948 -33
- package/lib/mail-bounce.js +386 -4
- package/lib/mail-mdn.js +424 -0
- package/lib/mail-unsubscribe.js +265 -25
- package/lib/mail.js +403 -21
- package/lib/middleware/bearer-auth.js +1 -1
- package/lib/middleware/clear-site-data.js +122 -0
- package/lib/middleware/dpop.js +1 -1
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/nel.js +214 -0
- package/lib/middleware/security-headers.js +56 -4
- package/lib/middleware/speculation-rules.js +323 -0
- package/lib/mime-parse.js +198 -0
- package/lib/mtls-ca.js +15 -5
- package/lib/network-dns.js +890 -27
- package/lib/network-tls.js +745 -0
- package/lib/object-store/sigv4.js +54 -0
- package/lib/public-suffix.js +414 -0
- package/lib/safe-buffer.js +7 -0
- package/lib/safe-json.js +1 -1
- package/lib/static.js +120 -0
- package/lib/storage.js +11 -0
- package/lib/vendor/MANIFEST.json +33 -0
- package/lib/vendor/bimi-trust-anchors.pem +33 -0
- package/lib/vendor/public-suffix-list.dat +16376 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.middleware.speculationRules
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Speculation Rules
|
|
6
|
+
* @order 130
|
|
7
|
+
* @card W3C Speculation Rules emitter — declares which links the
|
|
8
|
+
* user-agent is allowed to prerender or prefetch ahead of
|
|
9
|
+
* user navigation. Default emit path is the
|
|
10
|
+
* `Speculation-Rules` response header pointing at a
|
|
11
|
+
* JSON-served rules document; an opt-in `inline: true`
|
|
12
|
+
* mode injects `<script type="speculationrules">` into
|
|
13
|
+
* text/html bodies.
|
|
14
|
+
*
|
|
15
|
+
* @intro
|
|
16
|
+
* Speculation Rules (W3C draft) are the modern replacement for
|
|
17
|
+
* `<link rel="prerender">` / `<link rel="prefetch">`. The browser
|
|
18
|
+
* reads a JSON document declaring patterns of links the operator
|
|
19
|
+
* wants speculatively loaded under varying eagerness levels
|
|
20
|
+
* (`immediate`, `eager`, `moderate`, `conservative`) and
|
|
21
|
+
* pre-fetches or pre-renders matching anchors as the user hovers /
|
|
22
|
+
* scrolls / dwells.
|
|
23
|
+
*
|
|
24
|
+
* Two emit modes:
|
|
25
|
+
*
|
|
26
|
+
* 1. `Speculation-Rules` response header — points at a JSON
|
|
27
|
+
* document the operator serves elsewhere (typical for shared
|
|
28
|
+
* rules across many pages):
|
|
29
|
+
*
|
|
30
|
+
* Speculation-Rules: "/rules/speculation.json"
|
|
31
|
+
*
|
|
32
|
+
* The header form is the framework's default because it keeps
|
|
33
|
+
* HTML response bodies clean, lets the rules document be cached
|
|
34
|
+
* independently, and avoids touching response bodies (zero risk
|
|
35
|
+
* of body-parse / encoding mishaps).
|
|
36
|
+
*
|
|
37
|
+
* 2. Inline `<script type="speculationrules">` injection — for
|
|
38
|
+
* operators who want per-page rules, the framework can inject
|
|
39
|
+
* the JSON into `text/html` responses just before `</head>`
|
|
40
|
+
* (or, falling back, before the first `<body>` tag). Opt in
|
|
41
|
+
* with `{ inline: true }`.
|
|
42
|
+
*
|
|
43
|
+
* Mount AFTER `securityHeaders` and (when used) `cspNonce`. When
|
|
44
|
+
* `inline: true` is set, an operator-supplied nonce on `req` (via
|
|
45
|
+
* `cspNonce`) is added to the injected `<script>` tag so a strict
|
|
46
|
+
* CSP allows the rules to load.
|
|
47
|
+
*
|
|
48
|
+
* app.use(b.middleware.requestId());
|
|
49
|
+
* app.use(b.middleware.securityHeaders());
|
|
50
|
+
* app.use(b.middleware.cspNonce({ always: true }));
|
|
51
|
+
* app.use(b.middleware.speculationRules({
|
|
52
|
+
* rules: {
|
|
53
|
+
* prerender: [
|
|
54
|
+
* { where: { href_matches: "/articles/*" }, eagerness: "moderate" },
|
|
55
|
+
* ],
|
|
56
|
+
* prefetch: [
|
|
57
|
+
* { where: { href_matches: "/api/*" }, eagerness: "conservative" },
|
|
58
|
+
* ],
|
|
59
|
+
* },
|
|
60
|
+
* }));
|
|
61
|
+
*
|
|
62
|
+
* Both mode shapes validate the rules object at construct time so
|
|
63
|
+
* typos surface at boot, never as a silently-ignored speculation
|
|
64
|
+
* rule three deploys later.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
var validateOpts = require("../validate-opts");
|
|
68
|
+
|
|
69
|
+
// Per W3C draft + Chromium implementation. `immediate` triggers the
|
|
70
|
+
// speculation as soon as the rules are seen; `conservative` waits
|
|
71
|
+
// for a strong intent signal (mousedown). The framework permits the
|
|
72
|
+
// full set; operators pick per-rule.
|
|
73
|
+
var EAGERNESS_LEVELS = {
|
|
74
|
+
"immediate": true,
|
|
75
|
+
"eager": true,
|
|
76
|
+
"moderate": true,
|
|
77
|
+
"conservative": true,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// `prerender` fully renders the destination in a hidden tab —
|
|
81
|
+
// expensive, fast on activation. `prefetch` pulls bytes only — cheap,
|
|
82
|
+
// faster TTFB on activation. Both are arrays of rule objects.
|
|
83
|
+
var ACTION_KEYS = ["prerender", "prefetch"];
|
|
84
|
+
|
|
85
|
+
// Header value — when an operator passes a string instead of a rules
|
|
86
|
+
// object, treat it as the URL to a separately-served rules document
|
|
87
|
+
// (the simpler header-form path).
|
|
88
|
+
function _looksLikeRulesUrl(v) {
|
|
89
|
+
return typeof v === "string" && v.length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CR/LF/NUL screen — flows into the response header value, where
|
|
93
|
+
// header-injection would split into a forged second header.
|
|
94
|
+
var INJECTION_RE = /[\r\n\0]/;
|
|
95
|
+
|
|
96
|
+
function _refuseInjection(value, label) {
|
|
97
|
+
if (typeof value !== "string") return;
|
|
98
|
+
if (INJECTION_RE.test(value)) { // allow:regex-no-length-cap — CR/LF/NUL injection check, length bounded by caller
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
"middleware.speculationRules: " + label + " contains CR/LF/NUL — refused as a " +
|
|
101
|
+
"header-injection vector");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate the operator-supplied rules object. Returns nothing on
|
|
106
|
+
// success; throws TypeError with an actionable message at config
|
|
107
|
+
// time. Each entry must be a plain object with `where` (object) and
|
|
108
|
+
// `eagerness` (one of EAGERNESS_LEVELS).
|
|
109
|
+
function _validateRules(rules, label) {
|
|
110
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) {
|
|
111
|
+
throw new TypeError(label + " must be an object with `prerender` and/or `prefetch` arrays");
|
|
112
|
+
}
|
|
113
|
+
var seenAny = false;
|
|
114
|
+
for (var ki = 0; ki < ACTION_KEYS.length; ki += 1) {
|
|
115
|
+
var actionKey = ACTION_KEYS[ki];
|
|
116
|
+
var entries = rules[actionKey];
|
|
117
|
+
if (entries === undefined) continue;
|
|
118
|
+
if (!Array.isArray(entries)) {
|
|
119
|
+
throw new TypeError(label + "." + actionKey + " must be an array of rule objects");
|
|
120
|
+
}
|
|
121
|
+
seenAny = true;
|
|
122
|
+
for (var ei = 0; ei < entries.length; ei += 1) {
|
|
123
|
+
var rule = entries[ei];
|
|
124
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
125
|
+
throw new TypeError(label + "." + actionKey + "[" + ei + "] must be a plain object");
|
|
126
|
+
}
|
|
127
|
+
if (!rule.where || typeof rule.where !== "object" || Array.isArray(rule.where)) {
|
|
128
|
+
throw new TypeError(label + "." + actionKey + "[" + ei +
|
|
129
|
+
"].where must be a `where` clause object (W3C draft, e.g. { href_matches: \"/path/*\" })");
|
|
130
|
+
}
|
|
131
|
+
if (typeof rule.eagerness !== "string" || !EAGERNESS_LEVELS[rule.eagerness]) {
|
|
132
|
+
throw new TypeError(label + "." + actionKey + "[" + ei +
|
|
133
|
+
"].eagerness must be one of: " + Object.keys(EAGERNESS_LEVELS).join(", "));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!seenAny) {
|
|
138
|
+
throw new TypeError(label + " must declare at least one of `prerender` or `prefetch`");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @primitive b.middleware.speculationRules
|
|
144
|
+
* @signature b.middleware.speculationRules(req, res, next)
|
|
145
|
+
* @since 0.8.53
|
|
146
|
+
* @status stable
|
|
147
|
+
* @related b.middleware.cspNonce, b.middleware.securityHeaders
|
|
148
|
+
*
|
|
149
|
+
* Builds middleware that emits W3C Speculation Rules so the user-agent
|
|
150
|
+
* may prerender or prefetch matching links ahead of user navigation.
|
|
151
|
+
* Two emit paths:
|
|
152
|
+
*
|
|
153
|
+
* - Header form (default): emits `Speculation-Rules: "<url>"` where
|
|
154
|
+
* `<url>` points at an operator-served rules document.
|
|
155
|
+
* - Inline form (`inline: true`): injects a
|
|
156
|
+
* `<script type="speculationrules">{...}</script>` tag into
|
|
157
|
+
* `text/html` response bodies just before `</head>` (or fallback
|
|
158
|
+
* before `<body>`). When `cspNonce` middleware has populated
|
|
159
|
+
* `req.cspNonce`, the injected `<script>` carries that nonce so
|
|
160
|
+
* strict CSP allows it.
|
|
161
|
+
*
|
|
162
|
+
* The rules object is validated at construct time. Empty / malformed
|
|
163
|
+
* rules throw with a message naming the offending key.
|
|
164
|
+
*
|
|
165
|
+
* @opts
|
|
166
|
+
* {
|
|
167
|
+
* rules: object, // { prerender: [...], prefetch: [...] }
|
|
168
|
+
* rulesUrl: string, // header-mode URL (alternative to rules)
|
|
169
|
+
* inline: boolean, // default false; inject <script> instead of header
|
|
170
|
+
* }
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* var b = require("@blamejs/core");
|
|
174
|
+
* var app = b.router.create();
|
|
175
|
+
* app.use(b.middleware.speculationRules({
|
|
176
|
+
* rules: {
|
|
177
|
+
* prerender: [
|
|
178
|
+
* { where: { href_matches: "/articles/*" }, eagerness: "moderate" },
|
|
179
|
+
* ],
|
|
180
|
+
* prefetch: [
|
|
181
|
+
* { where: { href_matches: "/api/*" }, eagerness: "conservative" },
|
|
182
|
+
* ],
|
|
183
|
+
* },
|
|
184
|
+
* }));
|
|
185
|
+
*/
|
|
186
|
+
function create(opts) {
|
|
187
|
+
opts = opts || {};
|
|
188
|
+
validateOpts(opts, ["rules", "rulesUrl", "inline"], "middleware.speculationRules");
|
|
189
|
+
|
|
190
|
+
var inline = !!opts.inline;
|
|
191
|
+
var rulesUrl = opts.rulesUrl;
|
|
192
|
+
var rulesObj = opts.rules;
|
|
193
|
+
|
|
194
|
+
// Operators may pass `rules` as a string (URL form) for ergonomics —
|
|
195
|
+
// route that to rulesUrl so the validation logic below stays
|
|
196
|
+
// straightforward.
|
|
197
|
+
if (_looksLikeRulesUrl(rulesObj) && rulesUrl === undefined) {
|
|
198
|
+
rulesUrl = rulesObj;
|
|
199
|
+
rulesObj = undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (inline) {
|
|
203
|
+
if (!rulesObj) {
|
|
204
|
+
throw new TypeError(
|
|
205
|
+
"middleware.speculationRules: opts.rules is required when opts.inline is true " +
|
|
206
|
+
"(inline mode injects the rules JSON into the response body)");
|
|
207
|
+
}
|
|
208
|
+
_validateRules(rulesObj, "middleware.speculationRules: opts.rules");
|
|
209
|
+
} else {
|
|
210
|
+
if (rulesObj && rulesUrl !== undefined) {
|
|
211
|
+
throw new TypeError(
|
|
212
|
+
"middleware.speculationRules: pass either opts.rules or opts.rulesUrl, not both");
|
|
213
|
+
}
|
|
214
|
+
if (!rulesObj && (typeof rulesUrl !== "string" || rulesUrl.length === 0)) {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
"middleware.speculationRules: header mode requires opts.rulesUrl (the URL of " +
|
|
217
|
+
"an operator-served JSON rules document) or opts.rules (which the framework " +
|
|
218
|
+
"renders inline). Pass `inline: true` if you want the framework to embed the " +
|
|
219
|
+
"rules object as a <script type=\"speculationrules\"> tag.");
|
|
220
|
+
}
|
|
221
|
+
if (rulesObj) {
|
|
222
|
+
_validateRules(rulesObj, "middleware.speculationRules: opts.rules");
|
|
223
|
+
}
|
|
224
|
+
if (rulesUrl !== undefined) {
|
|
225
|
+
_refuseInjection(rulesUrl, "opts.rulesUrl");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Pre-build the emission payload once.
|
|
230
|
+
// Header value per W3C draft + RFC 8941 (Structured Field Value
|
|
231
|
+
// String — quoted, no internal CR/LF). When the operator supplied
|
|
232
|
+
// `rules` directly in header mode, the framework serializes the
|
|
233
|
+
// JSON as a data: URL so a single primitive can serve both shapes
|
|
234
|
+
// without forcing the operator to wire a separate route.
|
|
235
|
+
var headerValue;
|
|
236
|
+
if (!inline) {
|
|
237
|
+
if (rulesUrl) {
|
|
238
|
+
headerValue = '"' + rulesUrl + '"';
|
|
239
|
+
} else {
|
|
240
|
+
var dataUrl = "data:application/speculationrules+json;base64," +
|
|
241
|
+
Buffer.from(JSON.stringify(rulesObj), "utf8").toString("base64");
|
|
242
|
+
headerValue = '"' + dataUrl + '"';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Pre-built inline JSON. The body is small (rules objects are
|
|
247
|
+
// typically ~200 bytes); JSON.stringify once and cache.
|
|
248
|
+
var inlineJson = inline ? JSON.stringify(rulesObj) : null;
|
|
249
|
+
|
|
250
|
+
return function speculationRules(req, res, next) {
|
|
251
|
+
if (!inline) {
|
|
252
|
+
if (typeof res.setHeader === "function") {
|
|
253
|
+
res.setHeader("Speculation-Rules", headerValue);
|
|
254
|
+
}
|
|
255
|
+
return next();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Inline mode — patch res.write / res.end so we inject the
|
|
259
|
+
// <script> tag into the first text/html response. Same shape as
|
|
260
|
+
// botDisclose / aiActDisclosure to stay consistent with the
|
|
261
|
+
// framework's response-rewrite convention.
|
|
262
|
+
var origWrite = res.write && res.write.bind(res);
|
|
263
|
+
var origEnd = res.end && res.end.bind(res);
|
|
264
|
+
var injected = false;
|
|
265
|
+
|
|
266
|
+
function _scriptTag() {
|
|
267
|
+
var nonceAttr = "";
|
|
268
|
+
// Operator-supplied per-request nonce flows through cspNonce
|
|
269
|
+
// middleware. If absent we still emit the script tag (works
|
|
270
|
+
// under non-strict CSP); under strict CSP without nonce, the
|
|
271
|
+
// browser refuses — visible to operators via cspReport.
|
|
272
|
+
if (req && typeof req.cspNonce === "string" && req.cspNonce.length > 0) {
|
|
273
|
+
nonceAttr = ' nonce="' + req.cspNonce + '"';
|
|
274
|
+
}
|
|
275
|
+
return '<script type="speculationrules"' + nonceAttr + '>' + inlineJson + '</script>';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function _maybeInject(chunk) {
|
|
279
|
+
if (injected) return chunk;
|
|
280
|
+
var ct = typeof res.getHeader === "function" ? res.getHeader("content-type") : "";
|
|
281
|
+
if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
|
|
282
|
+
var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
|
|
283
|
+
(typeof chunk === "string" ? chunk : "");
|
|
284
|
+
if (body.length === 0) return chunk;
|
|
285
|
+
var tag = _scriptTag();
|
|
286
|
+
var headClose = body.search(/<\/head\s*>/i);
|
|
287
|
+
if (headClose !== -1) {
|
|
288
|
+
body = body.slice(0, headClose) + tag + body.slice(headClose);
|
|
289
|
+
} else {
|
|
290
|
+
var bodyOpen = body.match(/<body[^>]*>/i);
|
|
291
|
+
if (bodyOpen) {
|
|
292
|
+
var idx = bodyOpen.index + bodyOpen[0].length;
|
|
293
|
+
body = body.slice(0, idx) + tag + body.slice(idx);
|
|
294
|
+
} else {
|
|
295
|
+
// No <head> or <body> — prepend so the rules at least
|
|
296
|
+
// reach the parser. This matches the bot-disclose fallback.
|
|
297
|
+
body = tag + body;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
injected = true;
|
|
301
|
+
return Buffer.from(body, "utf8");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (origWrite) {
|
|
305
|
+
res.write = function (chunk, encoding, cb) {
|
|
306
|
+
return origWrite(_maybeInject(chunk), encoding, cb);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (origEnd) {
|
|
310
|
+
res.end = function (chunk, encoding, cb) {
|
|
311
|
+
if (chunk) chunk = _maybeInject(chunk);
|
|
312
|
+
return origEnd(chunk, encoding, cb);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return next();
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
create: create,
|
|
321
|
+
EAGERNESS_LEVELS: Object.keys(EAGERNESS_LEVELS),
|
|
322
|
+
ACTION_KEYS: ACTION_KEYS,
|
|
323
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* mime-parse — minimal RFC 5322 + RFC 2045 + RFC 2046 reader shared by
|
|
4
|
+
* lib/mail-bounce.js (RFC 3464 DSN parser) and lib/mail-mdn.js (RFC
|
|
5
|
+
* 3798 MDN parser).
|
|
6
|
+
*
|
|
7
|
+
* Scope:
|
|
8
|
+
*
|
|
9
|
+
* parseHeaderBlock(text) — split a header section into
|
|
10
|
+
* [{name, value}] with RFC 5322 §2.2.3
|
|
11
|
+
* continuation-line unfolding.
|
|
12
|
+
* splitHeadersAndBody(text) — bisect at the empty line per RFC
|
|
13
|
+
* 5322 §2.1; tolerate CRLF and LF
|
|
14
|
+
* line endings (real-world MTA chains
|
|
15
|
+
* normalize differently).
|
|
16
|
+
* findHeader(headers, name) — case-insensitive header lookup;
|
|
17
|
+
* returns the value of the first
|
|
18
|
+
* match or null.
|
|
19
|
+
* parseContentType(value) — `type/subtype; param=value;
|
|
20
|
+
* param="quoted value"` reader.
|
|
21
|
+
* Returns `{ type, params }`.
|
|
22
|
+
* splitMimeParts(body, boundary) — partition a multipart body on
|
|
23
|
+
* `--<boundary>` markers per RFC
|
|
24
|
+
* 2046 §5.1.1; consumes the trailing
|
|
25
|
+
* `--<boundary>--` close.
|
|
26
|
+
*
|
|
27
|
+
* Out of scope (intentionally): MIME word-decoding (RFC 2047), QP /
|
|
28
|
+
* base64 decoding (operators take the part body as-is and decode if
|
|
29
|
+
* Content-Transfer-Encoding asks them to), nested multipart recursion
|
|
30
|
+
* (DSN + MDN reports are flat by spec), full RFC 5322 address-list
|
|
31
|
+
* parsing (mail.js owns the From/To/Cc parser).
|
|
32
|
+
*
|
|
33
|
+
* The reader is byte-encoding-agnostic — strings come in, slices come
|
|
34
|
+
* out. UTF-8 inputs ride through unchanged (RFC 6533 EAI / RFC 6532
|
|
35
|
+
* SMTPUTF8 messages parse correctly). Operator-supplied bytes are
|
|
36
|
+
* length-capped by the calling primitive (mail-bounce: 1 MiB, mail-
|
|
37
|
+
* mdn: 1 MiB) so this module's hot-path doesn't add its own cap.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
function parseHeaderBlock(text) {
|
|
41
|
+
// RFC 5322 §2.2.3 — continuation lines (lines starting with WSP)
|
|
42
|
+
// fold onto the previous header value. The header section ends at
|
|
43
|
+
// the first empty line; subsequent lines belong to the body.
|
|
44
|
+
var lines = text.split(/\r?\n/);
|
|
45
|
+
var unfolded = [];
|
|
46
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
47
|
+
var line = lines[i];
|
|
48
|
+
if (line.length === 0) break;
|
|
49
|
+
if ((line.charAt(0) === " " || line.charAt(0) === "\t") && unfolded.length > 0) {
|
|
50
|
+
unfolded[unfolded.length - 1] += " " + line.replace(/^\s+/, "");
|
|
51
|
+
} else {
|
|
52
|
+
unfolded.push(line);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var headers = [];
|
|
56
|
+
for (var j = 0; j < unfolded.length; j += 1) {
|
|
57
|
+
var l = unfolded[j];
|
|
58
|
+
var colonAt = l.indexOf(":");
|
|
59
|
+
if (colonAt === -1) continue;
|
|
60
|
+
headers.push({
|
|
61
|
+
name: l.slice(0, colonAt).trim(),
|
|
62
|
+
value: l.slice(colonAt + 1).trim(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return headers;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function splitHeadersAndBody(text) {
|
|
69
|
+
// RFC 5322 §2.1 — header / body separator is one empty line. Real-
|
|
70
|
+
// world MTAs disagree on CRLF vs LF; accept both, prefer CRLF when
|
|
71
|
+
// present (the canonical wire form).
|
|
72
|
+
var sepCrlf = text.indexOf("\r\n\r\n");
|
|
73
|
+
var sepLf = text.indexOf("\n\n");
|
|
74
|
+
var sep, sepLen;
|
|
75
|
+
if (sepCrlf !== -1 && (sepLf === -1 || sepCrlf < sepLf)) {
|
|
76
|
+
sep = sepCrlf; sepLen = 4;
|
|
77
|
+
} else if (sepLf !== -1) {
|
|
78
|
+
sep = sepLf; sepLen = 2;
|
|
79
|
+
} else {
|
|
80
|
+
sep = -1; sepLen = 0;
|
|
81
|
+
}
|
|
82
|
+
if (sep === -1) {
|
|
83
|
+
return { headers: parseHeaderBlock(text), body: "" };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
headers: parseHeaderBlock(text.slice(0, sep)),
|
|
87
|
+
body: text.slice(sep + sepLen),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findHeader(headers, name) {
|
|
92
|
+
var target = String(name).toLowerCase();
|
|
93
|
+
for (var i = 0; i < headers.length; i += 1) {
|
|
94
|
+
if (headers[i].name.toLowerCase() === target) return headers[i].value;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseContentType(value) {
|
|
100
|
+
// RFC 2045 §5.1 — `type/subtype` followed by zero or more
|
|
101
|
+
// `; param=value` pairs. Parameter values may be quoted-string with
|
|
102
|
+
// backslash-escaped DQUOTE.
|
|
103
|
+
if (typeof value !== "string") return { type: "", params: {} };
|
|
104
|
+
var semi = value.indexOf(";");
|
|
105
|
+
var typePart = (semi === -1 ? value : value.slice(0, semi)).trim().toLowerCase();
|
|
106
|
+
var rest = semi === -1 ? "" : value.slice(semi + 1);
|
|
107
|
+
var params = {};
|
|
108
|
+
var i = 0;
|
|
109
|
+
while (i < rest.length) {
|
|
110
|
+
while (i < rest.length && (rest.charAt(i) === " " || rest.charAt(i) === "\t" || rest.charAt(i) === ";")) i += 1;
|
|
111
|
+
if (i >= rest.length) break;
|
|
112
|
+
var eq = rest.indexOf("=", i);
|
|
113
|
+
if (eq === -1) break;
|
|
114
|
+
var pname = rest.slice(i, eq).trim().toLowerCase();
|
|
115
|
+
var j = eq + 1;
|
|
116
|
+
while (j < rest.length && (rest.charAt(j) === " " || rest.charAt(j) === "\t")) j += 1;
|
|
117
|
+
var pval;
|
|
118
|
+
if (rest.charAt(j) === '"') {
|
|
119
|
+
var end = j + 1;
|
|
120
|
+
var buf = "";
|
|
121
|
+
while (end < rest.length) {
|
|
122
|
+
var ch = rest.charAt(end);
|
|
123
|
+
if (ch === "\\" && end + 1 < rest.length) {
|
|
124
|
+
buf += rest.charAt(end + 1);
|
|
125
|
+
end += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"') break;
|
|
129
|
+
buf += ch;
|
|
130
|
+
end += 1;
|
|
131
|
+
}
|
|
132
|
+
pval = buf;
|
|
133
|
+
i = end + 1;
|
|
134
|
+
} else {
|
|
135
|
+
var endTok = j;
|
|
136
|
+
while (endTok < rest.length && rest.charAt(endTok) !== ";") endTok += 1;
|
|
137
|
+
pval = rest.slice(j, endTok).trim();
|
|
138
|
+
i = endTok;
|
|
139
|
+
}
|
|
140
|
+
params[pname] = pval;
|
|
141
|
+
}
|
|
142
|
+
return { type: typePart, params: params };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function splitMimeParts(body, boundary) {
|
|
146
|
+
// RFC 2046 §5.1.1 — multipart parts are separated by `--<boundary>`
|
|
147
|
+
// delimiter lines. The closing delimiter is `--<boundary>--`.
|
|
148
|
+
// Preamble (before the first delimiter) and epilogue (after the
|
|
149
|
+
// close) are discarded.
|
|
150
|
+
var parts = [];
|
|
151
|
+
if (typeof boundary !== "string" || boundary.length === 0) return parts;
|
|
152
|
+
var marker = "--" + boundary;
|
|
153
|
+
var lines = String(body).split(/\r?\n/);
|
|
154
|
+
var current = null;
|
|
155
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
156
|
+
var line = lines[i];
|
|
157
|
+
if (line === marker || line === marker + "--") {
|
|
158
|
+
if (current !== null) parts.push(current.join("\r\n"));
|
|
159
|
+
if (line === marker + "--") { current = null; break; }
|
|
160
|
+
current = [];
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (current !== null) current.push(line);
|
|
164
|
+
}
|
|
165
|
+
return parts;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// stripAddressType — RFC 3464 §2.3.1 / RFC 6533 §3.2 / RFC 3798 §3.2.3.
|
|
169
|
+
// `addr-type;recipient` -> `recipient`. The type prefix (rfc822 /
|
|
170
|
+
// utf-8 / x-* on extension) is consumed by the caller for byte-
|
|
171
|
+
// encoding decisions but discarded from the returned recipient.
|
|
172
|
+
function stripAddressType(value) {
|
|
173
|
+
if (typeof value !== "string") return null;
|
|
174
|
+
var semi = value.indexOf(";");
|
|
175
|
+
if (semi === -1) return value.trim();
|
|
176
|
+
return value.slice(semi + 1).trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// addressType — symmetric helper for the BUILD path. RFC 6533 EAI
|
|
180
|
+
// awareness: any non-ASCII byte in the recipient flips the address-
|
|
181
|
+
// type to utf-8 so the wire form names the encoding correctly.
|
|
182
|
+
function addressType(addr) {
|
|
183
|
+
if (typeof addr !== "string") return "rfc822";
|
|
184
|
+
for (var i = 0; i < addr.length; i += 1) {
|
|
185
|
+
if (addr.charCodeAt(i) > 0x7F) return "utf-8";
|
|
186
|
+
}
|
|
187
|
+
return "rfc822";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
parseHeaderBlock: parseHeaderBlock,
|
|
192
|
+
splitHeadersAndBody: splitHeadersAndBody,
|
|
193
|
+
findHeader: findHeader,
|
|
194
|
+
parseContentType: parseContentType,
|
|
195
|
+
splitMimeParts: splitMimeParts,
|
|
196
|
+
stripAddressType: stripAddressType,
|
|
197
|
+
addressType: addressType,
|
|
198
|
+
};
|
package/lib/mtls-ca.js
CHANGED
|
@@ -97,14 +97,24 @@ var DEFAULT_PATHS = {
|
|
|
97
97
|
|
|
98
98
|
var VALID_SEAL_MODES = { required: 1, disabled: 1 };
|
|
99
99
|
|
|
100
|
+
// Resolve relative path entries under `dataDir`; pass absolute paths
|
|
101
|
+
// through unchanged. The pre-v0.8.58 shape always joined under
|
|
102
|
+
// dataDir, which silently overrode an operator-supplied absolute
|
|
103
|
+
// path (e.g. `MTLS_CA_KEY=/etc/ssl/ca.key` → `<dataDir>/etc/ssl/ca.key`).
|
|
104
|
+
// Standard Node `path.join` semantics already preserve absolute
|
|
105
|
+
// arguments — the always-join was an oversight, not by design.
|
|
106
|
+
function _absoluteOrUnderDataDir(dataDir, p) {
|
|
107
|
+
return path.isAbsolute(p) ? p : path.join(dataDir, p);
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
function _resolvePaths(dataDir, paths) {
|
|
101
111
|
var p = Object.assign({}, DEFAULT_PATHS, paths || {});
|
|
102
112
|
return {
|
|
103
|
-
caKey:
|
|
104
|
-
caKeySealed:
|
|
105
|
-
caCert:
|
|
106
|
-
revocations:
|
|
107
|
-
crl:
|
|
113
|
+
caKey: _absoluteOrUnderDataDir(dataDir, p.caKey),
|
|
114
|
+
caKeySealed: _absoluteOrUnderDataDir(dataDir, p.caKeySealed),
|
|
115
|
+
caCert: _absoluteOrUnderDataDir(dataDir, p.caCert),
|
|
116
|
+
revocations: _absoluteOrUnderDataDir(dataDir, p.revocations),
|
|
117
|
+
crl: _absoluteOrUnderDataDir(dataDir, p.crl),
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
120
|
|