tunnelfetch 1.4.0 → 1.4.1
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 +24 -3
- package/src/client.js +4 -2
- package/types/client/decode.d.ts +3 -1
- package/types/client.d.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
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
|
@@ -106,7 +106,12 @@ const DECOMPRESS_READ_BYTES = 65536;
|
|
|
106
106
|
* consumes many input chunks before producing output, so tying input progress to output
|
|
107
107
|
* pulls would deadlock.
|
|
108
108
|
*/
|
|
109
|
-
function decompressionStage(source, coding) {
|
|
109
|
+
function decompressionStage(source, coding, maxBytes = Infinity) {
|
|
110
|
+
// Bytes this stage has produced. `maxBodyBytes` bounded only the COMPRESSED wire body, so a
|
|
111
|
+
// caller asking for at most 1 MB received 20 MB from a 20 KB gzip bomb — the cap was applied to
|
|
112
|
+
// the wrong side of the decompressor. gzip reaches roughly 1000:1, so the gap was not bounded in
|
|
113
|
+
// any useful sense. Counted here, per stage, so a chain like `br, gzip` cannot exceed it either.
|
|
114
|
+
let produced = 0;
|
|
110
115
|
const srcReader = source.getReader();
|
|
111
116
|
/** Rejections here surface through the output stream; pre-observed like chunked.js does. */
|
|
112
117
|
let pumpDone = null;
|
|
@@ -207,6 +212,18 @@ function decompressionStage(source, coding) {
|
|
|
207
212
|
return;
|
|
208
213
|
}
|
|
209
214
|
if (value.byteLength === 0) continue; // legal, carries nothing; keep reading
|
|
215
|
+
produced += value.byteLength;
|
|
216
|
+
if (produced > maxBytes) {
|
|
217
|
+
// Refused BEFORE the over-long chunk is handed on, so the caller never holds more than
|
|
218
|
+
// it asked for. Fail closed: a truncated body delivered as if complete would be worse.
|
|
219
|
+
throw new HttpError(
|
|
220
|
+
codes.LIMIT_BODY,
|
|
221
|
+
`decoded body exceeded maxBodyBytes: ${produced} bytes of "${coding}" output past a ` +
|
|
222
|
+
`${maxBytes} byte cap. The compressed body was within the cap; the decompressed ` +
|
|
223
|
+
'one is what a gzip bomb inflates.',
|
|
224
|
+
{ coding, produced, maxBytes },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
210
227
|
c.enqueue(value);
|
|
211
228
|
return;
|
|
212
229
|
}
|
|
@@ -263,9 +280,11 @@ function firstBytes(chunks, i) {
|
|
|
263
280
|
* comma-separated list names codings in the order the SERVER applied them, so decoding
|
|
264
281
|
* applies them in reverse.
|
|
265
282
|
* @param {Record<string, BodyDecoder> | null} [decoders] caller-supplied codings
|
|
283
|
+
* @param {number} [maxBytes] cap on DECODED output, per stage. `maxBodyBytes` alone bounded the
|
|
284
|
+
* compressed body, which a gzip bomb walks straight past.
|
|
266
285
|
* @returns {ReadableStream<Uint8Array>} decoded bytes
|
|
267
286
|
*/
|
|
268
|
-
export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
287
|
+
export function decodeBody(stream, contentEncoding, decoders = null, maxBytes = Infinity) {
|
|
269
288
|
/** Look a coding up among the caller's decoders, case-insensitively as the header is. */
|
|
270
289
|
const custom = (coding) => {
|
|
271
290
|
if (!decoders) return null;
|
|
@@ -311,6 +330,8 @@ export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
|
311
330
|
// point can be reached with the other's assumption.
|
|
312
331
|
const fn = BUILT_IN.has(coding) ? null : custom(coding);
|
|
313
332
|
if (fn) {
|
|
333
|
+
// A caller-supplied decoder is not wrapped by the cap: it is their code producing their
|
|
334
|
+
// bytes, and silently truncating its output would be worse than leaving the bound to them.
|
|
314
335
|
const staged = fn(out);
|
|
315
336
|
// A decoder that returns something unreadable would surface far downstream as a confusing
|
|
316
337
|
// stream error; name it here, where the caller can see which coding misbehaved.
|
|
@@ -324,7 +345,7 @@ export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
|
324
345
|
out = staged;
|
|
325
346
|
continue;
|
|
326
347
|
}
|
|
327
|
-
out = decompressionStage(out, coding === 'x-gzip' ? 'gzip' : coding);
|
|
348
|
+
out = decompressionStage(out, coding === 'x-gzip' ? 'gzip' : coding, maxBytes);
|
|
328
349
|
}
|
|
329
350
|
return out;
|
|
330
351
|
}
|
package/src/client.js
CHANGED
|
@@ -72,7 +72,9 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
|
72
72
|
* @property {import('./client/cookies.js').CookieJar} [jar] supply a jar directly, e.g. to share
|
|
73
73
|
* one across Clients or to persist it.
|
|
74
74
|
* @property {number} [maxRedirects] default 20.
|
|
75
|
-
* @property {number} [maxBodyBytes]
|
|
75
|
+
* @property {number} [maxBodyBytes] the most body this client will produce. Checked against
|
|
76
|
+
* Content-Length before a byte is read, enforced on the raw stream, and enforced again on the
|
|
77
|
+
* DECODED output — a compressed body within the cap can decompress far past it.
|
|
76
78
|
* @property {boolean} [decompress] gzip/deflate. Default true.
|
|
77
79
|
* @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
|
|
78
80
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
|
@@ -825,7 +827,7 @@ function decodeResponseBody(body, headers, options) {
|
|
|
825
827
|
if (options.decompress === false) return body;
|
|
826
828
|
const encoding = headers.get('content-encoding');
|
|
827
829
|
if (!encoding) return body;
|
|
828
|
-
return decodeBody(body, encoding, options.decoders ?? null);
|
|
830
|
+
return decodeBody(body, encoding, options.decoders ?? null, options.maxBodyBytes ?? Infinity);
|
|
829
831
|
}
|
|
830
832
|
|
|
831
833
|
function buildResponse(headInfo, body, framing, conn) {
|
package/types/client/decode.d.ts
CHANGED
|
@@ -23,9 +23,11 @@ export function acceptEncodingFor(decoders: Record<string, BodyDecoder> | null |
|
|
|
23
23
|
* comma-separated list names codings in the order the SERVER applied them, so decoding
|
|
24
24
|
* applies them in reverse.
|
|
25
25
|
* @param {Record<string, BodyDecoder> | null} [decoders] caller-supplied codings
|
|
26
|
+
* @param {number} [maxBytes] cap on DECODED output, per stage. `maxBodyBytes` alone bounded the
|
|
27
|
+
* compressed body, which a gzip bomb walks straight past.
|
|
26
28
|
* @returns {ReadableStream<Uint8Array>} decoded bytes
|
|
27
29
|
*/
|
|
28
|
-
export function decodeBody(stream: ReadableStream<Uint8Array>, contentEncoding: string | null | undefined, decoders?: Record<string, BodyDecoder> | null): ReadableStream<Uint8Array>;
|
|
30
|
+
export function decodeBody(stream: ReadableStream<Uint8Array>, contentEncoding: string | null | undefined, decoders?: Record<string, BodyDecoder> | null, maxBytes?: number): ReadableStream<Uint8Array>;
|
|
29
31
|
/**
|
|
30
32
|
* Extract the charset parameter from a Content-Type value, handling quoting and other
|
|
31
33
|
* parameters: `text/html; boundary=x; charset="ISO-8859-4"` -> 'iso-8859-4'.
|
package/types/client.d.ts
CHANGED
|
@@ -53,7 +53,9 @@ export function install(options?: ClientOptions): () => void;
|
|
|
53
53
|
* @property {import('./client/cookies.js').CookieJar} [jar] supply a jar directly, e.g. to share
|
|
54
54
|
* one across Clients or to persist it.
|
|
55
55
|
* @property {number} [maxRedirects] default 20.
|
|
56
|
-
* @property {number} [maxBodyBytes]
|
|
56
|
+
* @property {number} [maxBodyBytes] the most body this client will produce. Checked against
|
|
57
|
+
* Content-Length before a byte is read, enforced on the raw stream, and enforced again on the
|
|
58
|
+
* DECODED output — a compressed body within the cap can decompress far past it.
|
|
57
59
|
* @property {boolean} [decompress] gzip/deflate. Default true.
|
|
58
60
|
* @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
|
|
59
61
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
|
@@ -227,7 +229,9 @@ export type ClientOptions = {
|
|
|
227
229
|
*/
|
|
228
230
|
maxRedirects?: number | undefined;
|
|
229
231
|
/**
|
|
230
|
-
*
|
|
232
|
+
* the most body this client will produce. Checked against
|
|
233
|
+
* Content-Length before a byte is read, enforced on the raw stream, and enforced again on the
|
|
234
|
+
* DECODED output — a compressed body within the cap can decompress far past it.
|
|
231
235
|
*/
|
|
232
236
|
maxBodyBytes?: number | undefined;
|
|
233
237
|
/**
|