diff --git a/Cargo.lock b/Cargo.lock index bb74d30e..38576c4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "async-channel" version = "1.9.0" @@ -756,6 +762,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -900,11 +912,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gloo-timers" version = "0.3.0" @@ -963,6 +988,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -971,7 +1005,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1212,6 +1246,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1245,12 +1285,14 @@ dependencies = [ "log", "lol_html", "mime", + "rand", "reqwest", "rustls", "rustls-platform-verifier", "thiserror 2.0.18", "tokio", "url", + "uuid", "webpki-root-certs", ] @@ -1297,7 +1339,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1385,6 +1429,12 @@ dependencies = [ "log", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -1447,8 +1497,8 @@ dependencies = [ "cfg-if", "cssparser", "encoding_rs", - "foldhash", - "hashbrown", + "foldhash 0.2.0", + "hashbrown 0.16.1", "memchr", "mime", "precomputed-hash", @@ -1765,6 +1815,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1948,6 +2008,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -2656,6 +2722,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2686,6 +2758,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "value-bag" version = "1.12.0" @@ -2732,6 +2815,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -2791,6 +2883,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -2804,6 +2918,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.91" @@ -3104,6 +3230,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/impit-node/index.wrapper.js b/impit-node/index.wrapper.js index cf8e70bb..d91db141 100644 --- a/impit-node/index.wrapper.js +++ b/impit-node/index.wrapper.js @@ -1,4 +1,3 @@ -const { castToTypedArray } = require('./request.js'); const errors = require('./errors.js'); const { rethrowNativeError } = errors; let native = null; @@ -40,56 +39,6 @@ function canonicalizeHeaders(headers) { return []; } -async function parseFetchOptions(resource, init) { - let url; - let options = { ...init }; - - // Handle Request instance - if (resource instanceof Request) { - url = resource.url; - options = { - method: resource.method, - headers: resource.headers, - body: resource.body, - ...init, // init overrides Request fields - }; - // Extract redirect from Request only if not already set by init. - // Request.redirect defaults to 'follow', which is indistinguishable - // from an explicit 'follow', so we only use it when non-default to - // avoid silently overriding instance-level followRedirects. - if (!('redirect' in options) && resource.redirect !== 'follow') { - options.redirect = resource.redirect; - } - } else if (resource.toString) { - url = resource.toString(); - } else { - url = resource; - } - - options.headers = canonicalizeHeaders(options?.headers); - - if (options?.body) { - const { body: requestBody, type } = await castToTypedArray(options.body); - options.body = requestBody; - if (type && !options.headers.some(([key]) => key.toLowerCase() === 'content-type')) { - options.headers.push(['Content-Type', type]); - } - } else { - delete options.body; - } - - return { - url: url, - method: options.method, - headers: options.headers, - body: options.body, - timeout: options.timeout, - forceHttp3: options.forceHttp3, - signal: options.signal, - redirect: options.redirect, - }; -} - function isRedirectStatus(status) { return [301, 302, 303, 307, 308].includes(status); } @@ -154,8 +103,147 @@ class Impit extends native.Impit { } } + // Taken from https://github.com/nodejs/undici/blob/14e62db0d0cff4bea27357aa5bd14881459b27c7/lib/web/fetch/body.js#L120 + async #generateMultipartFormData(formData) { + const boundary = super.getMultipartBoundary(); + const prefix = `--${boundary}\r\nContent-Disposition: form-data`; + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22'); + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n'); + + const blobParts = []; + const rn = new Uint8Array([13, 10]); + const textEncoder = new TextEncoder(); + + for (const [name, value] of formData) { + if (typeof value === 'string') { + const chunk = textEncoder.encode(prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`); + blobParts.push(chunk); + } else { + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + + `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`); + blobParts.push(chunk, value, rn); + } + } + + blobParts.push(textEncoder.encode(`--${boundary}--\r\n`)); + + const action = async function* () { + for (const part of blobParts) { + if (part.stream) { + yield* part.stream(); + } else { + yield part; + } + } + }; + + const parts = []; + for await (const part of action()) { + if (part instanceof Uint8Array) { + parts.push(part); + } else if (part instanceof Blob) { + parts.push(new Uint8Array(await part.arrayBuffer())); + } else { + throw new TypeError('Unsupported part type'); + } + } + const body = new Uint8Array(parts.reduce((acc, part) => acc + part.length, 0)); + let offset = 0; + for (const part of parts) { + body.set(part, offset); + offset += part.length; + } + + return { body, type: `multipart/form-data; boundary=${boundary}` }; + } + + // Based on https://github.com/nodejs/undici/blob/14e62db0d0cff4bea27357aa5bd14881459b27c7/lib/web/fetch/body.js#L90 + async #serializeBody(body) { + if (typeof body === 'string') { + return { body: new TextEncoder().encode(body), type: 'text/plain;charset=UTF-8' }; + } else if (body instanceof URLSearchParams) { + return { body: new TextEncoder().encode(body.toString()), type: 'application/x-www-form-urlencoded;charset=UTF-8' }; + } else if (body instanceof ArrayBuffer) { + return { body: new Uint8Array(body.slice()), type: '' }; + } else if (ArrayBuffer.isView(body)) { + return { body: new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)), type: '' }; + } else if (body instanceof Blob) { + return { body: new Uint8Array(await body.arrayBuffer()), type: body.type }; + } else if (body instanceof FormData) { + return await this.#generateMultipartFormData(body); + } else if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks = []; + let done = false; + while (!done) { + const { done: streamDone, value } = await reader.read(); + done = streamDone; + if (value) chunks.push(value); + } + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const typedArray = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + typedArray.set(chunk, offset); + offset += chunk.length; + } + return { body: typedArray, type: '' }; + } + return { body, type: '' }; + } + + async #parseFetchOptions(resource, init) { + let url; + let options = { ...init }; + + if (resource instanceof Request) { + url = resource.url; + options = { + method: resource.method, + headers: resource.headers, + body: resource.body, + ...init, + }; + if (!('redirect' in options) && resource.redirect !== 'follow') { + options.redirect = resource.redirect; + } + } else if (resource.toString) { + url = resource.toString(); + } else { + url = resource; + } + + options.headers = canonicalizeHeaders(options?.headers); + + if (options?.body) { + const { body: requestBody, type } = await this.#serializeBody(options.body); + options.body = requestBody; + if (type && !options.headers.some(([key]) => key.toLowerCase() === 'content-type')) { + options.headers.push(['Content-Type', type]); + } + } else { + delete options.body; + } + + return { + url: url, + method: options.method, + headers: options.headers, + body: options.body, + timeout: options.timeout, + forceHttp3: options.forceHttp3, + signal: options.signal, + redirect: options.redirect, + }; + } + async fetch(resource, init) { - const { url: initialUrl, signal, redirect, ...options } = await parseFetchOptions(resource, init); + const { url: initialUrl, signal, redirect, ...options } = await this.#parseFetchOptions(resource, init); // Check immediately if already aborted (before creating any promises) signal?.throwIfAborted(); diff --git a/impit-node/request.js b/impit-node/request.js deleted file mode 100644 index def45b91..00000000 --- a/impit-node/request.js +++ /dev/null @@ -1,126 +0,0 @@ -// Taken from https://github.com/nodejs/undici/blob/14e62db0d0cff4bea27357aa5bd14881459b27c7/lib/web/fetch/body.js#L120 -// patched for use with Impit -async function generateMultipartFormData(formData) { - const boundary = `----formdata-impit-0${`${Math.random().toString().slice(0, 5)}`.padStart(11, '0')}`; - const prefix = `--${boundary}\r\nContent-Disposition: form-data`; - - /*! formdata-polyfill. MIT License. Jimmy Wärting */ - const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22'); - const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n'); - - // Set action to this step: run the multipart/form-data - // encoding algorithm, with object’s entry list and UTF-8. - // - This ensures that the body is immutable and can't be changed afterwords - // - That the content-length is calculated in advance. - // - And that all parts are pre-encoded and ready to be sent. - const blobParts = []; - const rn = new Uint8Array([13, 10]); // '\r\n' - const textEncoder = new TextEncoder(); - - for (const [name, value] of formData) { - if (typeof value === 'string') { - const chunk = textEncoder.encode(prefix + - `; name="${escape(normalizeLinefeeds(name))}"` + - `\r\n\r\n${normalizeLinefeeds(value)}\r\n`); - blobParts.push(chunk); - } else { - const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + - (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + - `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`); - blobParts.push(chunk, value, rn); - } - } - - // CRLF is appended to the body to function with legacy servers and match other implementations. - // https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030 - // https://github.com/form-data/form-data/issues/63 - const chunk = textEncoder.encode(`--${boundary}--\r\n`); - blobParts.push(chunk); - - const action = async function* () { - for (const part of blobParts) { - if (part.stream) { - yield* part.stream(); - } else { - yield part; - } - } - }; - - const parts = []; - for await (const part of action()) { - if (part instanceof Uint8Array) { - parts.push(part); - } else if (part instanceof Blob) { - const arrayBuffer = await part.arrayBuffer(); - parts.push(new Uint8Array(arrayBuffer)); - } else { - throw new TypeError('Unsupported part type'); - } - } - const body = new Uint8Array(parts.reduce((acc, part) => acc + part.length, 0)); - let offset = 0; - for (const part of parts) { - body.set(part, offset); - offset += part.length; - } - - // Set type to `multipart/form-data; boundary=`, - // followed by the multipart/form-data boundary string generated - // by the multipart/form-data encoding algorithm. - return { - body, - type: `multipart/form-data; boundary=${boundary}`, - }; -} - - -// logic from https://github.com/nodejs/undici/blob/14e62db0d0cff4bea27357aa5bd14881459b27c7/lib/web/fetch/body.js#L90 -async function castToTypedArray(body) { - let typedArray = body; - let type = ""; - - if (typeof body === 'string') { - typedArray = new TextEncoder().encode(body); - type = 'text/plain;charset=UTF-8'; - } - else if (typedArray instanceof URLSearchParams) { - typedArray = new TextEncoder().encode(body.toString()); - type = 'application/x-www-form-urlencoded;charset=UTF-8'; - } else if (body instanceof ArrayBuffer) { - typedArray = new Uint8Array(body.slice()); - } else if (ArrayBuffer.isView(body)) { - typedArray = new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)); - } else if (body instanceof Blob) { - typedArray = new Uint8Array(await body.arrayBuffer()); - type = body.type; - } else if (body instanceof FormData) { - return await generateMultipartFormData(body); - } else if (body instanceof ReadableStream) { - const reader = body.getReader(); - const chunks = []; - - let done = false; - while (!done) { - const { done: streamDone, value } = await reader.read(); - done = streamDone; - if (value) { - chunks.push(value); - } - } - - const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); - typedArray = new Uint8Array(totalLength); - let offset = 0; - - for (const chunk of chunks) { - typedArray.set(chunk, offset); - offset += chunk.length; - } - } - - - return { body: typedArray, type }; -} - -exports.castToTypedArray = castToTypedArray; diff --git a/impit-node/src/lib.rs b/impit-node/src/lib.rs index 50ccfec1..333c444a 100644 --- a/impit-node/src/lib.rs +++ b/impit-node/src/lib.rs @@ -85,6 +85,11 @@ impl ImpitWrapper { }) } + #[napi(js_name = "getMultipartBoundary", skip_typescript)] + pub fn get_multipart_boundary(&self) -> String { + self.inner.generate_multipart_boundary() + } + #[napi(ts_args_type = "resource: string | URL | Request, init?: RequestInit")] /// Fetch a URL with the given options. /// diff --git a/impit/Cargo.toml b/impit/Cargo.toml index e5cb51fc..d40371ba 100644 --- a/impit/Cargo.toml +++ b/impit/Cargo.toml @@ -21,3 +21,5 @@ rustls-platform-verifier = "0.6" webpki-root-certs = "1.0.5" hyper-util = "0.1.18" hyper = "1.7.0" +rand = "0.9" +uuid = { version = "1", features = ["v4"] } diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index f77648ee..1d6bae8e 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -6,6 +6,8 @@ pub mod database; mod types; +use rand::Rng; + pub use types::*; /// A complete browser fingerprint containing TLS, HTTP/2, and HTTP header configurations. @@ -34,6 +36,36 @@ impl BrowserFingerprint { headers, } } + + pub fn generate_multipart_boundary(&self) -> String { + match self.name.as_str() { + "Chrome" => { + const CHARS: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let mut rng = rand::rng(); + let suffix: String = (0..16) + .map(|_| CHARS[rng.random_range(0..CHARS.len())] as char) + .collect(); + format!("----WebKitFormBoundary{suffix}") + } + "Firefox" => { + let mut rng = rand::rng(); + let a: u64 = rng.random(); + let b: u64 = rng.random(); + format!("----geckoformboundary{a:x}{b:x}") + } + "OkHttp" => uuid::Uuid::new_v4().to_string(), + _ => default_multipart_boundary(), + } + } +} + +pub fn default_multipart_boundary() -> String { + let mut rng = rand::rng(); + let suffix: String = (0..16) + .map(|_| rng.random_range(0u8..10).to_string()) + .collect(); + format!("----formdata-impit-{suffix}") } #[derive(Clone, Debug, PartialEq, Eq, Hash)] diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 6e6cb9f8..1bd7a75c 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -637,4 +637,11 @@ impl Impit { ) -> Result { self.make_request(Method::PATCH, url, body, options).await } + + pub fn generate_multipart_boundary(&self) -> String { + match &self.config.fingerprint { + Some(fp) => fp.generate_multipart_boundary(), + None => crate::fingerprint::default_multipart_boundary(), + } + } }