tunnelfetch 1.6.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunnelfetch",
3
- "version": "1.6.5",
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",
@@ -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/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