diff --git a/Cargo.lock b/Cargo.lock index e4f60ae3..235b7f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1509,7 +1509,6 @@ dependencies = [ "hyper-util", "log", "mime", - "num-bigint", "reqwest", "rustls", "rustls-platform-verifier", @@ -1926,40 +1925,12 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "object" version = "0.36.7" @@ -2585,7 +2556,7 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.36" -source = "git+https://github.com/apify/rustls?rev=c5338dd72cdccbfdb717aeb218fd05c60e3dd139#c5338dd72cdccbfdb717aeb218fd05c60e3dd139" +source = "git+https://github.com/apify/rustls?rev=8c46c4744be711e87946d967301c8dac648f4049#8c46c4744be711e87946d967301c8dac648f4049" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index 73964c2b..0c211faa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ [patch.crates-io] h2 = { git = "https://github.com/apify/h2", rev = "7f393a728a8db07cabb1b78d2094772b33943b9a" } -rustls = { git = "https://github.com/apify/rustls", rev = "c5338dd72cdccbfdb717aeb218fd05c60e3dd139" } +rustls = { git = "https://github.com/apify/rustls", rev="8c46c4744be711e87946d967301c8dac648f4049" } [profile.release] strip = true # Automatically strip symbols from the binary. diff --git a/README.md b/README.md index a5ba0675..aa70ea92 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,29 @@ impit is a `rust` library that allows you to impersonate a browser and make requ The library provides a simple API for making requests to websites, and it also allows you to customize the request headers, use proxies, custom timeouts and more. ```rust -use impit::impit::Impit; -use impit::emulation::Browser; -use reqwest::cookie::Jar; +use impit::cookie::Jar; +use impit::{impit::Impit, fingerprint::database as fingerprints}; #[tokio::main] async fn main() { - let impit = Impit::::builder() - .with_browser(Browser::Firefox) - .with_http3() - .build() - .unwrap(); - - let response = impit.get(String::from("https://example.com"), None, None).await; - - match response { - Ok(response) => { - println!("{}", response.text().await.unwrap()); - } - Err(e) => { - println!("{:#?}", e); - } - } + let impit = Impit::::builder() + .with_fingerprint(fingerprints::firefox_128::fingerprint()) + .with_http3() + .build() + .unwrap(); + + let response = impit + .get(String::from("https://example.com"), None, None) + .await; + + match response { + Ok(response) => { + println!("{}", response.text().await.unwrap()); + } + Err(e) => { + println!("{:#?}", e); + } + } } ``` diff --git a/impit-cli/src/main.rs b/impit-cli/src/main.rs index 46da3fc5..15ffdb14 100644 --- a/impit-cli/src/main.rs +++ b/impit-cli/src/main.rs @@ -2,7 +2,6 @@ use std::ffi::OsString; use clap::{Parser, ValueEnum}; use impit::{ - emulation::Browser as ImpitBrowser, impit::{Impit, RedirectBehavior}, request::RequestOptions, }; @@ -94,8 +93,12 @@ async fn main() { .with_fallback_to_vanilla(args.fallback); client = match args.impersonate { - Browser::Chrome => client.with_browser(ImpitBrowser::Chrome), - Browser::Firefox => client.with_browser(ImpitBrowser::Firefox), + Browser::Chrome => { + client.with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()) + } + Browser::Firefox => { + client.with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()) + } Browser::Impit => client, }; diff --git a/impit-node/src/impit_builder.rs b/impit-node/src/impit_builder.rs index 8b968c88..525a569b 100644 --- a/impit-node/src/impit_builder.rs +++ b/impit-node/src/impit_builder.rs @@ -1,7 +1,7 @@ use std::time::Duration; use impit::{ - emulation::Browser as ImpitBrowser, + fingerprint::BrowserFingerprint, impit::{ImpitBuilder, RedirectBehavior}, }; use napi::{bindgen_prelude::Object, Env}; @@ -18,15 +18,6 @@ pub enum Browser { Firefox, } -impl From for ImpitBrowser { - fn from(val: Browser) -> Self { - match val { - Browser::Chrome => ImpitBrowser::Chrome, - Browser::Firefox => ImpitBrowser::Firefox, - } - } -} - /// Options for configuring an {@link Impit} instance. /// /// These options allow you to customize the behavior of the Impit instance, including browser emulation, TLS settings, proxy configuration, timeouts, and more. @@ -96,11 +87,20 @@ pub struct ImpitOptions<'a> { pub local_address: Option, } +impl From for BrowserFingerprint { + fn from(val: Browser) -> Self { + match val { + Browser::Chrome => impit::fingerprint::database::chrome_125::fingerprint(), + Browser::Firefox => impit::fingerprint::database::firefox_128::fingerprint(), + } + } +} + impl ImpitOptions<'_> { pub fn into_builder(self, env: &Env) -> Result, napi::Error> { let mut config = ImpitBuilder::default(); if let Some(browser) = self.browser { - config = config.with_browser(browser.into()); + config = config.with_fingerprint(browser.into()); } if let Some(ignore_tls_errors) = self.ignore_tls_errors { config = config.with_ignore_tls_errors(ignore_tls_errors); diff --git a/impit-node/test/fingerprints.test.ts b/impit-node/test/fingerprints.test.ts index 2d5e1f75..5d4de32b 100644 --- a/impit-node/test/fingerprints.test.ts +++ b/impit-node/test/fingerprints.test.ts @@ -6,9 +6,21 @@ describe.each([ [Browser.Chrome, "t13d1516h2_8daaf6152771_02713d6af862"], [Browser.Firefox, "t13d1715h2_5b57614c22b0_5c2c66f702b0"], ])(`Browser emulation [%s]`, (browser, ja4) => { - const impit = new Impit({ browser }); - test('emulates JA4 fingerprint', async () => { + test(`[${browser}] emulates JA4 fingerprint`, async () => { + const impit = new Impit({ browser }); + const response = await impit.fetch("https://headers.superuser.one/"); + const text = await response.text(); + + const ja4Line = text.split('\n').find(line => line.startsWith('cf-ja4 => ')); + expect(ja4Line).toBeDefined(); + if (ja4Line) { + expect(ja4Line.split('=> ')[1]).toBe(ja4); + } + }); + + test(`[${browser}] without TLS verifier emulates JA4 fingerprint`, async () => { + const impit = new Impit({ browser, ignoreTlsErrors: false }); const response = await impit.fetch("https://headers.superuser.one/"); const text = await response.text(); diff --git a/impit-python/src/async_client.rs b/impit-python/src/async_client.rs index 239c1195..bb860fb4 100644 --- a/impit-python/src/async_client.rs +++ b/impit-python/src/async_client.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use impit::{ - emulation::Browser, errors::ImpitError, impit::{Impit, ImpitBuilder}, request::RequestOptions, @@ -59,8 +58,10 @@ impl AsyncClient { let builder = match browser { Some(browser) => match browser.to_lowercase().as_str() { - "chrome" => builder.with_browser(Browser::Chrome), - "firefox" => builder.with_browser(Browser::Firefox), + "chrome" => builder + .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), + "firefox" => builder + .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), _ => { return Err(PyErr::new::( "Unsupported browser", diff --git a/impit-python/src/client.rs b/impit-python/src/client.rs index c2b38b8b..d2c10b65 100644 --- a/impit-python/src/client.rs +++ b/impit-python/src/client.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, time::Duration}; use impit::{ - emulation::Browser, errors::ImpitError, impit::{Impit, ImpitBuilder}, request::RequestOptions, @@ -56,8 +55,10 @@ impl Client { let builder = match browser { Some(browser) => match browser.to_lowercase().as_str() { - "chrome" => builder.with_browser(Browser::Chrome), - "firefox" => builder.with_browser(Browser::Firefox), + "chrome" => builder + .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), + "firefox" => builder + .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), _ => panic!("Unsupported browser"), }, None => builder, diff --git a/impit/Cargo.toml b/impit/Cargo.toml index c22a53e2..f2328e3c 100644 --- a/impit/Cargo.toml +++ b/impit/Cargo.toml @@ -11,7 +11,6 @@ hickory-client = "0.25.1" hickory-proto = "0.25.1" log = "0.4.22" mime = "0.3.17" -num-bigint = "0.4.6" reqwest = { version="0.13.1", features = ["json", "gzip", "brotli", "zstd", "deflate", "http3", "cookies", "stream", "socks"] } rustls = { version="0.23.36", features=["impit"] } scraper = "0.25.0" diff --git a/impit/examples/basic.rs b/impit/examples/basic.rs index e7afdd8c..cfdce6ed 100644 --- a/impit/examples/basic.rs +++ b/impit/examples/basic.rs @@ -1,11 +1,10 @@ use impit::cookie::Jar; -use impit::emulation::Browser; -use impit::impit::Impit; +use impit::{fingerprint::database as fingerprints, impit::Impit}; #[tokio::main] async fn main() { let impit = Impit::::builder() - .with_browser(Browser::Firefox) + .with_fingerprint(fingerprints::firefox_128::fingerprint()) .with_http3() .build() .unwrap(); diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs new file mode 100644 index 00000000..9b3ebf97 --- /dev/null +++ b/impit/src/fingerprint/database.rs @@ -0,0 +1,9 @@ +//! Pre-defined browser fingerprints +//! +//! This module contains fingerprint definitions for various browsers. + +mod chrome; +mod firefox; + +pub use chrome::chrome_125; +pub use firefox::firefox_128; diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs new file mode 100644 index 00000000..41042967 --- /dev/null +++ b/impit/src/fingerprint/database/chrome.rs @@ -0,0 +1,136 @@ +//! Chrome browser fingerprints + +use crate::fingerprint::*; + +/// Chrome 125 fingerprint module +pub mod chrome_125 { + use super::*; + + /// Returns the complete Chrome 125 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "125", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Chrome 125 TLS fingerprint + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Cipher suites in Chrome 125 preference order (matching CHROME_CIPHER_SUITES) + vec![ + CipherSuite::Grease, + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + ], + // Key exchange groups (GREASE at the end for Chrome fingerprint) + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Grease, + ], + // Signature algorithms - order must match DEFAULT_SIGNATURE_VERIFICATION_ALGOS + // Note: No SHA1 algorithms for Chrome (matches original implementation) + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + ], + // TLS extensions configuration + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + true, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + Some(vec![CertificateCompressionAlgorithm::Brotli]), // compress_certificate + true, // application_settings + false, // delegated_credentials (Chrome doesn't use) + None, // record_size_limit (Chrome doesn't use) + // Extension order (critical for fingerprinting) + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::SessionTicket, + ExtensionType::SignatureAlgorithms, + ExtensionType::StatusRequest, + ExtensionType::SupportedGroups, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::SignedCertificateTimestamp, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::CompressCertificate, + ExtensionType::ApplicationSettings, + ], + ), + // ECH configuration (GREASE mode) + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Chrome 125 HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ":protocol".to_string(), + ":status".to_string(), + ], + } + } + + /// Chrome 125 HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("sec-ch-ua".to_string(), "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"".to_string()), + ("sec-ch-ua-mobile".to_string(), "?0".to_string()), + ("sec-ch-ua-platform".to_string(), "\"Linux\"".to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("user-agent".to_string(), "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36".to_string()), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ] + } +} diff --git a/impit/src/fingerprint/database/firefox.rs b/impit/src/fingerprint/database/firefox.rs new file mode 100644 index 00000000..17d2a800 --- /dev/null +++ b/impit/src/fingerprint/database/firefox.rs @@ -0,0 +1,141 @@ +//! Firefox browser fingerprints + +use crate::fingerprint::*; + +/// Firefox 128 fingerprint module +pub mod firefox_128 { + use super::*; + + /// Returns the complete Firefox 128 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Firefox", + "128", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Firefox 128 TLS fingerprint + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Cipher suites in Firefox 128 preference order (17 suites) + // TLS 1.3 cipher suites first (including fake ones for fingerprinting), then TLS 1.2 + vec![ + // Real TLS 1.3 cipher suites + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + // Fake cipher suites for TLS 1.3 fingerprinting (advertised but not used) + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + // Real TLS 1.2 cipher suites + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + ], + // Key exchange groups (Firefox includes FFDHE groups) + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Ffdhe2048, + KeyExchangeGroup::Ffdhe3072, + ], + // Signature algorithms - order must match FIREFOX_SIGNATURE_VERIFICATION_ALGOS mapping + // Note: Ed25519 is included in verification but not in the ClientHello extension + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::EcdsaSha1Legacy, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + // TLS extensions configuration + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp (Firefox doesn't use this, only Chrome) + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate (Firefox doesn't use this) + false, // application_settings + true, // delegated_credentials (Firefox uses this) + Some(16385), // record_size_limit (Firefox uses this) + // Extension order (critical for fingerprinting) + // Note: Firefox doesn't send SignedCertificateTimestamp + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::SessionTicket, + ExtensionType::SignatureAlgorithms, + ExtensionType::StatusRequest, + ExtensionType::SupportedGroups, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ], + ), + // ECH configuration (GREASE mode) + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Firefox 128 HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":path".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":protocol".to_string(), + ":status".to_string(), + ], + } + } + + /// Firefox 128 HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("User-Agent".to_string(), "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0".to_string()), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en,cs;q=0.7,en-US;q=0.3".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("Upgrade-Insecure-Requests".to_string(), "1".to_string()), + ("Priority".to_string(), "u=0, i".to_string()), + ] + } +} diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs new file mode 100644 index 00000000..21d5f82f --- /dev/null +++ b/impit/src/fingerprint/mod.rs @@ -0,0 +1,327 @@ +//! Browser fingerprint data structures +//! +//! This module contains all the types needed to define a complete browser fingerprint, +//! including TLS, HTTP/2, and HTTP header configurations. + +pub mod database; +mod types; + +pub use types::*; + +/// A complete browser fingerprint containing TLS, HTTP/2, and HTTP header configurations. +#[derive(Clone, Debug)] +pub struct BrowserFingerprint { + pub name: String, + pub version: String, + pub tls: TlsFingerprint, + pub http2: Http2Fingerprint, + pub headers: Vec<(String, String)>, +} + +impl BrowserFingerprint { + pub fn new( + name: impl Into, + version: impl Into, + tls: TlsFingerprint, + http2: Http2Fingerprint, + headers: Vec<(String, String)>, + ) -> Self { + Self { + name: name.into(), + version: version.into(), + tls, + http2, + headers, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TlsFingerprint { + pub cipher_suites: Vec, + pub key_exchange_groups: Vec, + pub signature_algorithms: Vec, + pub extensions: TlsExtensions, + pub ech_config: Option, + pub alpn_protocols: Vec>, +} + +impl TlsFingerprint { + #[allow(clippy::too_many_arguments)] + pub fn new( + cipher_suites: Vec, + key_exchange_groups: Vec, + signature_algorithms: Vec, + extensions: TlsExtensions, + ech_config: Option, + alpn_protocols: Vec>, + ) -> Self { + Self { + cipher_suites, + key_exchange_groups, + signature_algorithms, + extensions, + ech_config, + alpn_protocols, + } + } +} + +#[derive(Clone, Debug)] +pub struct Http2Fingerprint { + pub pseudo_header_order: Vec, +} + +/// TLS extensions configuration. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct TlsExtensions { + pub server_name: bool, + pub status_request: bool, + pub supported_groups: bool, + pub signature_algorithms: bool, + pub application_layer_protocol_negotiation: bool, + pub signed_certificate_timestamp: bool, + pub key_share: bool, + pub psk_key_exchange_modes: bool, + pub supported_versions: bool, + pub compress_certificate: Option>, + pub application_settings: bool, + pub delegated_credentials: bool, + pub record_size_limit: Option, + pub extension_order: Vec, +} + +impl TlsExtensions { + /// Creates a new TLS extensions configuration. + #[allow(clippy::too_many_arguments)] + pub fn new( + server_name: bool, + status_request: bool, + supported_groups: bool, + signature_algorithms: bool, + application_layer_protocol_negotiation: bool, + signed_certificate_timestamp: bool, + key_share: bool, + psk_key_exchange_modes: bool, + supported_versions: bool, + compress_certificate: Option>, + application_settings: bool, + delegated_credentials: bool, + record_size_limit: Option, + extension_order: Vec, + ) -> Self { + Self { + server_name, + status_request, + supported_groups, + signature_algorithms, + application_layer_protocol_negotiation, + signed_certificate_timestamp, + key_share, + psk_key_exchange_modes, + supported_versions, + compress_certificate, + application_settings, + delegated_credentials, + record_size_limit, + extension_order, + } + } +} + +/// ECH (Encrypted Client Hello) configuration. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct EchConfig { + mode: EchMode, + config_list: Option>, +} + +impl EchConfig { + /// Creates a new ECH configuration. + pub fn new(mode: EchMode, config_list: Option>) -> Self { + Self { mode, config_list } + } + + /// Returns the ECH mode. + pub fn mode(&self) -> &EchMode { + &self.mode + } + + /// Returns the ECH configuration list. + pub fn config_list(&self) -> Option<&[u8]> { + self.config_list.as_deref() + } +} + +/// ECH mode configuration. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum EchMode { + /// ECH is disabled + Disabled, + /// ECH GREASE mode with specified HPKE suite + Grease { hpke_suite: HpkeKemId }, + /// Real ECH with actual configuration + Real, +} + +impl TlsFingerprint { + /// Converts this fingerprint to a rustls TlsFingerprint. + pub fn to_rustls_fingerprint(&self) -> rustls::client::TlsFingerprint { + use rustls::client::{ + FingerprintCertCompressionAlgorithm, FingerprintCipherSuite, + FingerprintKeyExchangeGroup, FingerprintSignatureAlgorithm, TlsExtensionsConfig, + }; + + let cipher_suites: Vec = self + .cipher_suites + .iter() + .map(|cs| match cs { + CipherSuite::TLS13_AES_128_GCM_SHA256 => { + FingerprintCipherSuite::TLS13_AES_128_GCM_SHA256 + } + CipherSuite::TLS13_AES_256_GCM_SHA384 => { + FingerprintCipherSuite::TLS13_AES_256_GCM_SHA384 + } + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 => { + FingerprintCipherSuite::TLS13_CHACHA20_POLY1305_SHA256 + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + } + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + } + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + } + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + } + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + } + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + } + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256 => { + FingerprintCipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256 + } + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384 => { + FingerprintCipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384 + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + } + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA => { + FingerprintCipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA + } + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA => { + FingerprintCipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA + } + CipherSuite::Grease => FingerprintCipherSuite::Grease, + }) + .collect(); + + let key_exchange_groups: Vec = self + .key_exchange_groups + .iter() + .map(|kg| match kg { + KeyExchangeGroup::X25519 => FingerprintKeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1 => FingerprintKeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1 => FingerprintKeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Secp521r1 => FingerprintKeyExchangeGroup::Secp521r1, + KeyExchangeGroup::Ffdhe2048 => FingerprintKeyExchangeGroup::Ffdhe2048, + KeyExchangeGroup::Ffdhe3072 => FingerprintKeyExchangeGroup::Ffdhe3072, + KeyExchangeGroup::Ffdhe4096 => FingerprintKeyExchangeGroup::Ffdhe4096, + KeyExchangeGroup::Ffdhe6144 => FingerprintKeyExchangeGroup::Ffdhe6144, + KeyExchangeGroup::Ffdhe8192 => FingerprintKeyExchangeGroup::Ffdhe8192, + KeyExchangeGroup::Grease => FingerprintKeyExchangeGroup::Grease, + }) + .collect(); + + let signature_algorithms: Vec = self + .signature_algorithms + .iter() + .map(|sa| match sa { + SignatureAlgorithm::EcdsaSecp256r1Sha256 => { + FingerprintSignatureAlgorithm::EcdsaSecp256r1Sha256 + } + SignatureAlgorithm::EcdsaSecp384r1Sha384 => { + FingerprintSignatureAlgorithm::EcdsaSecp384r1Sha384 + } + SignatureAlgorithm::EcdsaSecp521r1Sha512 => { + FingerprintSignatureAlgorithm::EcdsaSecp521r1Sha512 + } + SignatureAlgorithm::RsaPssRsaSha256 => { + FingerprintSignatureAlgorithm::RsaPssRsaSha256 + } + SignatureAlgorithm::RsaPssRsaSha384 => { + FingerprintSignatureAlgorithm::RsaPssRsaSha384 + } + SignatureAlgorithm::RsaPssRsaSha512 => { + FingerprintSignatureAlgorithm::RsaPssRsaSha512 + } + SignatureAlgorithm::RsaPkcs1Sha256 => FingerprintSignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384 => FingerprintSignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512 => FingerprintSignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::RsaPkcs1Sha1 => FingerprintSignatureAlgorithm::RsaPkcs1Sha1, + SignatureAlgorithm::Ed25519 => FingerprintSignatureAlgorithm::Ed25519, + SignatureAlgorithm::Ed448 => FingerprintSignatureAlgorithm::Ed448, + SignatureAlgorithm::EcdsaSha1Legacy => { + FingerprintSignatureAlgorithm::EcdsaSha1Legacy + } + }) + .collect(); + + // Check if GREASE is needed based on extension order + let has_grease = self + .extensions + .extension_order + .iter() + .any(|e| matches!(e, ExtensionType::Grease)); + + let extensions_config = TlsExtensionsConfig { + grease: has_grease, + signed_certificate_timestamp: self.extensions.signed_certificate_timestamp, + application_settings: self.extensions.application_settings, + delegated_credentials: self.extensions.delegated_credentials, + record_size_limit: self.extensions.record_size_limit, + renegotiation_info: true, // Common for both browsers + }; + + let cert_compression = self.extensions.compress_certificate.clone().map(|algos| { + algos + .iter() + .map(|alg| match alg { + CertificateCompressionAlgorithm::Zlib => { + FingerprintCertCompressionAlgorithm::Zlib + } + CertificateCompressionAlgorithm::Brotli => { + FingerprintCertCompressionAlgorithm::Brotli + } + CertificateCompressionAlgorithm::Zstd => { + FingerprintCertCompressionAlgorithm::Zstd + } + }) + .collect() + }); + + rustls::client::TlsFingerprint::new( + cipher_suites, + key_exchange_groups, + signature_algorithms, + extensions_config, + self.alpn_protocols.clone(), + cert_compression, + ) + } +} diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs new file mode 100644 index 00000000..6f1953aa --- /dev/null +++ b/impit/src/fingerprint/types.rs @@ -0,0 +1,121 @@ +#![allow(non_camel_case_types)] +//! Type definitions for browser fingerprints +//! +//! This module contains enum types used to configure TLS and HTTP/2 fingerprints +//! in a type-safe manner. + +/// TLS cipher suites +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CipherSuite { + // TLS 1.3 cipher suites + TLS13_AES_128_GCM_SHA256, + TLS13_AES_256_GCM_SHA384, + TLS13_CHACHA20_POLY1305_SHA256, + // TLS 1.2 cipher suites + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + /// GREASE cipher suite for fingerprinting + Grease, +} + +/// Key exchange groups for TLS +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum KeyExchangeGroup { + X25519, + Secp256r1, + Secp384r1, + Secp521r1, + Ffdhe2048, + Ffdhe3072, + Ffdhe4096, + Ffdhe6144, + Ffdhe8192, + /// GREASE key exchange group for fingerprinting + Grease, +} + +/// Signature algorithms for TLS +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SignatureAlgorithm { + // RSASSA-PSS algorithms + RsaPssRsaSha256, + RsaPssRsaSha384, + RsaPssRsaSha512, + // ECDSA algorithms + EcdsaSecp256r1Sha256, + EcdsaSecp384r1Sha384, + EcdsaSecp521r1Sha512, + // Legacy RSA PKCS#1 v1.5 algorithms + RsaPkcs1Sha256, + RsaPkcs1Sha384, + RsaPkcs1Sha512, + RsaPkcs1Sha1, + // EdDSA algorithms + Ed25519, + Ed448, + // Legacy ECDSA with SHA-1 (for backwards compatibility) + EcdsaSha1Legacy, +} + +/// TLS extension types +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ExtensionType { + ServerName, + MaxFragmentLength, + StatusRequest, + SupportedGroups, + SignatureAlgorithms, + UseSrtp, + Heartbeat, + ApplicationLayerProtocolNegotiation, + SignedCertificateTimestamp, + ClientCertificateType, + ServerCertificateType, + Padding, + PreSharedKey, + EarlyData, + SupportedVersions, + Cookie, + PskKeyExchangeModes, + CertificateAuthorities, + OidFilters, + PostHandshakeAuth, + SignatureAlgorithmsCert, + KeyShare, + ExtendedMasterSecret, + SessionTicket, + CompressCertificate, + ApplicationSettings, + EarlyDataExtension, + Grease, +} + +/// Certificate compression algorithms +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CertificateCompressionAlgorithm { + Zlib, + Brotli, + Zstd, +} + +/// HPKE KEM identifiers for ECH +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum HpkeKemId { + DhKemP256HkdfSha256, + DhKemP384HkdfSha384, + DhKemP521HkdfSha512, + DhKemX25519HkdfSha256, + DhKemX448HkdfSha512, +} diff --git a/impit/src/http_headers/mod.rs b/impit/src/http_headers/mod.rs index e7cdc674..720001e2 100644 --- a/impit/src/http_headers/mod.rs +++ b/impit/src/http_headers/mod.rs @@ -1,9 +1,7 @@ -use crate::{emulation::Browser, errors::ImpitError}; +use crate::{errors::ImpitError, fingerprint::BrowserFingerprint}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use std::{collections::HashSet, str::FromStr}; -pub mod statics; - pub struct HttpHeaders { context: HttpHeadersBuilder, } @@ -22,18 +20,19 @@ impl HttpHeaders { impl HttpHeaders { pub fn iter(&self) -> impl Iterator + '_ { - let impersonated_headers = match self.context.browser { - Some(Browser::Chrome) => statics::CHROME_HEADERS, - Some(Browser::Firefox) => statics::FIREFOX_HEADERS, - None => &[], - } - .to_owned(); + // Use fingerprint headers if available, otherwise fall back to browser enum + let impersonated_headers: Vec<(String, String)> = + if let Some(ref fp) = self.context.fingerprint { + fp.headers.to_vec() + } else { + vec![] + }; let custom_headers = self .context .custom_headers .iter() - .map(|(k, v)| (k.as_str(), v.as_str())); + .map(|(k, v)| (k.clone(), v.clone())); let mut used_header_names: HashSet = HashSet::new(); @@ -44,7 +43,7 @@ impl HttpHeaders { None } else { used_header_names.insert(name.to_lowercase()); - Some((name.to_string(), value.to_string())) + Some((name, value)) } }) } @@ -85,7 +84,7 @@ impl From for Result { #[derive(Default, Clone)] pub struct HttpHeadersBuilder { host: String, - browser: Option, + fingerprint: Option, https: bool, custom_headers: Vec<(String, String)>, } @@ -97,8 +96,8 @@ impl HttpHeadersBuilder { self } - pub fn with_browser(&mut self, browser: &Option) -> &mut Self { - self.browser = browser.to_owned(); + pub fn with_fingerprint(&mut self, fingerprint: &Option) -> &mut Self { + self.fingerprint = fingerprint.clone(); self } diff --git a/impit/src/http_headers/statics.rs b/impit/src/http_headers/statics.rs deleted file mode 100644 index b823f71d..00000000 --- a/impit/src/http_headers/statics.rs +++ /dev/null @@ -1,49 +0,0 @@ -// [TODO!] -// Note that not all requests are made the same: -// - on forced (Ctrl+R) reloads, Chrome sets Cache-Control: max-age=0 -// - when the URL is in the address bar (but not submitted yet), Chrome sets `Purpose: prefetch` and `Sec-Purpose: prefetch` -pub static CHROME_HEADERS: &[(&str, &str)] = &[ - ("sec-ch-ua", "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""), - ("sec-ch-ua-mobile", "?0"), - ("sec-ch-ua-platform", "Linux"), - ("upgrade-insecure-requests", "1"), - ("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"), - ("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"), - ("sec-fetch-site", "none"), - ("sec-fetch-mode", "navigate"), - ("sec-fetch-user", "?1"), - ("sec-fetch-dest", "document"), - ("accept-encoding", "gzip, deflate, br, zstd"), - ("accept-language", "en-US,en;q=0.9"), -]; - -pub static CHROME_PSEUDOHEADERS_ORDER: [&str; 6] = [ - ":method", - ":authority", - ":scheme", - ":path", - ":protocol", - ":status", -]; - -pub static FIREFOX_HEADERS: &[(&str, &str)] = &[ - ("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"), - ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8"), - ("Accept-Language", "en,cs;q=0.7,en-US;q=0.3"), - ("Accept-Encoding", "gzip, deflate, br, zstd"), - ("sec-fetch-dest", "document"), - ("sec-fetch-mode", "navigate"), - ("sec-fetch-site", "none"), - ("sec-fetch-user", "?1"), - ("Upgrade-Insecure-Requests", "1"), - ("Priority", "u=0, i"), -]; - -pub static FIREFOX_PSEUDOHEADERS_ORDER: [&str; 6] = [ - ":method", - ":path", - ":authority", - ":scheme", - ":protocol", - ":status", -]; diff --git a/impit/src/impit.rs b/impit/src/impit.rs index dc196cbf..8a7437d5 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -6,10 +6,10 @@ use std::{fmt::Debug, net::IpAddr, str::FromStr, sync::Arc, time::Duration}; use url::Url; use crate::{ - emulation::Browser, errors::{ErrorContext, ImpitError}, + fingerprint::BrowserFingerprint, http3::H3Engine, - http_headers::{statics, HttpHeaders}, + http_headers::HttpHeaders, request::{ImpitRequest, RequestOptions}, tls, }; @@ -53,15 +53,14 @@ pub enum RedirectBehavior { /// /// ### Example /// ```rust,no_run -/// use impit::impit::Impit; -/// use impit::emulation::Browser; +/// use impit::{impit::Impit, fingerprint::database as fingerprints}; /// use reqwest::cookie::Jar; /// use std::time::Duration; /// /// # #[tokio::main] /// # async fn main() { /// let impit = Impit::::builder() -/// .with_browser(Browser::Firefox) +/// .with_fingerprint(fingerprints::firefox_128::fingerprint()) /// .with_ignore_tls_errors(true) /// .with_proxy("http://localhost:8080".to_string()) /// .with_default_timeout(Duration::from_secs(10)) @@ -74,7 +73,7 @@ pub enum RedirectBehavior { /// ``` #[derive(Debug)] pub struct ImpitBuilder { - browser: Option, + fingerprint: Option, ignore_tls_errors: bool, vanilla_fallback: bool, proxy_url: String, @@ -89,7 +88,7 @@ pub struct ImpitBuilder { impl Clone for ImpitBuilder { fn clone(&self) -> Self { ImpitBuilder { - browser: self.browser, + fingerprint: self.fingerprint.clone(), ignore_tls_errors: self.ignore_tls_errors, vanilla_fallback: self.vanilla_fallback, proxy_url: self.proxy_url.clone(), @@ -106,7 +105,7 @@ impl Clone for ImpitBuilder Default for ImpitBuilder { fn default() -> Self { ImpitBuilder { - browser: None, + fingerprint: None, ignore_tls_errors: false, vanilla_fallback: true, proxy_url: String::new(), @@ -121,13 +120,14 @@ impl Default for ImpitBuilder ImpitBuilder { - /// Sets the browser to impersonate. + /// Sets a complete browser fingerprint. /// - /// The [`Browser`] enum is used to set the HTTP headers, TLS behaviour and other markers to impersonate a specific browser. + /// This method allows you to provide a complete fingerprint that includes TLS, HTTP/2, and HTTP header configurations. + /// When set, this takes precedence over the `with_browser` method. /// - /// If not used, the client will use the default `reqwest` fingerprints. - pub fn with_browser(mut self, browser: Browser) -> Self { - self.browser = Some(browser); + /// You can use pre-defined fingerprints from [`crate::fingerprint::database`] or create custom fingerprints. + pub fn with_fingerprint(mut self, fingerprint: BrowserFingerprint) -> Self { + self.fingerprint = Some(fingerprint); self } @@ -234,13 +234,17 @@ impl Impit { ) -> Result { let mut client = reqwest::Client::builder(); let mut tls_config_builder = tls::TlsConfig::builder(); - let mut tls_config_builder = tls_config_builder.with_browser(config.browser); + + // Use fingerprint if provided, otherwise fall back to browser enum + if let Some(ref fingerprint) = config.fingerprint { + tls_config_builder.with_tls_fingerprint(fingerprint.tls.clone()); + } if config.max_http_version == Version::HTTP_3 { - tls_config_builder = tls_config_builder.with_http3(); + tls_config_builder.with_http3(); } - tls_config_builder = tls_config_builder.with_ignore_tls_errors(config.ignore_tls_errors); + tls_config_builder.with_ignore_tls_errors(config.ignore_tls_errors); let tls_config = tls_config_builder.build(); @@ -296,10 +300,11 @@ impl Impit { })?; } - let pseudo_headers_order: &[&str] = match config.browser { - Some(Browser::Chrome) => statics::CHROME_PSEUDOHEADERS_ORDER.as_ref(), - Some(Browser::Firefox) => statics::FIREFOX_PSEUDOHEADERS_ORDER.as_ref(), - None => &[], + // Set pseudo-header order from fingerprint or fall back to browser enum + let pseudo_headers_order: Vec = if let Some(ref fingerprint) = config.fingerprint { + fingerprint.http2.pseudo_header_order.to_vec() + } else { + vec![] }; if !pseudo_headers_order.is_empty() { @@ -369,7 +374,7 @@ impl Impit { let host = url.host_str().unwrap_or_default().to_string(); let headers = HttpHeaders::get_builder() - .with_browser(&self.config.browser) + .with_fingerprint(&self.config.fingerprint) .with_host(&host) .with_https(url.scheme() == "https") .with_custom_headers(self.config.headers.to_owned()) diff --git a/impit/src/lib.rs b/impit/src/lib.rs index 814a2482..66ac3829 100644 --- a/impit/src/lib.rs +++ b/impit/src/lib.rs @@ -5,14 +5,13 @@ //! The library provides a simple API for making requests to websites, and it also allows you to customize the request headers, use proxies, custom timeouts and more. //! //! ```rust,no_run -//! use impit::impit::Impit; -//! use impit::emulation::Browser; +//! use impit::{impit::Impit, fingerprint::database as fingerprints}; //! use reqwest::cookie::Jar; //! //! #[tokio::main] //! async fn main() { //! let impit = Impit::::builder() -//! .with_browser(Browser::Firefox) +//! .with_fingerprint(fingerprints::firefox_128::fingerprint()) //! .with_http3() //! .build() //! .unwrap(); @@ -75,20 +74,8 @@ pub mod request; /// Errors and error handling. pub mod errors; -/// Contains browser emulation-related types and functions. -pub mod emulation { - - /// The `Browser` enum is used to specify the browser that should be impersonated. - /// - /// It can be passed as a parameter to [`ImpitBuilder::with_browser`](crate::impit::ImpitBuilder::with_browser) - /// to use the browser emulation with the built [`Impit`](crate::impit::Impit) instance. - #[derive(PartialEq, Debug, Clone, Copy, Default)] - pub enum Browser { - #[default] - Chrome, - Firefox, - } -} +/// Browser fingerprint definitions and types. +pub mod fingerprint; /// Various utility functions and types. pub mod utils { diff --git a/impit/src/tls/ffdhe.rs b/impit/src/tls/ffdhe.rs deleted file mode 100644 index ba0ead79..00000000 --- a/impit/src/tls/ffdhe.rs +++ /dev/null @@ -1,117 +0,0 @@ -use num_bigint::BigUint; -use rustls::crypto::{ - ActiveKeyExchange, CipherSuiteCommon, CryptoProvider, KeyExchangeAlgorithm, SharedSecret, - SupportedKxGroup, -}; -use rustls::ffdhe_groups::FfdheGroup; -use rustls::{ffdhe_groups, CipherSuite, NamedGroup, SupportedCipherSuite, Tls12CipherSuite}; - -use rustls::crypto::ring as provider; - -/// A test-only `CryptoProvider`, only supporting FFDHE key exchange -pub fn ffdhe_provider() -> CryptoProvider { - CryptoProvider { - cipher_suites: FFDHE_CIPHER_SUITES.to_vec(), - kx_groups: FFDHE_KX_GROUPS.to_vec(), - ..provider::default_provider() - } -} - -static FFDHE_KX_GROUPS: &[&dyn SupportedKxGroup] = &[&FFDHE2048_KX_GROUP, &FFDHE3072_KX_GROUP]; - -pub const FFDHE2048_KX_GROUP: FfdheKxGroup = - FfdheKxGroup(NamedGroup::FFDHE2048, ffdhe_groups::FFDHE2048); -pub const FFDHE3072_KX_GROUP: FfdheKxGroup = - FfdheKxGroup(NamedGroup::FFDHE3072, ffdhe_groups::FFDHE3072); - -static FFDHE_CIPHER_SUITES: &[rustls::SupportedCipherSuite] = &[ - TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, - provider::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, -]; - -/// The (test-only) TLS1.2 ciphersuite TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 -pub static TLS_DHE_RSA_WITH_AES_128_GCM_SHA256: SupportedCipherSuite = - SupportedCipherSuite::Tls12(&TLS12_DHE_RSA_WITH_AES_128_GCM_SHA256); - -static TLS12_DHE_RSA_WITH_AES_128_GCM_SHA256: Tls12CipherSuite = - match &provider::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { - SupportedCipherSuite::Tls12(provider) => Tls12CipherSuite { - common: CipherSuiteCommon { - suite: CipherSuite::TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, - ..provider.common - }, - kx: KeyExchangeAlgorithm::DHE, - ..**provider - }, - _ => unreachable!(), - }; - -#[derive(Debug)] -pub struct FfdheKxGroup(pub NamedGroup, pub FfdheGroup<'static>); - -impl SupportedKxGroup for FfdheKxGroup { - fn start(&self) -> Result, rustls::Error> { - let mut x = vec![0; 64]; - ffdhe_provider().secure_random.fill(&mut x)?; - let x = BigUint::from_bytes_be(&x); - - let p = BigUint::from_bytes_be(self.1.p); - let g = BigUint::from_bytes_be(self.1.g); - - let x_pub = g.modpow(&x, &p); - let x_pub = to_bytes_be_with_len(x_pub, self.1.p.len()); - - Ok(Box::new(ActiveFfdheKx { - x_pub, - x, - p, - group: self.1, - named_group: self.0, - })) - } - - fn ffdhe_group(&self) -> Option> { - Some(self.1) - } - - fn name(&self) -> NamedGroup { - self.0 - } -} - -struct ActiveFfdheKx { - x_pub: Vec, - x: BigUint, - p: BigUint, - group: FfdheGroup<'static>, - named_group: NamedGroup, -} - -impl ActiveKeyExchange for ActiveFfdheKx { - fn complete(self: Box, peer_pub_key: &[u8]) -> Result { - let peer_pub = BigUint::from_bytes_be(peer_pub_key); - let secret = peer_pub.modpow(&self.x, &self.p); - let secret = to_bytes_be_with_len(secret, self.group.p.len()); - - Ok(SharedSecret::from(&secret[..])) - } - - fn pub_key(&self) -> &[u8] { - &self.x_pub - } - - fn ffdhe_group(&self) -> Option> { - Some(self.group) - } - - fn group(&self) -> NamedGroup { - self.named_group - } -} - -fn to_bytes_be_with_len(n: BigUint, len_bytes: usize) -> Vec { - let mut bytes = n.to_bytes_le(); - bytes.resize(len_bytes, 0); - bytes.reverse(); - bytes -} diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index 57196b3c..63638b4e 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -1,16 +1,79 @@ -mod ffdhe; mod statics; -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; -use crate::emulation::Browser; +use crate::fingerprint::TlsFingerprint; use reqwest::Version; use rustls::client::danger::NoVerifier; -use rustls::client::{BrowserEmulator as RusTLSBrowser, BrowserType, EchGreaseConfig}; -use rustls::crypto::aws_lc_rs::kx_group::{SECP256R1, SECP384R1, X25519}; +use rustls::client::EchGreaseConfig; use rustls::crypto::CryptoProvider; use rustls_platform_verifier::Verifier; +static VANILLA_CRYPTO_PROVIDER: OnceLock> = OnceLock::new(); +static VANILLA_VERIFIER: OnceLock> = OnceLock::new(); + +type BrowserCacheValue = (Arc, Arc); +static BROWSER_CACHE: OnceLock>> = OnceLock::new(); + +fn get_browser_cache() -> &'static Mutex> { + BROWSER_CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn get_vanilla_provider() -> Arc { + VANILLA_CRYPTO_PROVIDER + .get_or_init(|| CryptoProvider::builder().build().into()) + .clone() +} + +fn get_vanilla_verifier() -> Arc { + let provider = get_vanilla_provider(); + VANILLA_VERIFIER + .get_or_init(|| { + Arc::new( + Verifier::new_with_extra_roots( + webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned(), + provider, + ) + .expect("Failed to create certificate verifier with embedded CA roots"), + ) + }) + .clone() +} + +fn get_or_create_browser_provider_and_verifier( + tls_fingerprint: TlsFingerprint, +) -> BrowserCacheValue { + { + let cache = get_browser_cache().lock().unwrap(); + if let Some(cached) = cache.get(&tls_fingerprint) { + return cached.clone(); + } + } + + let rustls_fp = tls_fingerprint.to_rustls_fingerprint(); + + let provider: Arc = CryptoProvider::builder() + .with_tls_fingerprint(rustls_fp) + .build() + .into(); + + let verifier = Arc::new( + Verifier::new_with_extra_roots( + webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned(), + provider.clone(), + ) + .expect("Failed to create certificate verifier with embedded CA roots"), + ); + + { + let mut cache = get_browser_cache().lock().unwrap(); + cache.insert(tls_fingerprint, (provider.clone(), verifier.clone())); + } + + (provider, verifier) +} + pub struct TlsConfig {} impl TlsConfig { @@ -19,9 +82,9 @@ impl TlsConfig { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct TlsConfigBuilder { - browser: Option, + tls_fingerprint: Option, max_http_version: Version, ignore_tls_errors: bool, } @@ -29,22 +92,21 @@ pub struct TlsConfigBuilder { impl Default for TlsConfigBuilder { fn default() -> Self { TlsConfigBuilder { - browser: None, + tls_fingerprint: None, max_http_version: Version::HTTP_2, ignore_tls_errors: false, } } } -impl TlsConfigBuilder { - fn get_ech_mode(self) -> rustls::client::EchMode { - let (public_key, _) = statics::GREASE_HPKE_SUITE.generate_key_pair().unwrap(); - - EchGreaseConfig::new(statics::GREASE_HPKE_SUITE, public_key).into() - } +fn get_ech_mode() -> rustls::client::EchMode { + let (public_key, _) = statics::GREASE_HPKE_SUITE.generate_key_pair().unwrap(); + EchGreaseConfig::new(statics::GREASE_HPKE_SUITE, public_key).into() +} - pub fn with_browser(&mut self, browser: Option) -> &mut Self { - self.browser = browser; +impl TlsConfigBuilder { + pub fn with_tls_fingerprint(&mut self, fingerprint: TlsFingerprint) -> &mut Self { + self.tls_fingerprint = Some(fingerprint); self } @@ -59,91 +121,79 @@ impl TlsConfigBuilder { } pub fn build(self) -> rustls::ClientConfig { - let mut config = match self.browser { - Some(browser) => { - let rustls_browser = match browser { - Browser::Chrome => RusTLSBrowser { - browser_type: BrowserType::Chrome, - version: 125, - }, - Browser::Firefox => RusTLSBrowser { - browser_type: BrowserType::Firefox, - version: 125, - }, - }; - - let mut crypto_provider = CryptoProvider::builder() - .with_browser_emulator(&rustls_browser) - .build(); - - if browser == Browser::Firefox { - crypto_provider.kx_groups = vec![ - X25519, - SECP256R1, - SECP384R1, - // TODO : add SECPR521R1 - &ffdhe::FFDHE2048_KX_GROUP, - &ffdhe::FFDHE3072_KX_GROUP, - ]; - } - - let crypto_provider_arc: Arc = crypto_provider.into(); - - // Create verifier with embedded Mozilla CAs as fallback for minimal containers - let verifier = Verifier::new_with_extra_roots( - webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned(), - crypto_provider_arc.clone(), - ) - .expect("Failed to create certificate verifier with embedded CA roots"); - - let mut config: rustls::ClientConfig = - rustls::ClientConfig::builder_with_provider(crypto_provider_arc) - // TODO - use the ECH extension consistently - .with_ech(self.get_ech_mode()) - .unwrap() - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_browser_emulator(&rustls_browser) - .with_no_client_auth(); - - if self.ignore_tls_errors { - config - .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier::new(Some(rustls_browser)))); - } + let ignore_tls_errors = self.ignore_tls_errors; + let max_http_version = self.max_http_version; + + let (fingerprint, cache_browser) = if let Some(fp) = self.tls_fingerprint { + (Some(fp), None) + } else { + (None, None) + }; + let mut config = if let Some(fp) = fingerprint { + let rustls_fingerprint = fp.to_rustls_fingerprint(); + + let alpn_protocols = fp.alpn_protocols.to_vec(); + + let (crypto_provider_arc, verifier) = if let Some(b) = cache_browser { + get_or_create_browser_provider_and_verifier(b) + } else { + let provider: Arc = CryptoProvider::builder() + .with_tls_fingerprint(rustls_fingerprint.clone()) + .build() + .into(); + + let verifier = Arc::new( + Verifier::new_with_extra_roots( + webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned(), + provider.clone(), + ) + .expect("Failed to create certificate verifier with embedded CA roots"), + ); + + (provider, verifier) + }; + + let mut config: rustls::ClientConfig = + rustls::ClientConfig::builder_with_provider(crypto_provider_arc) + .with_ech(get_ech_mode()) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_tls_fingerprint(rustls_fingerprint) + .with_no_client_auth(); + + config.alpn_protocols = alpn_protocols; + + if ignore_tls_errors { config + .dangerous() + .set_certificate_verifier(Arc::new(NoVerifier::with_default_schemes())); } - None => { - let crypto_provider: Arc = CryptoProvider::builder().build().into(); - // Create verifier with embedded Mozilla CAs as fallback for minimal containers - let verifier = Verifier::new_with_extra_roots( - webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned(), - crypto_provider.clone(), - ) - .expect("Failed to create certificate verifier with embedded CA roots"); - - let mut config: rustls::ClientConfig = - rustls::ClientConfig::builder_with_provider(crypto_provider) - // TODO - use the ECH extension consistently - .with_ech(self.get_ech_mode()) - .unwrap() - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_no_client_auth(); - - if self.ignore_tls_errors { - config - .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier::new(None))); - } + config + } else { + let crypto_provider = get_vanilla_provider(); + let verifier = get_vanilla_verifier(); + let mut config: rustls::ClientConfig = + rustls::ClientConfig::builder_with_provider(crypto_provider) + .with_ech(get_ech_mode()) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth(); + + if ignore_tls_errors { config + .dangerous() + .set_certificate_verifier(Arc::new(NoVerifier::with_default_schemes())); } + + config }; - if self.max_http_version == Version::HTTP_3 { + if max_http_version == Version::HTTP_3 { config.alpn_protocols = vec![b"h3".to_vec()]; };