diff --git a/.trivyignore b/.trivyignore index 695f6c0..7e7378f 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,7 +1,3 @@ -# hickory-proto 0.25.2 is pulled transitively via reqwest 0.13.3 → -# hickory-resolver 0.25. The fix is in the 0.26 branch which is -# semver-incompatible with reqwest's pin. Tracking -# https://github.com/seanmonstar/reqwest/pull/3014 — remove this -# on the next reqwest release that bumps to hickory 0.26. -# Matches deny.toml advisory ignore for RUSTSEC-2026-0119. -GHSA-q2qq-hmj6-3wpp +# Vulnerability IDs listed here are excluded from `trivy fs` scans. +# Currently empty: the hickory-proto advisory (GHSA-q2qq-hmj6-3wpp / +# RUSTSEC-2026-0119) was resolved by the hickory 0.26 upgrade. diff --git a/Cargo.lock b/Cargo.lock index 204ffae..d865641 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,15 +144,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -183,9 +183,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -205,11 +205,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -313,6 +324,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -415,9 +435,9 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -468,18 +488,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -669,6 +677,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -720,46 +729,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", + "jni", + "rand 0.10.1", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", "once_cell", - "rand", + "prefix-trie", + "rand 0.10.1", "ring", "thiserror", "tinyvec", - "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror", "tokio", "tracing", @@ -767,9 +800,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -812,9 +845,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1044,6 +1077,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "itoa" @@ -1208,9 +1244,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru-slab" @@ -1220,9 +1256,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "meta" @@ -1276,9 +1312,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1302,6 +1338,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "neon" version = "1.1.1" @@ -1486,6 +1528,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1561,7 +1614,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -1615,7 +1668,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -1625,7 +1689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1637,6 +1701,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1677,9 +1747,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -1788,9 +1858,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -1985,9 +2055,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" @@ -2025,9 +2095,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2434,9 +2504,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3050,9 +3120,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3073,18 +3143,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 554d59e..e911d48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,7 @@ nrcore = { path = "packages/core", version = "0.0.0" } num-traits = { version = "0.2.19" } pretty_assertions = { version = "1.4.1", features = ["unstable"] } regex = { version = "1.12.3" } -reqwest = { version = "0.13.3", default-features = false, features = [ +reqwest = { version = "0.13.4", default-features = false, features = [ "brotli", "charset", "cookies", diff --git a/deny.toml b/deny.toml index ffee2b6..76a85bb 100644 --- a/deny.toml +++ b/deny.toml @@ -69,15 +69,7 @@ feature-depth = 1 #db-urls = ["https://github.com/rustsec/advisory-db"] # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. -ignore = [ - # hickory-proto 0.25.2 is pulled transitively via reqwest 0.13.3 → - # hickory-resolver 0.25. Both advisories are fixed only in the 0.26 - # branch, which is semver-incompatible with reqwest's pin. Tracking - # https://github.com/seanmonstar/reqwest/pull/3014 — remove these on - # the next reqwest release that bumps to hickory 0.26. - { id = "RUSTSEC-2026-0118", reason = "DnssecDnsHandle NSEC3 unbounded loop. Reqwest does not enable hickory-resolver's `dnssec-ring`/`dnssec-aws-lc-rs` features (verified via `cargo tree -p reqwest -e features`), so the vulnerable code path is not compiled." }, - { id = "RUSTSEC-2026-0119", reason = "BinEncoder O(n^2) name-compression DoS. Only reachable on outbound DNS resolution and bounded by request timeouts; impact is local CPU spike, not remote exploit." }, -] +ignore = [] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. diff --git a/packages/node/export/agent.ts b/packages/node/export/agent.ts index 1bfbb02..a220ce6 100644 --- a/packages/node/export/agent.ts +++ b/packages/node/export/agent.ts @@ -86,6 +86,8 @@ type BodyInput = | Uint8Array | Readable | ReadableStream + | Iterable + | AsyncIterable | null | undefined; @@ -127,6 +129,16 @@ function normalizeBody(body: BodyInput, maxBufferedBytes: number): NormalizedBod } if (body instanceof Readable) return normalizeBodyBuffered(body, maxBufferedBytes); if (body instanceof ReadableStream) return normalizeBodyStreaming(body); + // Each symbol lives on only one half of the `Iterable | AsyncIterable` + // union, so a cast is needed to probe both. Matches undici's own + // `typeof obj[Symbol.asyncIterator] === "function"` test (lib/core/util.js). + const iterable = body as Partial & Iterable>; + if ( + typeof iterable[Symbol.asyncIterator] === "function" || + typeof iterable[Symbol.iterator] === "function" + ) { + return normalizeBodyBuffered(Readable.from(body), maxBufferedBytes); + } return EMPTY_BODY; } diff --git a/packages/node/tests/vitest/dispatch-integration.test.ts b/packages/node/tests/vitest/dispatch-integration.test.ts index d542a58..2dd3f66 100644 --- a/packages/node/tests/vitest/dispatch-integration.test.ts +++ b/packages/node/tests/vitest/dispatch-integration.test.ts @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import type { AddressInfo } from "node:net"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { type Dispatcher, fetch } from "undici"; import { Agent } from "../../export/agent.ts"; import { InvalidArgumentError } from "../../export/errors.ts"; @@ -131,6 +132,84 @@ describe("E2E Dispatch Integration", () => { }); expect(r.bytes.toString()).toContain("10240 bytes"); }); + + it("uploads an async-iterable request body", async () => { + server = await startServer((req, res) => { + let len = 0; + req.on("data", (c: Buffer) => { + len += c.length; + }); + req.on("end", () => { + res.writeHead(200); + res.end(`Received ${len} bytes`); + }); + }); + assert(agent); + async function* chunks(): AsyncGenerator { + yield new Uint8Array(512).fill(120); + yield new Uint8Array(512).fill(121); + } + const r = await dispatchOnce(agent, { + origin: `http://127.0.0.1:${server.port}`, + path: "/upload", + method: "POST", + // undici's Dispatcher accepts iterable bodies; its TS type omits them. + body: chunks() as unknown as Dispatcher.DispatchOptions["body"], + }); + expect(r.error).toBeNull(); + expect(r.bytes.toString()).toContain("1024 bytes"); + }); + + it("uploads a sync-iterable request body", async () => { + server = await startServer((req, res) => { + let len = 0; + req.on("data", (c: Buffer) => { + len += c.length; + }); + req.on("end", () => { + res.writeHead(200); + res.end(`Received ${len} bytes`); + }); + }); + assert(agent); + // A plain array of chunks is a sync iterable (the `Symbol.iterator` leg). + const body: Iterable = [ + new Uint8Array(256).fill(120), + new Uint8Array(768).fill(121), + ]; + const r = await dispatchOnce(agent, { + origin: `http://127.0.0.1:${server.port}`, + path: "/upload", + method: "POST", + body: body as unknown as Dispatcher.DispatchOptions["body"], + }); + expect(r.error).toBeNull(); + expect(r.bytes.toString()).toContain("1024 bytes"); + }); + + it("uploads a body submitted through undici fetch", async () => { + // undici's `fetch` hands the body to the dispatcher as an async iterable + // (even a `Uint8Array` becomes a generator) while advertising a + // `content-length`. Dropping it would hang the request — guard against it. + server = await startServer((req, res) => { + let len = 0; + req.on("data", (c: Buffer) => { + len += c.length; + }); + req.on("end", () => { + res.writeHead(200); + res.end(`Received ${len} bytes`); + }); + }); + assert(agent); + const res = await fetch(`http://127.0.0.1:${server.port}/upload`, { + method: "POST", + body: new Uint8Array(1024).fill(120), + dispatcher: agent, + }); + expect(res.status).toBe(200); + expect(await res.text()).toContain("1024 bytes"); + }); }); describe("E2E concurrency and lifecycle", () => {