kinetex 1.3.0 → 1.4.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/README.md +246 -9
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +2545 -550
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +133 -19
- package/dist/cjs/cache.js +49 -7
- package/dist/cjs/circuit-breaker.js +45 -3
- package/dist/cjs/client.js +387 -104
- package/dist/cjs/cookie-parser.js +103 -5
- package/dist/cjs/cookie-store.js +125 -28
- package/dist/cjs/core.js +465 -66
- package/dist/cjs/dedup.js +49 -11
- package/dist/cjs/digest.js +160 -24
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +303 -45
- package/dist/cjs/interceptors.js +221 -7
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +168 -15
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +247 -22
- package/dist/cjs/progress.js +177 -27
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +131 -15
- package/dist/cjs/sse.js +173 -43
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/ws.js +19 -10
- package/dist/esm/aws-sigv4.js +133 -19
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +49 -7
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +45 -3
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +387 -104
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +103 -5
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +125 -28
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +465 -66
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +49 -11
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +160 -24
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +164 -24
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +303 -45
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +221 -7
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +89 -40
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +168 -15
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/mod.js +3 -2
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/pagination.js +247 -22
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +177 -27
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +413 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +316 -47
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +131 -15
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +173 -43
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js +191 -45
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +222 -48
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/ws.js +19 -10
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +19 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +14 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +69 -11
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts +0 -17
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/cookie-store.d.ts.map +1 -1
- package/dist/types/core.d.ts +103 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +17 -37
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +45 -27
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +102 -0
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts +19 -2
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts +22 -3
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mod.d.ts +5 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/pagination.d.ts +0 -25
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts +50 -0
- package/dist/types/proxy.d.ts.map +1 -0
- package/dist/types/response.d.ts +7 -1
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/sse.d.ts.map +1 -1
- package/dist/types/types.d.ts +114 -3
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -14
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/dedup.js
CHANGED
|
@@ -75,29 +75,54 @@ export class DedupMap {
|
|
|
75
75
|
const effectiveSignal = signal ?? this._globalSignal;
|
|
76
76
|
// Handle abort signal (F-2): if signal is already aborted, don't dedupe
|
|
77
77
|
if (effectiveSignal?.aborted) {
|
|
78
|
+
// Still a real network call. It used to return before the counters were
|
|
79
|
+
// touched, so a client whose method filter excluded everything reported
|
|
80
|
+
// totalRequests 0 and a hitRate of 0 after any number of real requests
|
|
81
|
+
// — and `misses` is documented as "requests that triggered a real
|
|
82
|
+
// network call". Both bypasses below now count.
|
|
83
|
+
this._misses++;
|
|
78
84
|
return factory();
|
|
79
85
|
}
|
|
80
86
|
if (!this.methods.has(method.toUpperCase())) {
|
|
87
|
+
this._misses++;
|
|
81
88
|
return factory();
|
|
82
89
|
}
|
|
83
|
-
|
|
90
|
+
// The method is canonicalised before it reaches the key function. The
|
|
91
|
+
// methods filter has always been case-insensitive, but the key was built
|
|
92
|
+
// from the caller's spelling, so `execute("get", u)` and
|
|
93
|
+
// `execute("GET", u)` were two different keys and never coalesced — the
|
|
94
|
+
// one case a caller gets wrong most often, and one the library otherwise
|
|
95
|
+
// treats as equal everywhere.
|
|
96
|
+
const canonicalMethod = method.toUpperCase();
|
|
97
|
+
const key = this.keyFn(canonicalMethod, url, headers);
|
|
84
98
|
const existing = this.inflight.get(key);
|
|
85
99
|
if (existing) {
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Date.now() - existing.resolvedAt <= effectiveWindowMs) {
|
|
100
|
+
// In-flight: share the promise. This is coalescing, not caching, so it
|
|
101
|
+
// is not gated on the window.
|
|
102
|
+
if (existing.resolvedAt === null) {
|
|
90
103
|
this._hits++;
|
|
91
|
-
// Return the typed promise directly - preserves error type
|
|
92
104
|
return existing.promise;
|
|
93
105
|
}
|
|
94
|
-
//
|
|
95
|
-
if (existing.resolvedAt
|
|
106
|
+
// Within window: return cached result
|
|
107
|
+
if (effectiveWindowMs > 0 && Date.now() - existing.resolvedAt <= effectiveWindowMs) {
|
|
96
108
|
this._hits++;
|
|
109
|
+
// Return the typed promise directly - preserves error type
|
|
97
110
|
return existing.promise;
|
|
98
111
|
}
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
if (effectiveWindowMs > 0) {
|
|
113
|
+
// This caller's own window has lapsed — drop the stale entry and lead.
|
|
114
|
+
this.inflight.delete(key);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// This caller explicitly asked for no window, so it gets its own
|
|
118
|
+
// request. It used to fall through to `inflight.delete(key)` and lead
|
|
119
|
+
// as well, which destroyed a window some *other* caller had
|
|
120
|
+
// established and was still relying on: one `windowMs: 0` call was
|
|
121
|
+
// enough to empty the cache for everyone, and the next windowed caller
|
|
122
|
+
// had to go to the network for a result that was still in hand.
|
|
123
|
+
this._misses++;
|
|
124
|
+
return factory();
|
|
125
|
+
}
|
|
101
126
|
}
|
|
102
127
|
this._misses++;
|
|
103
128
|
// Create entry and register it in the inflight map BEFORE starting the async work
|
|
@@ -132,6 +157,16 @@ export class DedupMap {
|
|
|
132
157
|
})().then((result) => {
|
|
133
158
|
entry.resolvedAt = Date.now();
|
|
134
159
|
entry.result = result;
|
|
160
|
+
// This entry may have lost the key while the factory was running: an
|
|
161
|
+
// abort, a `clear()`, or an `invalidate()` all drop it, and a later
|
|
162
|
+
// caller for the same URL then becomes the leader. Registering a
|
|
163
|
+
// window timer here regardless re-inserted a timer for a key someone
|
|
164
|
+
// else now owns, and when it fired it deleted their still-in-flight
|
|
165
|
+
// entry — so the request after that started a third copy. The
|
|
166
|
+
// identity check is what makes "who owns this key" decidable.
|
|
167
|
+
if (this.inflight.get(key) !== entry) {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
135
170
|
if (effectiveWindowMs <= 0) {
|
|
136
171
|
this.inflight.delete(key);
|
|
137
172
|
}
|
|
@@ -155,7 +190,10 @@ export class DedupMap {
|
|
|
155
190
|
}, (err) => {
|
|
156
191
|
entry.resolvedAt = Date.now();
|
|
157
192
|
entry.error = err;
|
|
158
|
-
|
|
193
|
+
// Same ownership rule as the success path: an entry that has already
|
|
194
|
+
// been dropped must not delete the entry that replaced it.
|
|
195
|
+
if (this.inflight.get(key) === entry)
|
|
196
|
+
this.inflight.delete(key);
|
|
159
197
|
const existingTimeout = this.timeouts.get(key);
|
|
160
198
|
if (existingTimeout !== undefined) {
|
|
161
199
|
clearTimeout(existingTimeout);
|
package/dist/cjs/digest.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* WWW-Authenticate challenge, compute the response hash, and format the
|
|
9
9
|
* Authorization header in one call.
|
|
10
10
|
*/
|
|
11
|
-
import { randomBytes } from "./utils.js";
|
|
11
|
+
import { randomBytes, uint8ArrayToBase64 } from "./utils.js";
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// §1 MD5 — Pure JS (no Web Crypto dependency)
|
|
14
14
|
// ============================================================================
|
|
@@ -133,16 +133,37 @@ function md5(input) {
|
|
|
133
133
|
* console.log(challenge.realm); // "testrealm@host.com"
|
|
134
134
|
* ```
|
|
135
135
|
*/
|
|
136
|
+
/** Decode a hex digest string into its raw bytes. */
|
|
137
|
+
function hexToBytes(hex) {
|
|
138
|
+
const out = new Uint8Array(hex.length / 2);
|
|
139
|
+
for (let i = 0; i < out.length; i++) {
|
|
140
|
+
out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** Decode `quoted-pair` escapes (RFC 7230 §3.2.6) in a quoted-string body. */
|
|
145
|
+
function unescapeQuoted(value) {
|
|
146
|
+
return value.replace(/\\(.)/g, "$1");
|
|
147
|
+
}
|
|
136
148
|
export function parseDigestChallenge(header) {
|
|
137
149
|
const challenge = {
|
|
138
150
|
algorithm: "MD5",
|
|
139
151
|
};
|
|
140
152
|
const cleaned = header.replace(/^Digest\s+/i, "").trim();
|
|
141
|
-
|
|
153
|
+
// A quoted-string may contain a `quoted-pair` (RFC 7230 §3.2.6): `"` and
|
|
154
|
+
// `\\` are literal characters, not delimiters. The previous `"([^"]*)"`
|
|
155
|
+
// stopped at the first quote regardless of a preceding backslash, so
|
|
156
|
+
// `realm="a\"b"` parsed as `a\` and left `b"` to be re-scanned as if it
|
|
157
|
+
// were another parameter — a realm containing a quote silently truncated
|
|
158
|
+
// the challenge, and `realm="x\", qop="auth-int"` did the same to `qop`.
|
|
159
|
+
// Round-tripping a header produced by `formatDigestAuth` was therefore
|
|
160
|
+
// lossy for exactly the values that need escaping.
|
|
161
|
+
const quotedString = '"((?:[^"\\\\]|\\\\.)*)"';
|
|
162
|
+
const regex = new RegExp(`(\\w+)\\s*=\\s*(?:${quotedString}|([^,\\s]+))`, "g");
|
|
142
163
|
let match;
|
|
143
164
|
while ((match = regex.exec(cleaned)) !== null) {
|
|
144
165
|
const key = match[1].toLowerCase();
|
|
145
|
-
const value = (match[2]
|
|
166
|
+
const value = match[2] !== undefined ? unescapeQuoted(match[2]) : match[3];
|
|
146
167
|
switch (key) {
|
|
147
168
|
case "realm":
|
|
148
169
|
challenge.realm = value;
|
|
@@ -428,7 +449,11 @@ export async function computeDigestResponse(challenge, username, password, metho
|
|
|
428
449
|
const realm = challenge.realm;
|
|
429
450
|
const _cnonce = cnonce || randomBytes(5);
|
|
430
451
|
const _nc = nc || "00000001";
|
|
431
|
-
const
|
|
452
|
+
const resolved = resolveHashAlgo(challenge.algorithm);
|
|
453
|
+
if (resolved === null) {
|
|
454
|
+
throw new Error(`Unsupported digest algorithm: ${challenge.algorithm}`);
|
|
455
|
+
}
|
|
456
|
+
const { algo, sess } = resolved;
|
|
432
457
|
async function hash(s) {
|
|
433
458
|
if (algo === "MD5")
|
|
434
459
|
return md5(s);
|
|
@@ -436,12 +461,44 @@ export async function computeDigestResponse(challenge, username, password, metho
|
|
|
436
461
|
return sha512256Hex(s);
|
|
437
462
|
return await sha256Hex(s);
|
|
438
463
|
}
|
|
439
|
-
|
|
464
|
+
// `-sess` binds the session (RFC 7616 §3.4.4/§3.4.5): the client nonce and
|
|
465
|
+
// the server nonce are folded into HA1, so the same credentials cannot be
|
|
466
|
+
// replayed under a different nonce. It requires `qop` to be present,
|
|
467
|
+
// because without one there is no cnonce to fold in.
|
|
468
|
+
// With `userhash=true`, RFC 7616 §3.4.4 substitutes the hashed username
|
|
469
|
+
// for the plaintext one everywhere it appears — including inside HA1.
|
|
470
|
+
const user = challenge.userhash
|
|
471
|
+
? uint8ArrayToBase64(hexToBytes(await hash(`${username}:${realm}`)))
|
|
472
|
+
: username;
|
|
473
|
+
let ha1 = await hash(`${user}:${realm}:${password}`);
|
|
474
|
+
if (sess) {
|
|
475
|
+
if (!challenge.qop) {
|
|
476
|
+
throw new Error(`Digest algorithm ${challenge.algorithm} requires a qop parameter`);
|
|
477
|
+
}
|
|
478
|
+
ha1 = await hash(`${ha1}:${nonce}:${_cnonce}`);
|
|
479
|
+
}
|
|
440
480
|
const ha2 = await hash(`${method}:${uri}`);
|
|
441
481
|
let response;
|
|
442
482
|
const qopList = qop.split(/\s*,\s*/).filter(Boolean);
|
|
443
|
-
if (qopList.
|
|
444
|
-
|
|
483
|
+
if (qopList.length > 0) {
|
|
484
|
+
// RFC 7616 §3.4.3: with `qop=auth-int`, HA2 is
|
|
485
|
+
// H(method:uri:H(entity-body))
|
|
486
|
+
// and the entity-body hash is part of the response. This API is not given
|
|
487
|
+
// the body, so that hash cannot be produced — and the old code took
|
|
488
|
+
// `qopList[0]` and used it in the response while still hashing
|
|
489
|
+
// H(method:uri). A server offering `qop="auth-int,auth"`, which RFC 7616
|
|
490
|
+
// §3.4.4 explicitly says a client may see, was therefore answered with a
|
|
491
|
+
// `qop=auth-int` header carrying a response computed as if it were `auth`:
|
|
492
|
+
// a client that claims a protection it does not implement, and fails
|
|
493
|
+
// against every conforming server.
|
|
494
|
+
//
|
|
495
|
+
// So: prefer `auth`, which is the only one implementable here, and refuse
|
|
496
|
+
// rather than answer wrongly when only `auth-int` is offered.
|
|
497
|
+
if (!qopList.includes("auth")) {
|
|
498
|
+
throw new Error(`Digest challenge offers only unsupported qop values: ${qopList.join(", ")}. ` +
|
|
499
|
+
`"auth-int" requires the entity body, which this API does not receive.`);
|
|
500
|
+
}
|
|
501
|
+
response = await hash(`${ha1}:${nonce}:${_nc}:${_cnonce}:auth:${ha2}`);
|
|
445
502
|
}
|
|
446
503
|
else {
|
|
447
504
|
response = await hash(`${ha1}:${nonce}:${ha2}`);
|
|
@@ -455,13 +512,64 @@ export async function computeDigestResponse(challenge, username, password, metho
|
|
|
455
512
|
* @param algorithm - Algorithm string from the challenge (e.g. "MD5", "SHA-256")
|
|
456
513
|
* @returns The resolved algorithm identifier
|
|
457
514
|
*/
|
|
515
|
+
/**
|
|
516
|
+
* Compute the `username*` value for a challenge offering `userhash=true`
|
|
517
|
+
* (RFC 7616 §3.4.4): `base64(H(username : realm))`.
|
|
518
|
+
*
|
|
519
|
+
* A server that sends `userhash=true` is telling the client *not* to send the
|
|
520
|
+
* username in the clear. The previous code parsed the flag, exposed it on the
|
|
521
|
+
* public {@link DigestChallenge} type with a JSDoc promising support, and then
|
|
522
|
+
* ignored it — sending a plain `username="admin"` back to a server that had
|
|
523
|
+
* explicitly asked for it to be hashed. That is a privacy regression, and the
|
|
524
|
+
* server rejects the request too, because HA1 was built from the unhashed name.
|
|
525
|
+
*
|
|
526
|
+
* @returns the base64 hashed username, or `null` when `userhash` is not offered
|
|
527
|
+
*/
|
|
528
|
+
export async function computeUsernameStar(challenge, username) {
|
|
529
|
+
if (!challenge.userhash)
|
|
530
|
+
return null;
|
|
531
|
+
const resolved = resolveHashAlgo(challenge.algorithm);
|
|
532
|
+
if (resolved === null) {
|
|
533
|
+
throw new Error(`Unsupported digest algorithm: ${challenge.algorithm}`);
|
|
534
|
+
}
|
|
535
|
+
const { algo } = resolved;
|
|
536
|
+
const digest = algo === "MD5"
|
|
537
|
+
? md5(`${username}:${challenge.realm}`)
|
|
538
|
+
: algo === "SHA-512-256"
|
|
539
|
+
? sha512256Hex(`${username}:${challenge.realm}`)
|
|
540
|
+
: await sha256Hex(`${username}:${challenge.realm}`);
|
|
541
|
+
return uint8ArrayToBase64(hexToBytes(digest));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Resolve the challenge's `algorithm` field to a base hash plus the
|
|
545
|
+
* `-sess` session-binding flag (RFC 7616 §3.4.4/§3.4.5).
|
|
546
|
+
*
|
|
547
|
+
* Two separate defects lived here. First, "anything unrecognised is MD5" was
|
|
548
|
+
* a silent downgrade: `SCRAM-SHA-256` and `garbage` were answered with a
|
|
549
|
+
* 32-hex MD5 under a header naming a different algorithm, so the server's
|
|
550
|
+
* rejection blamed an algorithm the caller never chose. Second, the
|
|
551
|
+
* `-sess` forms stripped to `MD5SESS`/`SHA256SESS`, matched nothing, and then
|
|
552
|
+
* fell through the `hash` dispatch to its SHA-256 default — so `MD5-sess`
|
|
553
|
+
* was computed with SHA-256, and `SHA-256-sess` happened to work only by
|
|
554
|
+
* accident. An unrecognised algorithm is now an error.
|
|
555
|
+
*
|
|
556
|
+
* @returns the base algorithm and whether HA1 is session-bound, or `null`
|
|
557
|
+
* when the algorithm is not one we can compute
|
|
558
|
+
*/
|
|
458
559
|
function resolveHashAlgo(algorithm) {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
560
|
+
const trimmed = algorithm.trim();
|
|
561
|
+
const sess = /-sess$/i.test(trimmed);
|
|
562
|
+
const base = trimmed
|
|
563
|
+
.replace(/-sess$/i, "")
|
|
564
|
+
.toUpperCase()
|
|
565
|
+
.replace(/-/g, "");
|
|
566
|
+
if (base === "MD5")
|
|
567
|
+
return { algo: "MD5", sess };
|
|
568
|
+
if (base === "SHA256")
|
|
569
|
+
return { algo: "SHA-256", sess };
|
|
570
|
+
if (base === "SHA512256")
|
|
571
|
+
return { algo: "SHA-512-256", sess };
|
|
572
|
+
return null;
|
|
465
573
|
}
|
|
466
574
|
// ============================================================================
|
|
467
575
|
// §5 DIGEST AUTHORIZATION HEADER FORMATTING
|
|
@@ -491,23 +599,49 @@ function resolveHashAlgo(algorithm) {
|
|
|
491
599
|
* "/dir/index.html", "f2/wE", "00000001");
|
|
492
600
|
* ```
|
|
493
601
|
*/
|
|
494
|
-
|
|
602
|
+
/**
|
|
603
|
+
* Escape a value for a `quoted-string` (RFC 7230 §3.2.6): backslash and double
|
|
604
|
+
* quote are escaped, everything else passes through.
|
|
605
|
+
*
|
|
606
|
+
* A username or URI containing `"` previously terminated the quoted string
|
|
607
|
+
* early and everything after it was parsed as further auth-params — a
|
|
608
|
+
* username of `evil", qop=auth, x="` produced a header carrying an attacker-
|
|
609
|
+
* chosen `qop` and an extra parameter. `realm` and `opaque` come from the
|
|
610
|
+
* *server*, so a hostile or compromised one could inject into the client's own
|
|
611
|
+
* outgoing Authorization header.
|
|
612
|
+
*/
|
|
613
|
+
function quoted(value) {
|
|
614
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
615
|
+
}
|
|
616
|
+
export function formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar) {
|
|
495
617
|
const parts = [];
|
|
496
618
|
const _cnonce = cnonce || randomBytes(5);
|
|
497
619
|
const _nc = nc || "00000001";
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
620
|
+
if (challenge.userhash) {
|
|
621
|
+
if (usernameStar === undefined) {
|
|
622
|
+
throw new Error("challenge offers userhash=true: pass the value from computeUsernameStar() " +
|
|
623
|
+
"as `usernameStar` — sending a plaintext username would defeat it");
|
|
624
|
+
}
|
|
625
|
+
parts.push(`username*="${quoted(usernameStar)}"`);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
if (usernameStar !== undefined) {
|
|
629
|
+
throw new Error("usernameStar was given but the challenge does not offer userhash");
|
|
630
|
+
}
|
|
631
|
+
parts.push(`username="${quoted(username)}"`);
|
|
632
|
+
}
|
|
633
|
+
parts.push(`realm="${quoted(challenge.realm)}"`);
|
|
634
|
+
parts.push(`nonce="${quoted(challenge.nonce)}"`);
|
|
635
|
+
parts.push(`uri="${quoted(uri)}"`);
|
|
636
|
+
parts.push(`response="${quoted(response)}"`);
|
|
503
637
|
if (challenge.opaque)
|
|
504
|
-
parts.push(`opaque="${challenge.opaque}"`);
|
|
638
|
+
parts.push(`opaque="${quoted(challenge.opaque)}"`);
|
|
505
639
|
if (challenge.algorithm && challenge.algorithm !== "MD5")
|
|
506
640
|
parts.push(`algorithm=${challenge.algorithm}`);
|
|
507
641
|
if (challenge.qop) {
|
|
508
|
-
parts.push(`qop
|
|
642
|
+
parts.push(`qop=auth`);
|
|
509
643
|
parts.push(`nc=${_nc}`);
|
|
510
|
-
parts.push(`cnonce="${_cnonce}"`);
|
|
644
|
+
parts.push(`cnonce="${quoted(_cnonce)}"`);
|
|
511
645
|
}
|
|
512
646
|
return `Digest ${parts.join(", ")}`;
|
|
513
647
|
}
|
|
@@ -548,7 +682,8 @@ export async function createDigestAuthorization(wwwAuth, username, password, met
|
|
|
548
682
|
const cnonce = randomBytes(5);
|
|
549
683
|
const nc = "00000001";
|
|
550
684
|
const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
|
|
551
|
-
|
|
685
|
+
const usernameStar = await computeUsernameStar(challenge, username);
|
|
686
|
+
return formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar ?? undefined);
|
|
552
687
|
}
|
|
553
688
|
/**
|
|
554
689
|
* Stateful Digest-auth authorizer.
|
|
@@ -573,6 +708,7 @@ export function createDigestAuthorizer() {
|
|
|
573
708
|
counters.set(scope, next);
|
|
574
709
|
const nc = next.toString(16).padStart(8, "0");
|
|
575
710
|
const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
|
|
576
|
-
|
|
711
|
+
const usernameStar = await computeUsernameStar(challenge, username);
|
|
712
|
+
return formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar ?? undefined);
|
|
577
713
|
};
|
|
578
714
|
}
|