tunnelfetch 1.0.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/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- package/types/warmup.d.ts +45 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// Byte plumbing shared by every layer.
|
|
2
|
+
//
|
|
3
|
+
// The one invariant that matters: nothing here may depend on how the underlying stream happens
|
|
4
|
+
// to chunk its data. A parser built on ByteReader must produce identical results when fed one
|
|
5
|
+
// byte at a time and when fed the whole message at once — the offline suite asserts exactly that,
|
|
6
|
+
// because network fragmentation is adversarial and untestable if the parser can see it.
|
|
7
|
+
|
|
8
|
+
import { TunnelFetchError, LimitError, codes } from '../errors.js';
|
|
9
|
+
|
|
10
|
+
const EMPTY = new Uint8Array(0);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* View size for BYOB pulls. A BYOB read resolves as soon as at least one byte is available —
|
|
14
|
+
* measured on the target runtime's sockets filling anywhere from 19 bytes to the full view —
|
|
15
|
+
* so a large view never delays delivery; it only lets bytes the transport has already buffered
|
|
16
|
+
* arrive in one crossing instead of many.
|
|
17
|
+
*/
|
|
18
|
+
const BYOB_PULL_BYTES = 65536;
|
|
19
|
+
|
|
20
|
+
/** Raised when the peer stops sending in the middle of a structure we must read whole. */
|
|
21
|
+
export class UnexpectedEofError extends TunnelFetchError {
|
|
22
|
+
/**
|
|
23
|
+
* @param {number} wanted bytes the structure needed (-1 when scanning for a delimiter)
|
|
24
|
+
* @param {number} got bytes actually buffered when the stream ended
|
|
25
|
+
* @param {string} what the structure being read, named in the message
|
|
26
|
+
*/
|
|
27
|
+
constructor(wanted, got, what) {
|
|
28
|
+
super('UNEXPECTED_EOF', `stream ended after ${got} of ${wanted} bytes while reading ${what}`, {
|
|
29
|
+
wanted,
|
|
30
|
+
got,
|
|
31
|
+
what,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Buffered reader over a ReadableStream<Uint8Array>.
|
|
38
|
+
*
|
|
39
|
+
* Returned slices may alias the stream's own chunks (or, on the BYOB path, buffers this class
|
|
40
|
+
* allocated and will never touch again); they are never written to by this class and must not
|
|
41
|
+
* be retained beyond the caller's immediate use if memory matters.
|
|
42
|
+
*
|
|
43
|
+
* When the source is a byte stream — on the target runtime, a socket's readable is one — the
|
|
44
|
+
* reader pulls with BYOB reads into large fresh views instead of taking the source's own
|
|
45
|
+
* chunking. This is measured, not stylistic: the runtime delivers socket data in chunks of at
|
|
46
|
+
* most 4096 bytes, ~1200 of them for a 4 MB body, and every chunk is a runtime/JS boundary
|
|
47
|
+
* crossing; a BYOB read hands over everything the transport has buffered (up to the view size)
|
|
48
|
+
* in one crossing, and resolves with a partial fill the instant anything at all is available,
|
|
49
|
+
* so delivery latency is unchanged. Sources that are not byte streams (every in-process
|
|
50
|
+
* ReadableStream in this package and its tests) take the default-reader path unchanged.
|
|
51
|
+
*/
|
|
52
|
+
export class ByteReader {
|
|
53
|
+
/** @param {ReadableStream<Uint8Array>} readable */
|
|
54
|
+
constructor(readable) {
|
|
55
|
+
/** @type {ReadableStreamBYOBReader | null} */
|
|
56
|
+
this._byob = null;
|
|
57
|
+
try {
|
|
58
|
+
this._byob = /** @type {any} */ (readable).getReader({ mode: 'byob' });
|
|
59
|
+
this._reader = this._byob;
|
|
60
|
+
} catch {
|
|
61
|
+
this._reader = readable.getReader();
|
|
62
|
+
}
|
|
63
|
+
/** @type {Uint8Array[]} */
|
|
64
|
+
this._chunks = [];
|
|
65
|
+
this._head = 0; // offset into _chunks[0]
|
|
66
|
+
this._len = 0; // total buffered bytes
|
|
67
|
+
this._eof = false;
|
|
68
|
+
this._done = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get buffered() {
|
|
72
|
+
return this._len;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get atEof() {
|
|
76
|
+
return this._eof && this._len === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Push bytes back to the front. Used when a layer over-reads (e.g. proxy replies with data).
|
|
81
|
+
* @param {Uint8Array} bytes
|
|
82
|
+
*/
|
|
83
|
+
unshift(bytes) {
|
|
84
|
+
if (bytes.byteLength === 0) return;
|
|
85
|
+
if (this._head > 0) {
|
|
86
|
+
this._chunks[0] = this._chunks[0].subarray(this._head);
|
|
87
|
+
this._head = 0;
|
|
88
|
+
}
|
|
89
|
+
this._chunks.unshift(bytes);
|
|
90
|
+
this._len += bytes.byteLength;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pull one more chunk from the source. Returns false at EOF.
|
|
95
|
+
* The BYOB view is freshly allocated per read and never reused: _take hands out subarrays of
|
|
96
|
+
* buffered chunks, so recycling a view would rewrite bytes a parser already holds.
|
|
97
|
+
* @returns {Promise<boolean>} annotated because the tail-recursive skip of empty chunks
|
|
98
|
+
* defeats return-type inference
|
|
99
|
+
*/
|
|
100
|
+
async _pull() {
|
|
101
|
+
if (this._eof) return false;
|
|
102
|
+
const { value, done } = this._byob
|
|
103
|
+
? await this._byob.read(new Uint8Array(BYOB_PULL_BYTES))
|
|
104
|
+
: await this._reader.read();
|
|
105
|
+
if (done) {
|
|
106
|
+
this._eof = true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (value && value.byteLength > 0) {
|
|
110
|
+
this._chunks.push(value);
|
|
111
|
+
this._len += value.byteLength;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
// A zero-length chunk is legal and carries no data; keep pulling.
|
|
115
|
+
return this._pull();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Take exactly n bytes from the buffer. Caller guarantees _len >= n.
|
|
120
|
+
* @param {number} n
|
|
121
|
+
* @returns {Uint8Array}
|
|
122
|
+
*/
|
|
123
|
+
_take(n) {
|
|
124
|
+
if (n === 0) return EMPTY;
|
|
125
|
+
const first = this._chunks[0];
|
|
126
|
+
const avail = first.byteLength - this._head;
|
|
127
|
+
if (avail >= n) {
|
|
128
|
+
const out = first.subarray(this._head, this._head + n);
|
|
129
|
+
this._head += n;
|
|
130
|
+
this._len -= n;
|
|
131
|
+
if (this._head === first.byteLength) {
|
|
132
|
+
this._chunks.shift();
|
|
133
|
+
this._head = 0;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
const out = new Uint8Array(n);
|
|
138
|
+
let o = 0;
|
|
139
|
+
while (o < n) {
|
|
140
|
+
const c = this._chunks[0];
|
|
141
|
+
const from = this._head;
|
|
142
|
+
const take = Math.min(c.byteLength - from, n - o);
|
|
143
|
+
out.set(c.subarray(from, from + take), o);
|
|
144
|
+
o += take;
|
|
145
|
+
this._head += take;
|
|
146
|
+
this._len -= take;
|
|
147
|
+
if (this._head === c.byteLength) {
|
|
148
|
+
this._chunks.shift();
|
|
149
|
+
this._head = 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read exactly n bytes, or throw. This is the workhorse for length-prefixed formats
|
|
157
|
+
* (TLS records, chunked bodies, SOCKS5 replies).
|
|
158
|
+
* @param {number} n
|
|
159
|
+
* @param {string} [what] described in the error if the stream ends early
|
|
160
|
+
* @returns {Promise<Uint8Array>}
|
|
161
|
+
*/
|
|
162
|
+
async readExactly(n, what = 'data') {
|
|
163
|
+
if (n < 0 || !Number.isSafeInteger(n)) {
|
|
164
|
+
throw new TunnelFetchError(codes.CONFIG_INVALID, `readExactly(${n}) is not a valid length`);
|
|
165
|
+
}
|
|
166
|
+
while (this._len < n) {
|
|
167
|
+
if (!(await this._pull())) throw new UnexpectedEofError(n, this._len, what);
|
|
168
|
+
}
|
|
169
|
+
return this._take(n);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Read at least 1 and at most n bytes. Returns null at clean EOF.
|
|
174
|
+
* @param {number} [n]
|
|
175
|
+
* @returns {Promise<Uint8Array | null>}
|
|
176
|
+
*/
|
|
177
|
+
async readSome(n = 65536) {
|
|
178
|
+
while (this._len === 0) {
|
|
179
|
+
if (!(await this._pull())) return null;
|
|
180
|
+
}
|
|
181
|
+
return this._take(Math.min(n, this._len));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Read until `needle` is found, returning everything up to and including it.
|
|
186
|
+
* Used for CRLF-delimited HTTP structures. Fails closed past `maxBytes` so a peer cannot
|
|
187
|
+
* make us buffer without bound.
|
|
188
|
+
* @param {Uint8Array} needle
|
|
189
|
+
* @param {number} maxBytes
|
|
190
|
+
* @param {string} [what]
|
|
191
|
+
* @returns {Promise<Uint8Array>}
|
|
192
|
+
*/
|
|
193
|
+
async readUntil(needle, maxBytes, what = 'delimited data') {
|
|
194
|
+
const nl = needle.byteLength;
|
|
195
|
+
if (nl === 0) throw new TunnelFetchError(codes.CONFIG_INVALID, 'readUntil needs a needle');
|
|
196
|
+
let scanned = 0;
|
|
197
|
+
for (;;) {
|
|
198
|
+
// Compact so the scan is over one contiguous view. Delimited structures are small
|
|
199
|
+
// (status lines, header blocks, chunk sizes) and bounded by maxBytes.
|
|
200
|
+
if (this._chunks.length > 1 || this._head > 0) {
|
|
201
|
+
const all = this._take(this._len);
|
|
202
|
+
this._chunks = all.byteLength ? [all] : [];
|
|
203
|
+
this._head = 0;
|
|
204
|
+
this._len = all.byteLength;
|
|
205
|
+
}
|
|
206
|
+
const buf = this._len ? this._chunks[0] : EMPTY;
|
|
207
|
+
const from = Math.max(0, scanned - nl + 1);
|
|
208
|
+
const idx = indexOf(buf, needle, from);
|
|
209
|
+
if (idx >= 0) {
|
|
210
|
+
const end = idx + nl;
|
|
211
|
+
if (end > maxBytes) {
|
|
212
|
+
throw new LimitError(
|
|
213
|
+
codes.LIMIT_HEADER,
|
|
214
|
+
`${what} is ${end} bytes, over the ${maxBytes} byte limit`,
|
|
215
|
+
{ bytes: end, limit: maxBytes },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return this._take(end);
|
|
219
|
+
}
|
|
220
|
+
scanned = buf.byteLength;
|
|
221
|
+
if (scanned > maxBytes) {
|
|
222
|
+
throw new LimitError(
|
|
223
|
+
codes.LIMIT_HEADER,
|
|
224
|
+
`${what} exceeded ${maxBytes} bytes with no delimiter found`,
|
|
225
|
+
{ bytes: scanned, limit: maxBytes },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (!(await this._pull())) throw new UnexpectedEofError(-1, scanned, what);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Drain everything remaining, up to maxBytes.
|
|
234
|
+
* @param {number} [maxBytes]
|
|
235
|
+
* @returns {Promise<Uint8Array>}
|
|
236
|
+
*/
|
|
237
|
+
async readToEnd(maxBytes = Infinity) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
let total = 0;
|
|
240
|
+
for (;;) {
|
|
241
|
+
const c = await this.readSome();
|
|
242
|
+
if (c === null) break;
|
|
243
|
+
total += c.byteLength;
|
|
244
|
+
if (total > maxBytes) {
|
|
245
|
+
throw new LimitError(codes.LIMIT_BODY, `body exceeded ${maxBytes} bytes`, {
|
|
246
|
+
limit: maxBytes,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
parts.push(c);
|
|
250
|
+
}
|
|
251
|
+
return concat(parts, total);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
releaseLock() {
|
|
255
|
+
if (this._done) return;
|
|
256
|
+
this._done = true;
|
|
257
|
+
try {
|
|
258
|
+
this._reader.releaseLock();
|
|
259
|
+
} catch {
|
|
260
|
+
/* already errored or locked elsewhere; nothing useful to do */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @param {unknown} [reason] */
|
|
265
|
+
async cancel(reason) {
|
|
266
|
+
if (this._done) return;
|
|
267
|
+
this._done = true;
|
|
268
|
+
try {
|
|
269
|
+
await this._reader.cancel(reason);
|
|
270
|
+
} catch {
|
|
271
|
+
/* the stream may already be errored */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Buffered writer over a WritableStream<Uint8Array>. */
|
|
277
|
+
export class ByteWriter {
|
|
278
|
+
/** @param {WritableStream<Uint8Array>} writable */
|
|
279
|
+
constructor(writable) {
|
|
280
|
+
this._writer = writable.getWriter();
|
|
281
|
+
this._done = false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {Uint8Array} bytes
|
|
286
|
+
* @returns {Promise<void>}
|
|
287
|
+
*/
|
|
288
|
+
write(bytes) {
|
|
289
|
+
return this._writer.write(bytes);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Write several buffers as one, avoiding per-piece stream overhead.
|
|
294
|
+
* @param {Uint8Array[]} parts
|
|
295
|
+
* @returns {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
writeAll(parts) {
|
|
298
|
+
let total = 0;
|
|
299
|
+
for (const p of parts) total += p.byteLength;
|
|
300
|
+
return this._writer.write(concat(parts, total));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
releaseLock() {
|
|
304
|
+
if (this._done) return;
|
|
305
|
+
this._done = true;
|
|
306
|
+
try {
|
|
307
|
+
this._writer.releaseLock();
|
|
308
|
+
} catch {
|
|
309
|
+
/* already released */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async close() {
|
|
314
|
+
if (this._done) return;
|
|
315
|
+
this._done = true;
|
|
316
|
+
try {
|
|
317
|
+
await this._writer.close();
|
|
318
|
+
} catch {
|
|
319
|
+
/* peer may have closed first */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {unknown} [reason] */
|
|
324
|
+
async abort(reason) {
|
|
325
|
+
if (this._done) return;
|
|
326
|
+
this._done = true;
|
|
327
|
+
try {
|
|
328
|
+
await this._writer.abort(reason);
|
|
329
|
+
} catch {
|
|
330
|
+
/* already errored */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ------------------------------------------------------------------ pure helpers
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {Uint8Array[]} parts
|
|
339
|
+
* @param {number} [total] byte total, when the caller already counted; computed otherwise
|
|
340
|
+
* @returns {Uint8Array}
|
|
341
|
+
*/
|
|
342
|
+
export function concat(parts, total) {
|
|
343
|
+
if (total === undefined) {
|
|
344
|
+
total = 0;
|
|
345
|
+
for (const p of parts) total += p.byteLength;
|
|
346
|
+
}
|
|
347
|
+
if (parts.length === 1 && parts[0].byteLength === total) return parts[0];
|
|
348
|
+
const out = new Uint8Array(total);
|
|
349
|
+
let o = 0;
|
|
350
|
+
for (const p of parts) {
|
|
351
|
+
out.set(p, o);
|
|
352
|
+
o += p.byteLength;
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Boyer-Moore is not worth it for 1-4 byte needles over small buffers.
|
|
359
|
+
* @param {Uint8Array} haystack
|
|
360
|
+
* @param {Uint8Array} needle
|
|
361
|
+
* @param {number} [from]
|
|
362
|
+
* @returns {number} index of the first occurrence at or after `from`, or -1
|
|
363
|
+
*/
|
|
364
|
+
export function indexOf(haystack, needle, from = 0) {
|
|
365
|
+
const n = needle.byteLength;
|
|
366
|
+
const limit = haystack.byteLength - n;
|
|
367
|
+
const first = needle[0];
|
|
368
|
+
outer: for (let i = Math.max(0, from); i <= limit; i++) {
|
|
369
|
+
if (haystack[i] !== first) continue;
|
|
370
|
+
for (let j = 1; j < n; j++) {
|
|
371
|
+
if (haystack[i + j] !== needle[j]) continue outer;
|
|
372
|
+
}
|
|
373
|
+
return i;
|
|
374
|
+
}
|
|
375
|
+
return -1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {Uint8Array} a
|
|
380
|
+
* @param {Uint8Array} b
|
|
381
|
+
* @returns {boolean}
|
|
382
|
+
*/
|
|
383
|
+
export function equal(a, b) {
|
|
384
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
385
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Comparison whose running time does not depend on where the first difference is.
|
|
391
|
+
* JS cannot truly guarantee constant time (JIT tiering, GC), which is precisely why this package
|
|
392
|
+
* refuses MAC-then-encrypt cipher suites. It is used only where a timing leak would be a
|
|
393
|
+
* nice-to-have for an attacker rather than a decryption oracle: certificate pins and Finished
|
|
394
|
+
* verification, where the compared value is already authenticated or public.
|
|
395
|
+
* @param {Uint8Array} a
|
|
396
|
+
* @param {Uint8Array} b
|
|
397
|
+
* @returns {boolean}
|
|
398
|
+
*/
|
|
399
|
+
export function timingSafeEqual(a, b) {
|
|
400
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
401
|
+
let diff = 0;
|
|
402
|
+
for (let i = 0; i < a.byteLength; i++) diff |= a[i] ^ b[i];
|
|
403
|
+
return diff === 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const HEX = '0123456789abcdef';
|
|
407
|
+
/**
|
|
408
|
+
* @param {Uint8Array} bytes
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
export function toHex(bytes) {
|
|
412
|
+
let s = '';
|
|
413
|
+
for (const b of bytes) s += HEX[b >> 4] + HEX[b & 15];
|
|
414
|
+
return s;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {string} hex whitespace and ':' separators tolerated
|
|
419
|
+
* @returns {Uint8Array}
|
|
420
|
+
*/
|
|
421
|
+
export function fromHex(hex) {
|
|
422
|
+
const clean = hex.replace(/[\s:]/g, '');
|
|
423
|
+
if (clean.length % 2) throw new TunnelFetchError(codes.CONFIG_INVALID, 'odd-length hex string');
|
|
424
|
+
const out = new Uint8Array(clean.length / 2);
|
|
425
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const encoder = new TextEncoder();
|
|
430
|
+
/** @type {(s: string) => Uint8Array} */
|
|
431
|
+
export const utf8 = (s) => encoder.encode(s);
|
|
432
|
+
/**
|
|
433
|
+
* Latin-1 decode: HTTP header field values are opaque octets, not UTF-8.
|
|
434
|
+
* @param {Uint8Array} bytes
|
|
435
|
+
* @returns {string}
|
|
436
|
+
*/
|
|
437
|
+
export function latin1(bytes) {
|
|
438
|
+
let s = '';
|
|
439
|
+
for (let i = 0; i < bytes.byteLength; i++) s += String.fromCharCode(bytes[i]);
|
|
440
|
+
return s;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Big-endian integer writers, the byte order of every protocol in this package.
|
|
445
|
+
* @param {number} n
|
|
446
|
+
* @returns {Uint8Array}
|
|
447
|
+
*/
|
|
448
|
+
export function u8(n) {
|
|
449
|
+
return new Uint8Array([n & 0xff]);
|
|
450
|
+
}
|
|
451
|
+
/** @param {number} n @returns {Uint8Array} */
|
|
452
|
+
export function u16(n) {
|
|
453
|
+
return new Uint8Array([(n >>> 8) & 0xff, n & 0xff]);
|
|
454
|
+
}
|
|
455
|
+
/** @param {number} n @returns {Uint8Array} */
|
|
456
|
+
export function u24(n) {
|
|
457
|
+
return new Uint8Array([(n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
|
458
|
+
}
|
|
459
|
+
/** @param {number} n @returns {Uint8Array} */
|
|
460
|
+
export function u32(n) {
|
|
461
|
+
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
|
462
|
+
}
|
|
463
|
+
/** Big-endian integer readers over a byte view at offset `o`. */
|
|
464
|
+
/** @type {(b: Uint8Array, o?: number) => number} */
|
|
465
|
+
export const readU16 = (b, o = 0) => (b[o] << 8) | b[o + 1];
|
|
466
|
+
/** @type {(b: Uint8Array, o?: number) => number} */
|
|
467
|
+
export const readU24 = (b, o = 0) => (b[o] << 16) | (b[o + 1] << 8) | b[o + 2];
|
|
468
|
+
/** @type {(b: Uint8Array, o?: number) => number} */
|
|
469
|
+
export const readU32 = (b, o = 0) =>
|
|
470
|
+
((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0;
|