tunnelfetch 1.6.4 → 1.7.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/package.json +1 -1
- package/src/client/decode.js +52 -8
- package/src/client.js +3 -0
- package/src/http2/connection.js +9 -2
- package/src/http2/frames.js +34 -4
- package/src/profiles.js +10 -7
- package/src/util/bytes.js +28 -0
- package/types/http2/connection.d.ts +1 -0
- package/types/http2/frames.d.ts +17 -3
- package/types/profiles.d.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A fetch-shaped HTTP client that can route through HTTP CONNECT / HTTPS / SOCKS5 proxies on runtimes with only raw TCP, such as Cloudflare Workers. Implements TLS in userland because the runtime cannot verify a tunnelled peer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
package/src/client/decode.js
CHANGED
|
@@ -190,17 +190,53 @@ function decompressionStage(source, coding, maxBytes = Infinity) {
|
|
|
190
190
|
})();
|
|
191
191
|
ready.catch(() => {});
|
|
192
192
|
|
|
193
|
+
// The fast path: hand the decompressor's output to the runtime and never touch a chunk in JS.
|
|
194
|
+
//
|
|
195
|
+
// Measured on the edge, CPU per MB of decompressed output, all shapes interleaved in one isolate:
|
|
196
|
+
//
|
|
197
|
+
// this wrapper, BYOB reads + counting 7.00
|
|
198
|
+
// the same wrapper with counting removed 7.33 (counting is free; the WRAPPER is not)
|
|
199
|
+
// a standard `new TransformStream()` hop 13.33 (JS-backed: every 4 KiB chunk queues)
|
|
200
|
+
// an IdentityTransformStream hop 3.67 (native, and BYOB on its readable)
|
|
201
|
+
// DecompressionStream + native collect 3.33 (the floor)
|
|
202
|
+
//
|
|
203
|
+
// So a native identity hop is 48% cheaper than this wrapper and lands within noise of the floor,
|
|
204
|
+
// while the standard TransformStream — the obvious way to write the same thing — is nearly twice
|
|
205
|
+
// as expensive as doing nothing at all. The difference is that one is C++ and one is JavaScript;
|
|
206
|
+
// nothing about the API surface says so.
|
|
207
|
+
//
|
|
208
|
+
// It applies only when there is no cap, and that is not a limitation to be worked around: the cap
|
|
209
|
+
// is enforced by counting bytes, counting requires seeing them in JS, and seeing them in JS is
|
|
210
|
+
// exactly the cost being removed. The two are mutually exclusive. `maxBodyBytes: Infinity` is
|
|
211
|
+
// already a caller saying they will bound the body themselves, so giving that caller the fast
|
|
212
|
+
// path is coherent — you gave up the guard, you get the speed — rather than a compromise.
|
|
213
|
+
if (maxBytes === Infinity && typeof globalThis.IdentityTransformStream === 'function') {
|
|
214
|
+
const relay = new globalThis.IdentityTransformStream();
|
|
215
|
+
(async () => {
|
|
216
|
+
const ds = await ready;
|
|
217
|
+
if (ds === null) {
|
|
218
|
+
await relay.writable.close().catch(() => {});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// preventAbort: pipeTo's default is to abort the destination with the SOURCE's error, which
|
|
222
|
+
// would hand the consumer a bare zlib message from a stream it never asked about. Keeping
|
|
223
|
+
// the abort here is what lets the coding be named, the same as on the wrapper path.
|
|
224
|
+
await ds.readable.pipeTo(relay.writable, { preventAbort: true });
|
|
225
|
+
if (pumpDone) await pumpDone;
|
|
226
|
+
await relay.writable.close();
|
|
227
|
+
})().catch(async (e) => {
|
|
228
|
+
// The consumer sees the same typed error it would have seen through the wrapper; the relay is
|
|
229
|
+
// errored rather than closed so a truncated body can never read as a complete one.
|
|
230
|
+
await srcReader.cancel(wrapCoding(coding, e)).catch(() => {});
|
|
231
|
+
await relay.writable.abort(wrapCoding(coding, e)).catch(() => {});
|
|
232
|
+
});
|
|
233
|
+
return relay.readable;
|
|
234
|
+
}
|
|
235
|
+
|
|
193
236
|
/** @type {ReadableStreamBYOBReader | ReadableStreamDefaultReader<Uint8Array> | null} */
|
|
194
237
|
let out = null;
|
|
195
238
|
let byob = false;
|
|
196
|
-
const wrap = (e) =>
|
|
197
|
-
e instanceof HttpError
|
|
198
|
-
? e
|
|
199
|
-
: new HttpError(
|
|
200
|
-
codes.HTTP_CONTENT_ENCODING,
|
|
201
|
-
`decoding "${coding}" failed: ${e?.message ?? e}`,
|
|
202
|
-
{ coding },
|
|
203
|
-
);
|
|
239
|
+
const wrap = (e) => wrapCoding(coding, e);
|
|
204
240
|
|
|
205
241
|
return new ReadableStream({
|
|
206
242
|
async pull(c) {
|
|
@@ -334,6 +370,14 @@ function capDecodedOutput(source, coding, maxBytes) {
|
|
|
334
370
|
});
|
|
335
371
|
}
|
|
336
372
|
|
|
373
|
+
/** Name the coding in a decode failure, so a caller sees which one misbehaved rather than a bare
|
|
374
|
+
* zlib message from somewhere downstream. */
|
|
375
|
+
function wrapCoding(coding, e) {
|
|
376
|
+
return e instanceof HttpError
|
|
377
|
+
? e
|
|
378
|
+
: new HttpError(codes.HTTP_CONTENT_ENCODING, `decoding "${coding}" failed: ${e?.message ?? e}`, { coding });
|
|
379
|
+
}
|
|
380
|
+
|
|
337
381
|
/** Byte at logical offset `i` across the buffered head chunks. */
|
|
338
382
|
function firstBytes(chunks, i) {
|
|
339
383
|
for (const c of chunks) {
|
package/src/client.js
CHANGED
|
@@ -516,6 +516,9 @@ function registerHttp2(client, key, conn) {
|
|
|
516
516
|
...(client.options.http2HpackIndexing
|
|
517
517
|
? { hpackIndexing: client.options.http2HpackIndexing }
|
|
518
518
|
: {}),
|
|
519
|
+
...(client.options.http2HeadersPriority
|
|
520
|
+
? { headersPriority: client.options.http2HeadersPriority }
|
|
521
|
+
: {}),
|
|
519
522
|
onClose: () => {
|
|
520
523
|
client._h2conns.delete(h2);
|
|
521
524
|
// Only drop the keyed entry if it is still this connection; a newer one may have replaced it.
|
package/src/http2/connection.js
CHANGED
|
@@ -266,6 +266,9 @@ export class Http2Connection {
|
|
|
266
266
|
// fields go into the HPACK dynamic table. Both default to curl's, both captured off the wire.
|
|
267
267
|
this._pseudoHeaderOrder = opts.pseudoHeaderOrder ?? null;
|
|
268
268
|
this._hpackIndexing = opts.hpackIndexing ?? null;
|
|
269
|
+
// Whether to set PRIORITY on the request HEADERS, and with what. null means do not set it,
|
|
270
|
+
// which is curl's behaviour and stays the default.
|
|
271
|
+
this._headersPriority = opts.headersPriority ?? null;
|
|
269
272
|
this._expectFirstSettings = true;
|
|
270
273
|
|
|
271
274
|
this._fatal = null; // set once; rejects every stream and every future request
|
|
@@ -449,14 +452,18 @@ export class Http2Connection {
|
|
|
449
452
|
* run is one write, so no other frame can interleave it (RFC 9113 s6.10). */
|
|
450
453
|
_sendHeaderBlock(streamId, block, endStream) {
|
|
451
454
|
const max = this._peerMaxFrameSize;
|
|
455
|
+
// Priority rides the opening HEADERS and nothing else: a CONTINUATION has no flags but
|
|
456
|
+
// END_HEADERS (RFC 9113 s6.10), so putting it anywhere else would be malformed rather than
|
|
457
|
+
// merely wrong-looking.
|
|
458
|
+
const priority = this._headersPriority;
|
|
452
459
|
if (block.length <= max) {
|
|
453
|
-
this._write(headersFrame(streamId, block, { endStream, endHeaders: true }));
|
|
460
|
+
this._write(headersFrame(streamId, block, { endStream, endHeaders: true, priority }));
|
|
454
461
|
return;
|
|
455
462
|
}
|
|
456
463
|
const frames = [];
|
|
457
464
|
let o = 0;
|
|
458
465
|
const first = block.subarray(0, max);
|
|
459
|
-
frames.push(headersFrame(streamId, first, { endStream, endHeaders: false }));
|
|
466
|
+
frames.push(headersFrame(streamId, first, { endStream, endHeaders: false, priority }));
|
|
460
467
|
o = max;
|
|
461
468
|
while (o < block.length) {
|
|
462
469
|
const chunk = block.subarray(o, Math.min(o + max, block.length));
|
package/src/http2/frames.js
CHANGED
|
@@ -152,13 +152,43 @@ export function goawayFrame(lastStreamId, errorCode, debug = EMPTY) {
|
|
|
152
152
|
return serializeFrame(FRAME.GOAWAY, 0, 0, payload);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
*
|
|
157
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A HEADERS frame carrying a full (already-fragmented-if-needed) block. Never PADDED.
|
|
157
|
+
*
|
|
158
|
+
* PRIORITY is emitted only when `priority` is supplied, because whether a client sends it is part
|
|
159
|
+
* of its identity: curl does not, Chromium does — flags 0x25 with `80 00 00 00 ff`, captured in
|
|
160
|
+
* test/tls/_captured-h2.js. RFC 9113 s5.3.2 deprecates the mechanism and this package ignores
|
|
161
|
+
* every PRIORITY frame it receives, but a frame-level fingerprinter reads whether the flag is set,
|
|
162
|
+
* so refusing to emit it would make the Chromium identity wrong in a way nothing else could fix.
|
|
163
|
+
*
|
|
164
|
+
* The five bytes are exclusive (1 bit) + stream dependency (31) + weight (8). The weight is sent
|
|
165
|
+
* as-is: RFC 7540 s6.3 defines the wire byte as weight-minus-one, so Chromium's 255 is a weight of
|
|
166
|
+
* 256, and passing the byte through keeps this function free of an off-by-one nobody would see.
|
|
167
|
+
*
|
|
168
|
+
* @param {{ exclusive?: boolean, streamDependency?: number, weight: number } | null} [opts.priority]
|
|
169
|
+
*/
|
|
170
|
+
export function headersFrame(
|
|
171
|
+
streamId,
|
|
172
|
+
block,
|
|
173
|
+
{ endStream = false, endHeaders = true, priority = null } = {},
|
|
174
|
+
) {
|
|
158
175
|
let flags = 0;
|
|
159
176
|
if (endStream) flags |= FLAG.END_STREAM;
|
|
160
177
|
if (endHeaders) flags |= FLAG.END_HEADERS;
|
|
161
|
-
return serializeFrame(FRAME.HEADERS, flags, streamId, block);
|
|
178
|
+
if (!priority) return serializeFrame(FRAME.HEADERS, flags, streamId, block);
|
|
179
|
+
|
|
180
|
+
flags |= FLAG.PRIORITY;
|
|
181
|
+
const dep = priority.streamDependency ?? 0;
|
|
182
|
+
const head = new Uint8Array(5);
|
|
183
|
+
head[0] = ((dep >>> 24) & 0x7f) | (priority.exclusive ? 0x80 : 0);
|
|
184
|
+
head[1] = (dep >>> 16) & 0xff;
|
|
185
|
+
head[2] = (dep >>> 8) & 0xff;
|
|
186
|
+
head[3] = dep & 0xff;
|
|
187
|
+
head[4] = priority.weight & 0xff;
|
|
188
|
+
const payload = new Uint8Array(5 + block.length);
|
|
189
|
+
payload.set(head, 0);
|
|
190
|
+
payload.set(block, 5);
|
|
191
|
+
return serializeFrame(FRAME.HEADERS, flags, streamId, payload);
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
/** A CONTINUATION frame (RFC 9113 s6.10), for a header block that overflows one frame. */
|
package/src/profiles.js
CHANGED
|
@@ -117,12 +117,15 @@ export const chrome = Object.freeze({
|
|
|
117
117
|
// have been right, which is not a reason to have guessed. It is declared explicitly now so the
|
|
118
118
|
// identity states its own value instead of inheriting one by accident.
|
|
119
119
|
http2HpackIndexing: Object.freeze({ ':path': 'without' }),
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
120
|
+
// Chromium sets PRIORITY on its request HEADERS (flags 0x25) and carries the five deprecated
|
|
121
|
+
// priority bytes: `80 00 00 00 ff`, i.e. exclusive, dependency 0, weight byte 255. Captured, in
|
|
122
|
+
// test/tls/_captured-h2.js. 1.6.2 found the difference and documented it; 1.6.5 emits it.
|
|
123
|
+
//
|
|
124
|
+
// RFC 9113 s5.3.2 deprecates the mechanism and this package still IGNORES every PRIORITY frame it
|
|
125
|
+
// receives — sending it is a statement about identity, not a request to be prioritised, and those
|
|
126
|
+
// are different things. curl sets no priority, so `profiles.curl` leaves this unset and the
|
|
127
|
+
// default stays "do not send".
|
|
128
|
+
http2HeadersPriority: Object.freeze({ exclusive: true, streamDependency: 0, weight: 255 }),
|
|
126
129
|
//
|
|
127
130
|
// The connection window immediately above was DEAD until 1.6.1 — declared here, copied by
|
|
128
131
|
// nothing, passed by nothing, and read under a different name — so every connection using this
|
|
@@ -211,7 +214,7 @@ export function applyProfile(options) {
|
|
|
211
214
|
// was missing, which made it dead config: the chrome profile declared Chromium's ~15 MiB window
|
|
212
215
|
// and every chrome connection sent curl's 1000 MiB one.
|
|
213
216
|
for (const key of ['headerOrder', 'http2Settings', 'http2ConnectionWindow',
|
|
214
|
-
'http2PseudoHeaderOrder', 'http2HpackIndexing']) {
|
|
217
|
+
'http2PseudoHeaderOrder', 'http2HpackIndexing', 'http2HeadersPriority']) {
|
|
215
218
|
if (options[key] === undefined && p[key] != null) out[key] = p[key];
|
|
216
219
|
}
|
|
217
220
|
// Profile headers are DEFAULTS: a request that sets its own User-Agent keeps it. They are folded
|
package/src/util/bytes.js
CHANGED
|
@@ -398,11 +398,39 @@ export function equal(a, b) {
|
|
|
398
398
|
*/
|
|
399
399
|
export function timingSafeEqual(a, b) {
|
|
400
400
|
if (a.byteLength !== b.byteLength) return false;
|
|
401
|
+
// Prefer the runtime's own, which is compiled rather than interpreted and therefore actually has
|
|
402
|
+
// the property this function is named for. `crypto.subtle.timingSafeEqual` is a non-standard
|
|
403
|
+
// Cloudflare extension, so it is FEATURE-DETECTED rather than assumed — this package runs on
|
|
404
|
+
// Node, Deno and Bun as well, and inferring a capability from a runtime name is the mistake it
|
|
405
|
+
// refuses to make everywhere else. Detected once, at module load, because doing it per call would
|
|
406
|
+
// put a property lookup on a path whose whole point is uniform timing.
|
|
407
|
+
if (NATIVE_TIMING_SAFE_EQUAL) return NATIVE_TIMING_SAFE_EQUAL(a, b);
|
|
401
408
|
let diff = 0;
|
|
402
409
|
for (let i = 0; i < a.byteLength; i++) diff |= a[i] ^ b[i];
|
|
403
410
|
return diff === 0;
|
|
404
411
|
}
|
|
405
412
|
|
|
413
|
+
/**
|
|
414
|
+
* The runtime's constant-time compare, bound once, or null where there is none.
|
|
415
|
+
*
|
|
416
|
+
* Bound with a probe rather than a typeof check: a property that exists but throws on real input
|
|
417
|
+
* would otherwise be discovered inside a TLS Finished verification, where the failure mode is a
|
|
418
|
+
* dead connection on a path that is supposed to be the careful one.
|
|
419
|
+
*/
|
|
420
|
+
const NATIVE_TIMING_SAFE_EQUAL = (() => {
|
|
421
|
+
const fn = globalThis.crypto?.subtle?.timingSafeEqual;
|
|
422
|
+
if (typeof fn !== 'function') return null;
|
|
423
|
+
try {
|
|
424
|
+
const one = Uint8Array.of(1, 2, 3);
|
|
425
|
+
const two = Uint8Array.of(1, 2, 4);
|
|
426
|
+
if (fn.call(globalThis.crypto.subtle, one, one) !== true) return null;
|
|
427
|
+
if (fn.call(globalThis.crypto.subtle, one, two) !== false) return null;
|
|
428
|
+
} catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return (a, b) => fn.call(globalThis.crypto.subtle, a, b);
|
|
432
|
+
})();
|
|
433
|
+
|
|
406
434
|
const HEX = '0123456789abcdef';
|
|
407
435
|
/**
|
|
408
436
|
* @param {Uint8Array} bytes
|
|
@@ -79,6 +79,7 @@ export class Http2Connection {
|
|
|
79
79
|
_settingsFlight: Array<[number, number]> | null;
|
|
80
80
|
_pseudoHeaderOrder: string[] | null;
|
|
81
81
|
_hpackIndexing: Record<string, "without" | "incremental" | "never"> | null;
|
|
82
|
+
_headersPriority: any;
|
|
82
83
|
_expectFirstSettings: boolean;
|
|
83
84
|
_fatal: any;
|
|
84
85
|
_goaway: {
|
package/types/http2/frames.d.ts
CHANGED
|
@@ -42,11 +42,25 @@ export function rstStreamFrame(streamId: any, errorCode: any): Uint8Array<ArrayB
|
|
|
42
42
|
export function pingFrame(opaque: any, ack?: boolean): Uint8Array<ArrayBufferLike>;
|
|
43
43
|
/** A GOAWAY frame (RFC 9113 s6.8). */
|
|
44
44
|
export function goawayFrame(lastStreamId: any, errorCode: any, debug?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBufferLike>;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* A HEADERS frame carrying a full (already-fragmented-if-needed) block. Never PADDED.
|
|
47
|
+
*
|
|
48
|
+
* PRIORITY is emitted only when `priority` is supplied, because whether a client sends it is part
|
|
49
|
+
* of its identity: curl does not, Chromium does — flags 0x25 with `80 00 00 00 ff`, captured in
|
|
50
|
+
* test/tls/_captured-h2.js. RFC 9113 s5.3.2 deprecates the mechanism and this package ignores
|
|
51
|
+
* every PRIORITY frame it receives, but a frame-level fingerprinter reads whether the flag is set,
|
|
52
|
+
* so refusing to emit it would make the Chromium identity wrong in a way nothing else could fix.
|
|
53
|
+
*
|
|
54
|
+
* The five bytes are exclusive (1 bit) + stream dependency (31) + weight (8). The weight is sent
|
|
55
|
+
* as-is: RFC 7540 s6.3 defines the wire byte as weight-minus-one, so Chromium's 255 is a weight of
|
|
56
|
+
* 256, and passing the byte through keeps this function free of an off-by-one nobody would see.
|
|
57
|
+
*
|
|
58
|
+
* @param {{ exclusive?: boolean, streamDependency?: number, weight: number } | null} [opts.priority]
|
|
59
|
+
*/
|
|
60
|
+
export function headersFrame(streamId: any, block: any, { endStream, endHeaders, priority }?: {
|
|
48
61
|
endStream?: boolean | undefined;
|
|
49
62
|
endHeaders?: boolean | undefined;
|
|
63
|
+
priority?: null | undefined;
|
|
50
64
|
}): Uint8Array<ArrayBufferLike>;
|
|
51
65
|
/** A CONTINUATION frame (RFC 9113 s6.10), for a header block that overflows one frame. */
|
|
52
66
|
export function continuationFrame(streamId: any, block: any, endHeaders: any): Uint8Array<ArrayBufferLike>;
|
package/types/profiles.d.ts
CHANGED
|
@@ -74,6 +74,11 @@ export const chrome: Readonly<{
|
|
|
74
74
|
http2HpackIndexing: Readonly<{
|
|
75
75
|
':path': "without";
|
|
76
76
|
}>;
|
|
77
|
+
http2HeadersPriority: Readonly<{
|
|
78
|
+
exclusive: true;
|
|
79
|
+
streamDependency: 0;
|
|
80
|
+
weight: 255;
|
|
81
|
+
}>;
|
|
77
82
|
http2PseudoHeaderOrder: readonly string[];
|
|
78
83
|
headerOrder: readonly string[];
|
|
79
84
|
headers: readonly string[][];
|