@blamejs/pki 0.6.14 → 0.6.15
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 +14 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +8 -252
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/http-digest.js
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- not on pki.*. HTTP Digest access authentication (RFC 7616): parse an untrusted
|
|
6
|
-
// WWW-Authenticate challenge (fail-closed, quoted-string-honoring) and compute the Authorization
|
|
7
|
-
// response header (byte-exact A1/A2/KD). Prefix-agnostic: every thrown code comes from the caller's
|
|
8
|
-
// E(code, msg) factory, never a defineClass class (which would crash on the error path), plus its
|
|
9
|
-
// policy.codes map, so pki.est composes it now and pki.acme / pki.cmp can reuse it as config, not a fork.
|
|
10
|
-
//
|
|
11
|
-
// The challenge is attacker-shaped: a comma inside a quoted value is not a delimiter (the _hasBasicChallenge
|
|
12
|
-
// substring-scan bug class), a missing realm or nonce is rejected and not defaulted, and an unsupported or
|
|
13
|
-
// weak algorithm, or a no-qop challenge, fails closed instead of downgrading.
|
|
4
|
+
// @internal
|
|
14
5
|
|
|
15
6
|
var crypto = require("crypto");
|
|
16
7
|
var constants = require("./constants");
|
|
17
8
|
var intrinsic = require("./guard-intrinsic");
|
|
18
|
-
// Own-key membership through an operation taken at load. Written out, the question reads two
|
|
19
|
-
// replaceable properties to answer one thing, and here it decides whether an auth-param repeats --
|
|
20
|
-
// answering false lets the second copy of a nonce overwrite the first.
|
|
21
9
|
var _hasOwn = intrinsic.hasOwn;
|
|
22
10
|
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
23
11
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
24
12
|
var _fromCharCode = intrinsic.fromCharCode;
|
|
25
|
-
var pkix = require("./schema-pkix");
|
|
13
|
+
var pkix = require("./schema-pkix");
|
|
26
14
|
|
|
27
|
-
// Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
|
|
28
|
-
// "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
|
|
29
|
-
//
|
|
30
|
-
// Null prototype, because the gate on an unknown algorithm is `if (!ALGS[name])`. With an ordinary
|
|
31
|
-
// object literal that question is asked of the prototype chain too, so an algorithm named for any
|
|
32
|
-
// member of Object.prototype answers with a truthy value the table never carried: the
|
|
33
|
-
// unsupported-algorithm refusal is skipped and the descriptor's own fields read as undefined.
|
|
34
15
|
var ALGS = intrinsic.assign(intrinsic.create(null), {
|
|
35
16
|
"SHA-512-256": { hash: "sha512-256", rank: 3, sess: false }, "SHA-512-256-SESS": { hash: "sha512-256", rank: 3, sess: true },
|
|
36
17
|
"SHA-256": { hash: "sha256", rank: 2, sess: false }, "SHA-256-SESS": { hash: "sha256", rank: 2, sess: true },
|
|
@@ -41,18 +22,14 @@ var DEFAULT_CODES = {
|
|
|
41
22
|
noQop: "digest/no-qop", badChallenge: "digest/bad-challenge",
|
|
42
23
|
};
|
|
43
24
|
|
|
44
|
-
// H = lowercase-hex digest; the octet string is fed as latin1 so a UTF-8-encoded credential (charset=UTF-8)
|
|
45
|
-
// is hashed over its exact bytes. KD(secret, data) = H(secret ":" data).
|
|
46
25
|
function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest("hex"); }
|
|
47
26
|
function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
|
|
48
|
-
// Quote a value: wrap in double quotes, backslash-escaping any " or \ (matching /(["\\])/g -> "\\$1").
|
|
49
27
|
function _qstr(v) {
|
|
50
28
|
var s = String(v), out = "";
|
|
51
29
|
for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 34 || c === 92) out += "\\"; out += _fromCharCode(c); }
|
|
52
30
|
return "\"" + out + "\"";
|
|
53
31
|
}
|
|
54
32
|
|
|
55
|
-
// Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
|
|
56
33
|
function _commaSplitOutsideQuotes(s) {
|
|
57
34
|
var out = [], buf = "", inQ = false, esc = false;
|
|
58
35
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -66,7 +43,6 @@ function _commaSplitOutsideQuotes(s) {
|
|
|
66
43
|
out.push(buf);
|
|
67
44
|
return out;
|
|
68
45
|
}
|
|
69
|
-
// Index of the first '=' outside a quoted-string, or -1.
|
|
70
46
|
function _firstEqOutsideQuotes(s) {
|
|
71
47
|
var inQ = false, esc = false;
|
|
72
48
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -78,15 +54,11 @@ function _firstEqOutsideQuotes(s) {
|
|
|
78
54
|
}
|
|
79
55
|
return -1;
|
|
80
56
|
}
|
|
81
|
-
// Unquote a well-formed quoted-string (only ever called after _closedQuote): drop the surrounding
|
|
82
|
-
// quotes and each escaping backslash (matching s.slice(1,-1).replace(/\\(.)/g, "$1")).
|
|
83
57
|
function _unq(s) {
|
|
84
|
-
var out = "", last = 1, n = s.length;
|
|
58
|
+
var out = "", last = 1, n = s.length;
|
|
85
59
|
for (var i = 1; i < n - 1; i++) { if (_charCodeAt(s, i) === 92) { out += _strSlice(s, last, i); last = i + 1; i += 1; } }
|
|
86
|
-
return out + _strSlice(s, last, n - 1);
|
|
60
|
+
return out + _strSlice(s, last, n - 1);
|
|
87
61
|
}
|
|
88
|
-
// A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
|
|
89
|
-
// it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
|
|
90
62
|
function _closedQuote(s) {
|
|
91
63
|
if (s.length < 2 || s.charAt(0) !== "\"") return false;
|
|
92
64
|
var esc = false;
|
|
@@ -98,41 +70,31 @@ function _closedQuote(s) {
|
|
|
98
70
|
}
|
|
99
71
|
return false;
|
|
100
72
|
}
|
|
101
|
-
// A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
|
|
102
|
-
// instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
|
|
103
73
|
function _hasCtl(s) {
|
|
104
74
|
for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
|
|
105
75
|
return false;
|
|
106
76
|
}
|
|
107
77
|
|
|
108
|
-
// Split a WWW-Authenticate header into [{ scheme, paramText }]. A comma-separated segment whose pre-'='
|
|
109
|
-
// text is "token WS token" (a scheme name before the first auth-param key), or a bare token with no '=',
|
|
110
|
-
// STARTS a new challenge; a "key=value" segment is a param continuation of the current challenge.
|
|
111
|
-
// The leading token when `pre` is "token WS more" (/^(\S+)\s+(\S[\s\S]*)$/) -- a scheme name before the
|
|
112
|
-
// first auth-param -- else null. \s is the shared JS-\s predicate (RFC 7230 OWS is a subset).
|
|
113
78
|
function _matchTokenRest(pre) {
|
|
114
79
|
var n = pre.length, p = 0;
|
|
115
|
-
while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
|
|
80
|
+
while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
|
|
116
81
|
if (p === 0) return null;
|
|
117
82
|
var tokEnd = p;
|
|
118
|
-
if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
|
|
83
|
+
if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
|
|
119
84
|
while (p < n && pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
|
|
120
|
-
if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
|
|
85
|
+
if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null;
|
|
121
86
|
return _strSlice(pre, 0, tokEnd);
|
|
122
87
|
}
|
|
123
|
-
// The index past a leading "OWS token OWS" (/^\s*\S+\s+/), or 0 when the string does not begin that way.
|
|
124
|
-
// The caller slices from this index (the slice that replaces the removed .replace).
|
|
125
88
|
function _stripTokenIndex(seg) {
|
|
126
89
|
var n = seg.length, p = 0;
|
|
127
|
-
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
90
|
+
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
128
91
|
var ts = p;
|
|
129
|
-
while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
92
|
+
while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
130
93
|
if (p === ts) return 0;
|
|
131
94
|
var ws = p;
|
|
132
|
-
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
95
|
+
while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1;
|
|
133
96
|
return p === ws ? 0 : p;
|
|
134
97
|
}
|
|
135
|
-
// String.prototype.split(/\s+/): the pieces between \s runs (a leading/trailing run yields a "" element).
|
|
136
98
|
function _splitWhitespace(s) {
|
|
137
99
|
var out = [], i = 0, n = s.length, ps = 0;
|
|
138
100
|
while (i < n) {
|
|
@@ -155,13 +117,12 @@ function _splitChallenges(s) {
|
|
|
155
117
|
var eq = _firstEqOutsideQuotes(seg);
|
|
156
118
|
var pre = (eq < 0 ? seg : _strSlice(seg, 0, eq)).trim();
|
|
157
119
|
var scheme = _matchTokenRest(pre);
|
|
158
|
-
if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); }
|
|
159
|
-
else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); }
|
|
160
|
-
else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; }
|
|
120
|
+
if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); }
|
|
121
|
+
else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); }
|
|
122
|
+
else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; }
|
|
161
123
|
}
|
|
162
124
|
return out;
|
|
163
125
|
}
|
|
164
|
-
// Parse a challenge's params into { key(lower): { value, quoted } }; a repeated key is malformed (fail closed).
|
|
165
126
|
function _parseParams(paramText, E, code) {
|
|
166
127
|
var segs = _commaSplitOutsideQuotes(paramText), map = Object.create(null);
|
|
167
128
|
for (var i = 0; i < segs.length; i++) {
|
|
@@ -175,14 +136,11 @@ function _parseParams(paramText, E, code) {
|
|
|
175
136
|
if (quoted && !_closedQuote(rawVal)) throw E(code, "an unterminated or trailing-garbage Digest quoted-string (RFC 7230 sec. 3.2.6)");
|
|
176
137
|
if (_hasOwn(map, key)) throw E(code, "repeated Digest auth-param " + JSON.stringify(key));
|
|
177
138
|
var value = quoted ? _unq(rawVal) : rawVal;
|
|
178
|
-
// Reject a control octet in any value (CR / LF / NUL / ...): it is a MUST-reject per RFC 7230 qdtext AND a
|
|
179
|
-
// header-injection sink -- an unescaped CR/LF reflected into the outgoing Authorization would split the header.
|
|
180
139
|
if (_hasCtl(value)) throw E(code, "a Digest auth-param value contains a control character (RFC 7230 sec. 3.2.6)");
|
|
181
140
|
map[key] = { value: value, quoted: quoted };
|
|
182
141
|
}
|
|
183
142
|
return map;
|
|
184
143
|
}
|
|
185
|
-
// Structural validation of one Digest challenge's params (RFC 7616 sec. 3.3). Throws on any violation.
|
|
186
144
|
function _validateDigest(paramText, E, code) {
|
|
187
145
|
var p = _parseParams(paramText, E, code);
|
|
188
146
|
if (!p.realm || !p.realm.quoted || p.realm.value === "") throw E(code, "a Digest challenge requires a non-empty quoted realm (RFC 7616 sec. 3.3)");
|
|
@@ -192,38 +150,20 @@ function _validateDigest(paramText, E, code) {
|
|
|
192
150
|
if (p.qop) {
|
|
193
151
|
if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
|
|
194
152
|
qop = p.qop.value.split(",").map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
|
|
195
|
-
// A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
|
|
196
|
-
// absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
|
|
197
|
-
// cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
|
|
198
153
|
if (qop.length === 0) throw E(code, "a present Digest qop directive must list at least one value (RFC 7616 sec. 3.3)");
|
|
199
154
|
}
|
|
200
|
-
// domain (RFC 7616 sec. 3.3): a quoted, space-separated list of URIs defining the protection space. An
|
|
201
|
-
// unquoted domain is malformed and rejected (fail closed like realm / nonce / qop) instead of silently
|
|
202
|
-
// widened: widening a mis-encoded scope to "the whole server" would send credentials more broadly than the
|
|
203
|
-
// server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
|
|
204
|
-
// the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
|
|
205
155
|
if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
|
|
206
156
|
var domain = (p.domain && p.domain.value.trim() !== "")
|
|
207
157
|
? _splitWhitespace(p.domain.value.trim()) : null;
|
|
208
|
-
// charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
|
|
209
|
-
// other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
|
|
210
|
-
// rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
|
|
211
158
|
if (p.charset && (p.charset.quoted || String(p.charset.value).toUpperCase() !== "UTF-8")) throw E(code, "the Digest charset must be the unquoted token UTF-8 (RFC 7616 sec. 3.3)");
|
|
212
|
-
// stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
|
|
213
|
-
// (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
|
|
214
|
-
// could shadow a conforming offer.
|
|
215
159
|
if (p.stale) {
|
|
216
160
|
var sv = String(p.stale.value).toLowerCase();
|
|
217
161
|
if (p.stale.quoted || (sv !== "true" && sv !== "false")) throw E(code, "the Digest stale directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
|
|
218
162
|
}
|
|
219
|
-
// userhash (RFC 7616 sec. 3.3) is likewise the unquoted token "true" or "false". A quoted or otherwise invalid
|
|
220
|
-
// value is malformed and rejected (skipped in selection), never silently changing how the username is sent.
|
|
221
163
|
if (p.userhash) {
|
|
222
164
|
var uhv = String(p.userhash.value).toLowerCase();
|
|
223
165
|
if (p.userhash.quoted || (uhv !== "true" && uhv !== "false")) throw E(code, "the Digest userhash directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
|
|
224
166
|
}
|
|
225
|
-
// opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An unquoted opaque is malformed and
|
|
226
|
-
// rejected (skipped in selection), never accepted, where it could shadow a conforming offer.
|
|
227
167
|
if (p.opaque && !p.opaque.quoted) throw E(code, "the Digest opaque directive must be a quoted-string (RFC 7616 sec. 3.3)");
|
|
228
168
|
return {
|
|
229
169
|
scheme: "Digest", realm: p.realm.value, nonce: p.nonce.value, qop: qop, domain: domain,
|
|
@@ -235,21 +175,9 @@ function _validateDigest(paramText, E, code) {
|
|
|
235
175
|
};
|
|
236
176
|
}
|
|
237
177
|
|
|
238
|
-
// parseChallenge(www, E, code, policy?) -> the most-secure USABLE structurally-valid Digest challenge,
|
|
239
|
-
// OR null (no Digest challenge present), OR throw E(code) (a Digest challenge present but none valid).
|
|
240
|
-
// Selection is policy-aware: a challenge answer() would refuse under the active policy (an unsupported /
|
|
241
|
-
// MD5-when-disallowed algorithm, or a qop this client cannot satisfy) is ranked BELOW every usable one, so a
|
|
242
|
-
// higher-algorithm-rank but unusable offer (e.g. a SHA-512-256 no-qop challenge under the default policy)
|
|
243
|
-
// never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth"), per RFC 7616 sec. 3.3. When no
|
|
244
|
-
// challenge is usable the highest-algorithm-rank one is still returned, so answer() reports the specific
|
|
245
|
-
// policy reason (which opt to set) in place of a generic "no challenge".
|
|
246
178
|
function parseChallenge(www, E, code, policy) {
|
|
247
179
|
var pol = policy || {};
|
|
248
180
|
var codes = pol.codes || DEFAULT_CODES;
|
|
249
|
-
// preferStale marks a CREDENTIAL-REJECTION context (a 401 answering a credentialed request in the same space):
|
|
250
|
-
// there, a stale=true offer is RETRYABLE with a fresh nonce while a stale=false offer is a terminal rejection,
|
|
251
|
-
// so a retryable offer must outrank a stronger non-retryable one (RFC 7616 sec. 3.3). In the initial context
|
|
252
|
-
// it is not set and stale plays no part.
|
|
253
181
|
var preferStale = !!pol.preferStale;
|
|
254
182
|
var raw = String(www == null ? "" : www);
|
|
255
183
|
if (raw.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E(code, "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap");
|
|
@@ -259,8 +187,6 @@ function parseChallenge(www, E, code, policy) {
|
|
|
259
187
|
if (challenges[i].scheme.toLowerCase() !== "digest") continue;
|
|
260
188
|
sawDigest = true;
|
|
261
189
|
var parsed;
|
|
262
|
-
// A MALFORMED offer is skipped, not fatal: parseChallenge's contract is to throw only when NO valid Digest
|
|
263
|
-
// offer exists (RFC 7616 sec. 3.3), so a bad offer alongside a good one still authenticates on the good one.
|
|
264
190
|
try { parsed = _validateDigest(challenges[i].paramText, E, code); }
|
|
265
191
|
catch (_e) {
|
|
266
192
|
continue;
|
|
@@ -268,18 +194,8 @@ function parseChallenge(www, E, code, policy) {
|
|
|
268
194
|
var alg = ALGS[parsed.algorithm];
|
|
269
195
|
var rank = alg ? alg.rank : 0;
|
|
270
196
|
var usable = _rejection(parsed, pol, codes) === null;
|
|
271
|
-
// APPLICABLE: the offer's own protection space covers the CURRENT request target (RFC 7616 sec. 3.5). An
|
|
272
|
-
// offer whose `domain` excludes this request cannot authenticate it (the caller would omit the credential),
|
|
273
|
-
// so it must not shadow a weaker offer that does apply. Only checked when the caller supplies the request
|
|
274
|
-
// context; otherwise every offer is treated as applicable.
|
|
275
197
|
var applicable = pol.requestTarget === undefined ? true : inProtectionSpace(parsed, pol.requestOrigin, pol.requestTarget);
|
|
276
|
-
// A retryable offer (only meaningful in the rejection context) is stale=true, carries a fresh nonce, and is
|
|
277
|
-
// for the same realm whose credential was just rejected: the conditions under which the caller re-answers.
|
|
278
|
-
// A stale offer for a different realm is a new protection space (not a retry), and a stale offer repeating
|
|
279
|
-
// the prior nonce cannot re-answer, so neither must out-rank a weaker genuinely retryable offer (RFC 7616 sec. 3.3).
|
|
280
198
|
var retryable = preferStale && !!parsed.stale && parsed.nonce !== pol.priorNonce && parsed.realm === pol.priorRealm;
|
|
281
|
-
// Usability dominates; then an offer that APPLIES to this request; then (in a rejection) a retryable offer;
|
|
282
|
-
// then the stronger algorithm.
|
|
283
199
|
if (best === null ||
|
|
284
200
|
(usable && !bestUsable) ||
|
|
285
201
|
(usable === bestUsable && applicable && !bestApplicable) ||
|
|
@@ -293,22 +209,17 @@ function parseChallenge(www, E, code, policy) {
|
|
|
293
209
|
return null;
|
|
294
210
|
}
|
|
295
211
|
|
|
296
|
-
// Credential octets for the given charset (RFC 7616 sec. 4): UTF-8 -> the exact UTF-8 bytes; else the
|
|
297
|
-
// string as-is (ASCII / latin1). H() hashes over latin1, so a UTF-8 credential is fed as its octet string.
|
|
298
212
|
function _octets(s, charset) {
|
|
299
213
|
var v = s == null ? "" : String(s);
|
|
300
214
|
return (charset && String(charset).toUpperCase() === "UTF-8") ? Buffer.from(v, "utf8").toString("latin1") : v;
|
|
301
215
|
}
|
|
302
216
|
|
|
303
|
-
// Does the string carry any non-ASCII code unit (> U+007F)? Such a UTF-8 username is sent via `username*`.
|
|
304
217
|
function _hasNonAscii(s) {
|
|
305
218
|
s = String(s == null ? "" : s);
|
|
306
219
|
for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) > 0x7F) return true; }
|
|
307
220
|
return false;
|
|
308
221
|
}
|
|
309
222
|
|
|
310
|
-
// RFC 5987 / RFC 8187 percent-encoding of a string's UTF-8 octets: attr-char (ALPHA / DIGIT / "!#$&+-.^_`|~")
|
|
311
|
-
// is kept literal, every other octet is %XX (upper-hex). Used for the Digest `username*` extended value.
|
|
312
223
|
function _pctEncodeUtf8(s) {
|
|
313
224
|
var bytes = Buffer.from(String(s == null ? "" : s), "utf8");
|
|
314
225
|
var out = "";
|
|
@@ -325,11 +236,6 @@ function _pctEncodeUtf8(s) {
|
|
|
325
236
|
return out;
|
|
326
237
|
}
|
|
327
238
|
|
|
328
|
-
// The policy gates a parsed challenge must clear to be answerable (RFC 7616 sec. 3.4): a supported
|
|
329
|
-
// algorithm, MD5 only when allowed, and a qop this client can satisfy (auth / auth-int, or no-qop only when
|
|
330
|
-
// allowLegacyQop). Returns a { code, msg } rejection or null. This is the SINGLE definition both answer()
|
|
331
|
-
// and parseChallenge() consult, so challenge SELECTION can never rank a challenge as usable that answer()
|
|
332
|
-
// would then refuse (or vice-versa) -- the two cannot drift.
|
|
333
239
|
function _rejection(challenge, pol, codes) {
|
|
334
240
|
var alg = ALGS[challenge.algorithm];
|
|
335
241
|
if (!alg) return { code: codes.unsupportedAlgorithm, msg: "unsupported Digest algorithm " + challenge.algorithm + " (supported: SHA-512-256, SHA-256, MD5)" };
|
|
@@ -342,7 +248,6 @@ function _rejection(challenge, pol, codes) {
|
|
|
342
248
|
return null;
|
|
343
249
|
}
|
|
344
250
|
|
|
345
|
-
// answer(challenge, { method, uri, username, password, body, policy, rng }, E) -> the Authorization header value.
|
|
346
251
|
function answer(challenge, params, E) {
|
|
347
252
|
var pol = params.policy || {};
|
|
348
253
|
var codes = pol.codes || DEFAULT_CODES;
|
|
@@ -351,15 +256,7 @@ function answer(challenge, params, E) {
|
|
|
351
256
|
var alg = ALGS[challenge.algorithm];
|
|
352
257
|
var useQop = challenge.qop.indexOf("auth") !== -1 ? "auth" : (challenge.qop.indexOf("auth-int") !== -1 ? "auth-int" : null);
|
|
353
258
|
var cnonce = String((params.rng || function () { return crypto.randomBytes(18).toString("base64"); })());
|
|
354
|
-
// The realm is used with its incoming header OCTETS unchanged (RFC 7616 sec. 4). The realm arrives from the
|
|
355
|
-
// wire, so a transport that exposes header bytes as Latin-1 (the Node default) already presents the server's
|
|
356
|
-
// UTF-8 octets one-per-char; re-encoding them as UTF-8 (as user / pass, which come from the caller as Unicode
|
|
357
|
-
// strings, correctly do) would DOUBLE-encode and make the digest disagree with the server. H() hashes over
|
|
358
|
-
// Latin-1, so the raw realm string contributes exactly the octets the server used.
|
|
359
259
|
var realm = challenge.realm, nonce = challenge.nonce;
|
|
360
|
-
// Normalize UTF-8 credentials to NFC before hashing (RFC 7616 sec. 4): a server hashes the normalized form,
|
|
361
|
-
// so a canonically-decomposed username / password (e + combining accent vs the precomposed char) must be
|
|
362
|
-
// composed first or HA1 disagrees. That one normalized username feeds A1, userhash, and username*.
|
|
363
260
|
var isUtf8 = String(challenge.charset || "").toUpperCase() === "UTF-8";
|
|
364
261
|
var pUser = params.username == null ? "" : String(params.username);
|
|
365
262
|
var pPass = params.password == null ? "" : String(params.password);
|
|
@@ -372,19 +269,11 @@ function answer(challenge, params, E) {
|
|
|
372
269
|
? params.method + ":" + params.uri + ":" + H(alg.hash, params.body == null ? "" : params.body)
|
|
373
270
|
: params.method + ":" + params.uri;
|
|
374
271
|
var HA2 = H(alg.hash, A2);
|
|
375
|
-
// The nonce-count: a (nonce, nc) pair MUST NOT be reused (RFC 7616 sec. 3.4), so a request that REUSES a nonce
|
|
376
|
-
// (a same-origin redirect, an auth-int retry) increments nc; a fresh nonce restarts at 1. The caller supplies
|
|
377
|
-
// the count for this nonce (default 1); formatted as 8 lowercase hex.
|
|
378
272
|
var ncNum = (typeof params.nc === "number" && params.nc >= 1) ? Math.floor(params.nc) : 1;
|
|
379
273
|
var nc = _strSlice("0000000" + ncNum.toString(16), -8);
|
|
380
274
|
var response = useQop
|
|
381
275
|
? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
|
|
382
276
|
: KD(alg.hash, HA1, nonce + ":" + HA2);
|
|
383
|
-
// The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4),
|
|
384
|
-
// an ASCII hex value; A1 above still used the actual username. Otherwise a charset=UTF-8 username containing
|
|
385
|
-
// non-ASCII characters MUST be carried in the extended `username*` form (RFC 5987 / RFC 8187 percent-encoded
|
|
386
|
-
// UTF-8), because a server reads the legacy quoted `username` as ISO-8859-1 and could not resolve the account;
|
|
387
|
-
// `username` and `username*` MUST NOT both appear. An ASCII (or userhash) username uses the legacy quoted form.
|
|
388
277
|
var parts;
|
|
389
278
|
if (!challenge.userhash && isUtf8 && _hasNonAscii(pUser)) {
|
|
390
279
|
parts = ["username*=UTF-8''" + _pctEncodeUtf8(pUser)];
|
|
@@ -394,8 +283,6 @@ function answer(challenge, params, E) {
|
|
|
394
283
|
}
|
|
395
284
|
parts.push("realm=" + _qstr(realm), "nonce=" + _qstr(nonce), "uri=" + _qstr(params.uri), "algorithm=" + challenge.algorithm);
|
|
396
285
|
if (useQop) { parts.push("qop=" + useQop); parts.push("nc=" + nc); }
|
|
397
|
-
// The cnonce directive is emitted whenever the server needs it to recompute the response: under a qop, and
|
|
398
|
-
// also under a -sess algorithm whose A1 folds in the cnonce even with no qop (RFC 7616 sec. 3.4.2 / 3.9.1).
|
|
399
286
|
if (useQop || alg.sess) { parts.push("cnonce=" + _qstr(cnonce)); }
|
|
400
287
|
parts.push("response=" + _qstr(response));
|
|
401
288
|
if (challenge.opaque != null) parts.push("opaque=" + _qstr(challenge.opaque));
|
|
@@ -403,28 +290,13 @@ function answer(challenge, params, E) {
|
|
|
403
290
|
return "Digest " + parts.join(", ");
|
|
404
291
|
}
|
|
405
292
|
|
|
406
|
-
// Parse one domain URI entry (RFC 7616 sec. 3.3) into { origin, full }: an abs_path ("/a", "/a?x=1") is
|
|
407
|
-
// relative to the responding server -> origin null (any same-origin request); an absolute URI carries its own
|
|
408
|
-
// authority -> origin "scheme://authority" (lowercased, default port normalized) and matches only a request to
|
|
409
|
-
// that same origin, since an entry to a different host names a protection space on a different server. `full` is
|
|
410
|
-
// the pathname + query the request URI is prefix-compared against. A relative / unparseable absolute entry -> null.
|
|
411
293
|
function _domainEntry(d) {
|
|
412
294
|
d = String(d == null ? "" : d);
|
|
413
295
|
if (d.charAt(0) === "/") return { origin: null, full: d };
|
|
414
|
-
// An absolute entry is parsed through the URL constructor so its origin is NORMALIZED the same way the
|
|
415
|
-
// request's URL.origin is -- an explicit default port (https :443 / http :80) is dropped, so a domain naming
|
|
416
|
-
// "https://h:443/x" still matches a request whose origin normalizes to "https://h". A relative / unparseable
|
|
417
|
-
// entry throws and never matches (fail closed).
|
|
418
296
|
try { var u = new URL(d); return { origin: u.origin.toLowerCase(), full: (u.pathname || "/") + (u.search || "") }; }
|
|
419
297
|
catch (_e) { return null; }
|
|
420
298
|
}
|
|
421
299
|
|
|
422
|
-
// inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) -> is the request within the challenge's
|
|
423
|
-
// protection space? An absent / empty domain means "all URIs on the responding server" (RFC 7616 sec. 3.3), so
|
|
424
|
-
// any same-origin request matches; otherwise membership is by LITERAL URI PREFIX (RFC 7616 sec. 3.5 / RFC 2617
|
|
425
|
-
// sec. 1.2): the request's pathname + query must have a listed domain URI as a prefix, and for an absolute entry
|
|
426
|
-
// the same origin. The query is part of the prefix, so /enroll?tenant=a does not cover /enroll?tenant=b while a
|
|
427
|
-
// query-less /enroll covers its whole subtree (including /enroll?...). Used to scope where a cached answer is reused.
|
|
428
300
|
function inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) {
|
|
429
301
|
var domain = challenge && challenge.domain;
|
|
430
302
|
if (!domain || !domain.length) return true;
|
|
@@ -433,7 +305,7 @@ function inProtectionSpace(challenge, requestOrigin, requestPathAndSearch) {
|
|
|
433
305
|
for (var i = 0; i < domain.length; i++) {
|
|
434
306
|
var e = _domainEntry(domain[i]);
|
|
435
307
|
if (e === null) continue;
|
|
436
|
-
if (e.origin !== null && e.origin !== origin) continue;
|
|
308
|
+
if (e.origin !== null && e.origin !== origin) continue;
|
|
437
309
|
if (full.indexOf(e.full) === 0) return true;
|
|
438
310
|
}
|
|
439
311
|
return false;
|
package/lib/http-retry-after.js
CHANGED
|
@@ -1,66 +1,41 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the shared HTTP Retry-After parser (RFC 7231 sec. 7.1.3): a delay-seconds count OR an
|
|
6
|
-
// HTTP-date (sec. 7.1.1.1). The enrollment protocol clients (pki.est now, pki.acme next) all surface a
|
|
7
|
-
// server's Retry-After as a bounded delay, never sleeping on it in a single-step call, so the
|
|
8
|
-
// delay-seconds | HTTP-date grammar and the one-year ceiling live in one place both compose, error-
|
|
9
|
-
// factory-parameterized so each domain keeps its own typed verdict. A malformed / overflowing value
|
|
10
|
-
// fails closed (the caller's E(code, ...)); the value is never returned unparsed.
|
|
4
|
+
// @internal
|
|
11
5
|
|
|
12
6
|
var constants = require("./constants");
|
|
13
7
|
|
|
14
|
-
// The largest delay this parser surfaces as a number: a generous one-year ceiling that keeps the
|
|
15
|
-
// value a safe integer and rejects an overflowing / nonsensical delay-seconds or far-future date.
|
|
16
8
|
var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(1);
|
|
17
9
|
|
|
18
|
-
// The three HTTP-date forms (RFC 7231 sec. 7.1.1.1): IMF-fixdate (the required form), and the obsolete
|
|
19
|
-
// rfc850-date / asctime-date a recipient must still accept. Gating Date.parse on this grammar keeps its
|
|
20
|
-
// permissiveness (an ISO string like "2026-07-10") from passing as a valid Retry-After header.
|
|
21
10
|
var MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
|
|
22
11
|
var DOW_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
23
12
|
var DOW_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
24
13
|
|
|
25
|
-
// The grammar is scanned character by character (no regex): each form is a fixed layout of a
|
|
26
|
-
// day-of-week name, delimiters, an all-digit field, a three-letter month name, and an HH:MM:SS time.
|
|
27
14
|
function _isDigit(ch) { return ch >= "0" && ch <= "9"; }
|
|
28
|
-
// The two-digit / four-digit integer at `i`, or -1 when those positions are not all digits.
|
|
29
15
|
function _digits2(s, i) { return _isDigit(s.charAt(i)) && _isDigit(s.charAt(i + 1)) ? (s.charCodeAt(i) - 48) * 10 + (s.charCodeAt(i + 1) - 48) : -1; }
|
|
30
16
|
function _digits4(s, i) { for (var k = 0; k < 4; k++) if (!_isDigit(s.charAt(i + k))) return -1; return parseInt(s.slice(i, i + 4), 10); }
|
|
31
|
-
// The length of a name from `list` matching `s` at `i` (the day-of-week alternation), or -1.
|
|
32
17
|
function _matchName(s, i, list) { for (var j = 0; j < list.length; j++) { if (s.slice(i, i + list[j].length) === list[j]) return list[j].length; } return -1; }
|
|
33
|
-
// The three-letter month at `i` when it is a real month name, else null.
|
|
34
18
|
function _monthAt(s, i) { return _matchName(s, i, MONTHS) === -1 ? null : s.slice(i, i + 3); }
|
|
35
|
-
// An "HH:MM:SS" time at `i` -> { hh, mi, ss, next } (next = i + 8), or null.
|
|
36
19
|
function _timeAt(s, i) {
|
|
37
20
|
var hh = _digits2(s, i); if (hh < 0 || s.charAt(i + 2) !== ":") return null;
|
|
38
21
|
var mi = _digits2(s, i + 3); if (mi < 0 || s.charAt(i + 5) !== ":") return null;
|
|
39
22
|
var ss = _digits2(s, i + 6); if (ss < 0) return null;
|
|
40
23
|
return { hh: hh, mi: mi, ss: ss, next: i + 8 };
|
|
41
24
|
}
|
|
42
|
-
// A non-empty run of ASCII digits (matching /^\d+$/, the delay-seconds form).
|
|
43
25
|
function _isAllDigits(s) {
|
|
44
26
|
if (s.length === 0) return false;
|
|
45
27
|
for (var i = 0; i < s.length; i++) if (!_isDigit(s.charAt(i))) return false;
|
|
46
28
|
return true;
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
// Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
|
|
50
|
-
// year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
|
|
51
|
-
// otherwise be more than 50 years ahead). A fixed cutoff would misdate a year near the century boundary.
|
|
52
|
-
// With no receipt time, fall back to the RFC 6265 fixed rule (< 70 -> 2000s, else 1900s).
|
|
53
31
|
function _rfc850Year(yy, refMs) {
|
|
54
32
|
if (typeof refMs !== "number" || !isFinite(refMs)) return yy + (yy < 70 ? 2000 : 1900);
|
|
55
33
|
var refYear = new Date(refMs).getUTCFullYear();
|
|
56
34
|
var full = Math.floor(refYear / 100) * 100 + yy;
|
|
57
|
-
// Move to the previous century only when the same-century year would be more than 50 years ahead; never
|
|
58
|
-
// advance a past year to the future (an old two-digit year stays in the past).
|
|
59
35
|
if (full - refYear > 50) full -= 100;
|
|
60
36
|
return full;
|
|
61
37
|
}
|
|
62
38
|
|
|
63
|
-
// IMF-fixdate (the required form): "Sun, 06 Nov 1994 08:49:37 GMT".
|
|
64
39
|
function _scanImf(s) {
|
|
65
40
|
var p = _matchName(s, 0, DOW_SHORT);
|
|
66
41
|
if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
|
|
@@ -70,8 +45,6 @@ function _scanImf(s) {
|
|
|
70
45
|
var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
|
|
71
46
|
return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
|
|
72
47
|
}
|
|
73
|
-
// rfc850-date (obsolete, still accepted): "Sunday, 06-Nov-94 08:49:37 GMT" -- a two-digit year expanded
|
|
74
|
-
// relative to the receipt year.
|
|
75
48
|
function _scanRfc850(s, refMs) {
|
|
76
49
|
var p = _matchName(s, 0, DOW_LONG);
|
|
77
50
|
if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
|
|
@@ -81,7 +54,6 @@ function _scanRfc850(s, refMs) {
|
|
|
81
54
|
var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
|
|
82
55
|
return { day: dd, monName: mo, year: _rfc850Year(yy, refMs), hh: t.hh, mi: t.mi, ss: t.ss };
|
|
83
56
|
}
|
|
84
|
-
// asctime-date (obsolete, still accepted): "Sun Nov 6 08:49:37 1994" -- a space-padded one/two-digit day.
|
|
85
57
|
function _scanAsctime(s) {
|
|
86
58
|
var p = _matchName(s, 0, DOW_SHORT);
|
|
87
59
|
if (p === -1 || s.charAt(p) !== " ") return null; p += 1;
|
|
@@ -94,10 +66,6 @@ function _scanAsctime(s) {
|
|
|
94
66
|
return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
|
|
95
67
|
}
|
|
96
68
|
|
|
97
|
-
// Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
|
|
98
|
-
// extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
|
|
99
|
-
// no GMT token so delegating would parse it in local time, and the round-trip check rejects an
|
|
100
|
-
// impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
|
|
101
69
|
function httpDateMs(s, refMs) {
|
|
102
70
|
var f = _scanImf(s) || _scanRfc850(s, refMs) || _scanAsctime(s);
|
|
103
71
|
if (!f) return NaN;
|
|
@@ -110,25 +78,13 @@ function httpDateMs(s, refMs) {
|
|
|
110
78
|
return when;
|
|
111
79
|
}
|
|
112
80
|
|
|
113
|
-
// parse(value, opts) -> { retryAfterSeconds, retryAfterDate }. A PRESENT Retry-After value (the caller
|
|
114
|
-
// gates absence with its own code) is a delay-seconds integer -> retryAfterSeconds, or an HTTP-date ->
|
|
115
|
-
// retryAfterDate (epoch ms) plus, when opts.now (epoch ms) is given, a bounded retryAfterSeconds. Either
|
|
116
|
-
// form beyond the one-year ceiling, or a value that is neither, fails closed via opts.E(opts.code, ...).
|
|
117
|
-
// opts.cap (seconds) clamps a value above the cap to the cap instead of rejecting it; opts.lenient surfaces
|
|
118
|
-
// an otherwise-rejected value as a null retryAfterSeconds. Both exist for a caller (e.g. an ARI poll
|
|
119
|
-
// cadence) for whom the value is advisory and must not discard the response. Nothing sleeps here; the
|
|
120
|
-
// value is surfaced for the caller to decide.
|
|
121
81
|
function parse(value, opts) {
|
|
122
82
|
opts = opts || {};
|
|
123
83
|
var raStr = String(value).trim();
|
|
124
84
|
var out = { retryAfterSeconds: null, retryAfterDate: null };
|
|
125
85
|
if (_isAllDigits(raStr)) {
|
|
126
86
|
var n = parseInt(raStr, 10);
|
|
127
|
-
// opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
|
|
128
|
-
// ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
|
|
129
87
|
if (typeof opts.cap === "number" && n > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
|
|
130
|
-
// opts.lenient: a caller for whom the value is purely advisory wants an unparseable one surfaced as
|
|
131
|
-
// null (retryAfterSeconds stays null) instead of a hard reject that would discard the whole response.
|
|
132
88
|
if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
|
|
133
89
|
out.retryAfterSeconds = n;
|
|
134
90
|
return out;
|
|
@@ -137,8 +93,6 @@ function parse(value, opts) {
|
|
|
137
93
|
if (isNaN(when)) { if (opts.lenient) return out; throw _fail(opts, "a Retry-After must be delay-seconds or a valid HTTP-date (RFC 7231 sec. 7.1.1.1/7.1.3), got " + JSON.stringify(raStr)); }
|
|
138
94
|
out.retryAfterDate = when;
|
|
139
95
|
if (typeof opts.now === "number" && isFinite(opts.now)) {
|
|
140
|
-
// Round the remaining whole-second delay UP (a sub-second date must not retry before the requested
|
|
141
|
-
// time), clamping a past date to 0.
|
|
142
96
|
var d = Math.max(0, Math.ceil((when - opts.now) / constants.TIME.seconds(1)));
|
|
143
97
|
if (typeof opts.cap === "number" && d > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
|
|
144
98
|
if (d > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After date is beyond the supported horizon (" + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
|