From 2e957b6f18d574378065d328496c7ab87e625d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Sun, 25 Jan 2026 15:08:14 +0100 Subject: [PATCH 1/8] feat: add custom browser fingerprint support Add support for custom browser fingerprints that allow fine-grained control over TLS, HTTP/2, and HTTP header configurations. This enables users to provide complete fingerprints via `with_fingerprint()` method, which takes precedence over the `with_browser()` method. Changes: - Add fingerprint module with types for TLS, HTTP/2, and header fingerprints - Add fingerprint database with Chrome and Firefox preset fingerprints - Update TLS config builder to accept custom TLS fingerprints - Update HTTP headers builder to use fingerprint headers when available - Update HTTP/2 pseudo-header order to use fingerprint configuration - Add example demonstrating custom fingerprint usage Co-Authored-By: Claude Sonnet 4.5 --- impit/examples/custom_fingerprint.rs | 161 ++++++++ impit/src/fingerprint/database.rs | 22 + impit/src/fingerprint/database/chrome.rs | 149 +++++++ impit/src/fingerprint/database/firefox.rs | 147 +++++++ impit/src/fingerprint/mod.rs | 465 ++++++++++++++++++++++ impit/src/fingerprint/types.rs | 112 ++++++ impit/src/http_headers/mod.rs | 36 +- impit/src/impit.rs | 47 ++- impit/src/lib.rs | 3 + impit/src/tls/mod.rs | 92 ++++- 10 files changed, 1198 insertions(+), 36 deletions(-) create mode 100644 impit/examples/custom_fingerprint.rs create mode 100644 impit/src/fingerprint/database.rs create mode 100644 impit/src/fingerprint/database/chrome.rs create mode 100644 impit/src/fingerprint/database/firefox.rs create mode 100644 impit/src/fingerprint/mod.rs create mode 100644 impit/src/fingerprint/types.rs diff --git a/impit/examples/custom_fingerprint.rs b/impit/examples/custom_fingerprint.rs new file mode 100644 index 00000000..0d4b58e8 --- /dev/null +++ b/impit/examples/custom_fingerprint.rs @@ -0,0 +1,161 @@ +//! Example demonstrating custom browser fingerprint usage +//! +//! This example shows how to: +//! 1. Use pre-defined fingerprints from the database +//! 2. Create a completely custom fingerprint +//! 3. Mix and match components from different fingerprints + +use impit::emulation::Browser; +use impit::fingerprint::database; +use impit::fingerprint::*; +use impit::impit::Impit; +use reqwest::cookie::Jar; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Example 1: Using pre-defined fingerprint with the old API (backward compatible) + println!("Example 1: Using Browser enum (backward compatible)"); + let impit1 = Impit::::builder() + .with_browser(Browser::Chrome) + .build()?; + + let response = impit1.get("https://httpbin.org/headers".to_string(), None, None).await?; + println!("Status: {}", response.status()); + println!("Response: {}\n", response.text().await?); + + // Example 2: Using pre-defined fingerprint with new API (explicit) + println!("Example 2: Using pre-defined Chrome fingerprint explicitly"); + let impit2 = Impit::::builder() + .with_fingerprint(database::chrome_125::fingerprint()) + .build()?; + + let response = impit2.get("https://httpbin.org/headers".to_string(), None, None).await?; + println!("Status: {}", response.status()); + println!("Response: {}\n", response.text().await?); + + // Example 3: Creating a completely custom fingerprint + println!("Example 3: Creating a custom fingerprint"); + + let custom_tls = TlsFingerprint::new( + // Minimal cipher suite list + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + ], + // Key exchange groups + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + ], + // Signature algorithms + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + ], + // TLS extensions + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate + false, // application_settings + vec![ + ExtensionType::ServerName, + ExtensionType::SupportedGroups, + ExtensionType::SignatureAlgorithms, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ], + ), + // ECH configuration + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ); + + let custom_http2 = Http2Fingerprint::new( + // Custom pseudo-header order + vec![ + ":method".to_string(), + ":path".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ], + // SETTINGS frame + Http2Settings::new( + Some(65536), + Some(false), + Some(1000), + Some(6291456), + Some(16384), + Some(262144), + vec![], + ), + // Window sizes + Http2WindowSize::new(15728640, 6291456), + // Priority + Some(Http2Priority::new(255, 0, false)), + ); + + let custom_headers = vec![ + ("user-agent".to_string(), "CustomBrowser/1.0".to_string()), + ("accept".to_string(), "text/html,application/xhtml+xml".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ]; + + let custom_fp = BrowserFingerprint::new( + "CustomBrowser", + "1.0", + custom_tls, + custom_http2, + custom_headers, + ); + + let impit3 = Impit::::builder() + .with_fingerprint(custom_fp) + .build()?; + + let response = impit3.get("https://httpbin.org/headers".to_string(), None, None).await?; + println!("Status: {}", response.status()); + println!("Response: {}\n", response.text().await?); + + // Example 4: Hybrid approach - use Chrome's TLS/HTTP2 but custom headers + println!("Example 4: Hybrid fingerprint (Chrome TLS/HTTP2 + custom headers)"); + + let base_fp = database::chrome_125::fingerprint(); + let custom_headers = vec![ + ("user-agent".to_string(), "MyApp/1.0 (Custom)".to_string()), + ("x-custom-header".to_string(), "custom-value".to_string()), + ]; + + let hybrid_fp = BrowserFingerprint::new( + "HybridBrowser", + "1.0", + base_fp.tls().clone(), // Reuse Chrome's TLS fingerprint + base_fp.http2().clone(), // Reuse Chrome's HTTP/2 fingerprint + custom_headers, // Custom headers + ); + + let impit4 = Impit::::builder() + .with_fingerprint(hybrid_fp) + .build()?; + + let response = impit4.get("https://httpbin.org/headers".to_string(), None, None).await?; + println!("Status: {}", response.status()); + println!("Response: {}", response.text().await?); + + Ok(()) +} diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs new file mode 100644 index 00000000..ab7e7c68 --- /dev/null +++ b/impit/src/fingerprint/database.rs @@ -0,0 +1,22 @@ +//! Pre-defined browser fingerprints +//! +//! This module contains fingerprint definitions for various browsers. + +mod chrome; +mod firefox; + +use crate::emulation::Browser; +use super::BrowserFingerprint; + +pub use chrome::chrome_125; +pub use firefox::firefox_128; + +/// Returns a fingerprint for the specified browser. +/// +/// This function provides a convenient way to get a fingerprint using the Browser enum. +pub fn get_fingerprint(browser: Browser) -> BrowserFingerprint { + match browser { + Browser::Chrome => chrome_125::fingerprint(), + Browser::Firefox => firefox_128::fingerprint(), + } +} diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs new file mode 100644 index 00000000..5c41cca3 --- /dev/null +++ b/impit/src/fingerprint/database/chrome.rs @@ -0,0 +1,149 @@ +//! 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 + vec![ + 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 + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + ], + // Signature algorithms + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + // 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 + // 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::new( + // Pseudo-header ordering + vec![ + ":method".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ], + // SETTINGS frame values + Http2Settings::new( + Some(65536), // header_table_size + Some(false), // enable_push + Some(1000), // max_concurrent_streams + Some(6291456), // initial_window_size + Some(16384), // max_frame_size + Some(262144), // max_header_list_size + vec![], // custom settings + ), + // Window sizes + Http2WindowSize::new( + 15728640, // connection_window_size + 6291456, // stream_window_size + ), + // Priority + Some(Http2Priority::new(255, 0, false)), + ) + } + + /// 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..c043caa0 --- /dev/null +++ b/impit/src/fingerprint/database/firefox.rs @@ -0,0 +1,147 @@ +//! 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 + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + 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, + 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 (Firefox includes FFDHE groups) + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Ffdhe2048, + KeyExchangeGroup::Ffdhe3072, + ], + // Signature algorithms + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::Ed25519, + ], + // 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 + None, // compress_certificate (Firefox doesn't use this) + false, // application_settings + // 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, + ], + ), + // 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::new( + // Pseudo-header ordering (Firefox uses different order than Chrome) + vec![ + ":method".to_string(), + ":path".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ], + // SETTINGS frame values + Http2Settings::new( + Some(65536), // header_table_size + Some(false), // enable_push + Some(1000), // max_concurrent_streams + Some(131072), // initial_window_size + Some(16384), // max_frame_size + Some(262144), // max_header_list_size + vec![], // custom settings + ), + // Window sizes + Http2WindowSize::new( + 12517377, // connection_window_size + 131072, // stream_window_size + ), + // Priority + Some(Http2Priority::new(200, 0, false)), + ) + } + + /// 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..8c5a3c23 --- /dev/null +++ b/impit/src/fingerprint/mod.rs @@ -0,0 +1,465 @@ +//! 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. + +mod types; +pub mod database; + +pub use types::*; + +/// A complete browser fingerprint containing TLS, HTTP/2, and HTTP header configurations. +/// +/// This struct is immutable after creation to ensure consistency and prevent +/// accidental modifications that could break fingerprint accuracy. +#[derive(Clone, Debug)] +pub struct BrowserFingerprint { + name: String, + version: String, + tls: TlsFingerprint, + http2: Http2Fingerprint, + headers: Vec<(String, String)>, +} + +impl BrowserFingerprint { + /// Creates a new browser fingerprint. + 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, + } + } + + /// Returns the browser name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the browser version. + pub fn version(&self) -> &str { + &self.version + } + + /// Returns the TLS fingerprint. + pub fn tls(&self) -> &TlsFingerprint { + &self.tls + } + + /// Returns the HTTP/2 fingerprint. + pub fn http2(&self) -> &Http2Fingerprint { + &self.http2 + } + + /// Returns the HTTP headers. + pub fn headers(&self) -> &[(String, String)] { + &self.headers + } +} + +/// TLS fingerprint configuration. +#[derive(Clone, Debug)] +pub struct TlsFingerprint { + /// Cipher suites in preference order + cipher_suites: Vec, + /// Supported key exchange groups in preference order + key_exchange_groups: Vec, + /// Signature algorithms in preference order + signature_algorithms: Vec, + /// TLS extensions configuration + extensions: TlsExtensions, + /// ECH (Encrypted Client Hello) configuration + ech_config: Option, + /// ALPN protocols in preference order + alpn_protocols: Vec>, +} + +impl TlsFingerprint { + /// Creates a new TLS fingerprint. + #[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, + } + } + + /// Returns the cipher suites. + pub fn cipher_suites(&self) -> &[CipherSuite] { + &self.cipher_suites + } + + /// Returns the key exchange groups. + pub fn key_exchange_groups(&self) -> &[KeyExchangeGroup] { + &self.key_exchange_groups + } + + /// Returns the signature algorithms. + pub fn signature_algorithms(&self) -> &[SignatureAlgorithm] { + &self.signature_algorithms + } + + /// Returns the TLS extensions configuration. + pub fn extensions(&self) -> &TlsExtensions { + &self.extensions + } + + /// Returns the ECH configuration. + pub fn ech_config(&self) -> Option<&EchConfig> { + self.ech_config.as_ref() + } + + /// Returns the ALPN protocols. + pub fn alpn_protocols(&self) -> &[Vec] { + &self.alpn_protocols + } +} + +/// HTTP/2 fingerprint configuration. +#[derive(Clone, Debug)] +pub struct Http2Fingerprint { + /// Pseudo-header ordering + pseudo_header_order: Vec, + /// SETTINGS frame values + settings: Http2Settings, + /// Initial window sizes + window_size: Http2WindowSize, + /// Priority configuration + priority: Option, +} + +impl Http2Fingerprint { + /// Creates a new HTTP/2 fingerprint. + pub fn new( + pseudo_header_order: Vec, + settings: Http2Settings, + window_size: Http2WindowSize, + priority: Option, + ) -> Self { + Self { + pseudo_header_order, + settings, + window_size, + priority, + } + } + + /// Returns the pseudo-header order. + pub fn pseudo_header_order(&self) -> &[String] { + &self.pseudo_header_order + } + + /// Returns the SETTINGS frame values. + pub fn settings(&self) -> &Http2Settings { + &self.settings + } + + /// Returns the window sizes. + pub fn window_size(&self) -> &Http2WindowSize { + &self.window_size + } + + /// Returns the priority configuration. + pub fn priority(&self) -> Option<&Http2Priority> { + self.priority.as_ref() + } +} + +/// HTTP/2 SETTINGS frame configuration. +#[derive(Clone, Debug)] +pub struct Http2Settings { + header_table_size: Option, + enable_push: Option, + max_concurrent_streams: Option, + initial_window_size: Option, + max_frame_size: Option, + max_header_list_size: Option, + /// Custom settings (non-standard settings IDs) + custom: Vec<(u16, u32)>, +} + +impl Http2Settings { + /// Creates a new HTTP/2 settings configuration. + #[allow(clippy::too_many_arguments)] + pub fn new( + header_table_size: Option, + enable_push: Option, + max_concurrent_streams: Option, + initial_window_size: Option, + max_frame_size: Option, + max_header_list_size: Option, + custom: Vec<(u16, u32)>, + ) -> Self { + Self { + header_table_size, + enable_push, + max_concurrent_streams, + initial_window_size, + max_frame_size, + max_header_list_size, + custom, + } + } + + /// Returns the header table size. + pub fn header_table_size(&self) -> Option { + self.header_table_size + } + + /// Returns whether push is enabled. + pub fn enable_push(&self) -> Option { + self.enable_push + } + + /// Returns the maximum concurrent streams. + pub fn max_concurrent_streams(&self) -> Option { + self.max_concurrent_streams + } + + /// Returns the initial window size. + pub fn initial_window_size(&self) -> Option { + self.initial_window_size + } + + /// Returns the maximum frame size. + pub fn max_frame_size(&self) -> Option { + self.max_frame_size + } + + /// Returns the maximum header list size. + pub fn max_header_list_size(&self) -> Option { + self.max_header_list_size + } + + /// Returns the custom settings. + pub fn custom(&self) -> &[(u16, u32)] { + &self.custom + } +} + +/// HTTP/2 window size configuration. +#[derive(Clone, Copy, Debug)] +pub struct Http2WindowSize { + connection_window_size: u32, + stream_window_size: u32, +} + +impl Http2WindowSize { + /// Creates a new window size configuration. + pub fn new(connection_window_size: u32, stream_window_size: u32) -> Self { + Self { + connection_window_size, + stream_window_size, + } + } + + /// Returns the connection window size. + pub fn connection_window_size(&self) -> u32 { + self.connection_window_size + } + + /// Returns the stream window size. + pub fn stream_window_size(&self) -> u32 { + self.stream_window_size + } +} + +/// HTTP/2 priority configuration. +#[derive(Clone, Copy, Debug)] +pub struct Http2Priority { + weight: u8, + depends_on: u32, + exclusive: bool, +} + +impl Http2Priority { + /// Creates a new priority configuration. + pub fn new(weight: u8, depends_on: u32, exclusive: bool) -> Self { + Self { + weight, + depends_on, + exclusive, + } + } + + /// Returns the priority weight. + pub fn weight(&self) -> u8 { + self.weight + } + + /// Returns the stream this depends on. + pub fn depends_on(&self) -> u32 { + self.depends_on + } + + /// Returns whether this dependency is exclusive. + pub fn exclusive(&self) -> bool { + self.exclusive + } +} + +/// TLS extensions configuration. +#[derive(Clone, Debug)] +pub struct TlsExtensions { + 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, + /// Extension order matters for fingerprinting + 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, + 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, + extension_order, + } + } + + /// Returns whether server_name extension is enabled. + pub fn server_name(&self) -> bool { + self.server_name + } + + /// Returns whether status_request extension is enabled. + pub fn status_request(&self) -> bool { + self.status_request + } + + /// Returns whether supported_groups extension is enabled. + pub fn supported_groups(&self) -> bool { + self.supported_groups + } + + /// Returns whether signature_algorithms extension is enabled. + pub fn signature_algorithms(&self) -> bool { + self.signature_algorithms + } + + /// Returns whether ALPN extension is enabled. + pub fn application_layer_protocol_negotiation(&self) -> bool { + self.application_layer_protocol_negotiation + } + + /// Returns whether signed_certificate_timestamp extension is enabled. + pub fn signed_certificate_timestamp(&self) -> bool { + self.signed_certificate_timestamp + } + + /// Returns whether key_share extension is enabled. + pub fn key_share(&self) -> bool { + self.key_share + } + + /// Returns whether psk_key_exchange_modes extension is enabled. + pub fn psk_key_exchange_modes(&self) -> bool { + self.psk_key_exchange_modes + } + + /// Returns whether supported_versions extension is enabled. + pub fn supported_versions(&self) -> bool { + self.supported_versions + } + + /// Returns the certificate compression algorithms. + pub fn compress_certificate(&self) -> Option<&[CertificateCompressionAlgorithm]> { + self.compress_certificate.as_deref() + } + + /// Returns whether application_settings extension is enabled. + pub fn application_settings(&self) -> bool { + self.application_settings + } + + /// Returns the extension order. + pub fn extension_order(&self) -> &[ExtensionType] { + &self.extension_order + } +} + +/// ECH (Encrypted Client Hello) configuration. +#[derive(Clone, Debug)] +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)] +pub enum EchMode { + /// ECH is disabled + Disabled, + /// ECH GREASE mode with specified HPKE suite + Grease { hpke_suite: HpkeKemId }, + /// Real ECH with actual configuration + Real, +} diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs new file mode 100644 index 00000000..1f2671e2 --- /dev/null +++ b/impit/src/fingerprint/types.rs @@ -0,0 +1,112 @@ +//! 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)] +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_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, +} + +/// Key exchange groups for TLS +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KeyExchangeGroup { + X25519, + Secp256r1, + Secp384r1, + Secp521r1, + Ffdhe2048, + Ffdhe3072, + Ffdhe4096, + Ffdhe6144, + Ffdhe8192, +} + +/// Signature algorithms for TLS +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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, +} + +/// TLS extension types +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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)] +pub enum CertificateCompressionAlgorithm { + Zlib, + Brotli, + Zstd, +} + +/// HPKE KEM identifiers for ECH +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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..7ac1c775 100644 --- a/impit/src/http_headers/mod.rs +++ b/impit/src/http_headers/mod.rs @@ -1,4 +1,4 @@ -use crate::{emulation::Browser, errors::ImpitError}; +use crate::{emulation::Browser, errors::ImpitError, fingerprint::BrowserFingerprint}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use std::{collections::HashSet, str::FromStr}; @@ -22,29 +22,39 @@ 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 { + match self.context.browser { + Some(Browser::Chrome) => statics::CHROME_HEADERS + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + Some(Browser::Firefox) => statics::FIREFOX_HEADERS + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + None => 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(); custom_headers - .chain(impersonated_headers) + .chain(impersonated_headers.into_iter()) .filter_map(move |(name, value)| { if used_header_names.contains(&name.to_lowercase()) { None } else { used_header_names.insert(name.to_lowercase()); - Some((name.to_string(), value.to_string())) + Some((name, value)) } }) } @@ -86,6 +96,7 @@ impl From for Result { pub struct HttpHeadersBuilder { host: String, browser: Option, + fingerprint: Option, https: bool, custom_headers: Vec<(String, String)>, } @@ -102,6 +113,11 @@ impl HttpHeadersBuilder { self } + pub fn with_fingerprint(&mut self, fingerprint: &Option) -> &mut Self { + self.fingerprint = fingerprint.clone(); + self + } + pub fn with_https(&mut self, https: bool) -> &mut Self { self.https = https; self diff --git a/impit/src/impit.rs b/impit/src/impit.rs index dc196cbf..82447f84 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -8,6 +8,7 @@ use url::Url; use crate::{ emulation::Browser, errors::{ErrorContext, ImpitError}, + fingerprint::BrowserFingerprint, http3::H3Engine, http_headers::{statics, HttpHeaders}, request::{ImpitRequest, RequestOptions}, @@ -75,6 +76,7 @@ pub enum RedirectBehavior { #[derive(Debug)] pub struct ImpitBuilder { browser: Option, + fingerprint: Option, ignore_tls_errors: bool, vanilla_fallback: bool, proxy_url: String, @@ -90,6 +92,7 @@ impl Clone for ImpitBuilder 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(), @@ -107,6 +110,7 @@ impl Default for ImpitBuilder Self { ImpitBuilder { browser: None, + fingerprint: None, ignore_tls_errors: false, vanilla_fallback: true, proxy_url: String::new(), @@ -131,6 +135,17 @@ impl ImpitBuilder { self } + /// Sets a complete browser fingerprint. + /// + /// 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. + /// + /// 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 + } + /// If set to true, the client will ignore TLS-related errors. pub fn with_ignore_tls_errors(mut self, ignore_tls_errors: bool) -> Self { self.ignore_tls_errors = ignore_tls_errors; @@ -234,13 +249,19 @@ 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()); + } else { + tls_config_builder.with_browser(config.browser); + } 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 +317,21 @@ 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 { + match config.browser { + Some(Browser::Chrome) => statics::CHROME_PSEUDOHEADERS_ORDER + .iter() + .map(|s| s.to_string()) + .collect(), + Some(Browser::Firefox) => statics::FIREFOX_PSEUDOHEADERS_ORDER + .iter() + .map(|s| s.to_string()) + .collect(), + None => vec![], + } }; if !pseudo_headers_order.is_empty() { @@ -370,6 +402,7 @@ impl Impit { 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..249473ae 100644 --- a/impit/src/lib.rs +++ b/impit/src/lib.rs @@ -75,6 +75,9 @@ pub mod request; /// Errors and error handling. pub mod errors; +/// Browser fingerprint definitions and types. +pub mod fingerprint; + /// Contains browser emulation-related types and functions. pub mod emulation { diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index 57196b3c..59557aa8 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -4,6 +4,7 @@ mod statics; use std::sync::Arc; use crate::emulation::Browser; +use crate::fingerprint::{self, TlsFingerprint}; use reqwest::Version; use rustls::client::danger::NoVerifier; use rustls::client::{BrowserEmulator as RusTLSBrowser, BrowserType, EchGreaseConfig}; @@ -19,9 +20,10 @@ 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, } @@ -30,24 +32,32 @@ 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(); +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() +} - EchGreaseConfig::new(statics::GREASE_HPKE_SUITE, public_key).into() - } +impl TlsConfigBuilder { pub fn with_browser(&mut self, browser: Option) -> &mut Self { self.browser = browser; self } + /// Sets the TLS fingerprint directly. + /// This takes precedence over the browser parameter. + pub fn with_tls_fingerprint(&mut self, fingerprint: TlsFingerprint) -> &mut Self { + self.tls_fingerprint = Some(fingerprint); + self + } + pub fn with_http3(&mut self) -> &mut Self { self.max_http_version = Version::HTTP_3; self @@ -59,9 +69,30 @@ impl TlsConfigBuilder { } pub fn build(self) -> rustls::ClientConfig { - let mut config = match self.browser { - Some(browser) => { - let rustls_browser = match browser { + // Save fields before consuming self + let ignore_tls_errors = self.ignore_tls_errors; + let max_http_version = self.max_http_version; + let browser = self.browser; + + // Determine which fingerprint to use + let fingerprint = if let Some(fp) = self.tls_fingerprint { + // Use provided fingerprint directly + Some(fp) + } else if let Some(browser) = browser { + // Fall back to looking up fingerprint from browser enum + Some(fingerprint::database::get_fingerprint(browser).tls().clone()) + } else { + None + }; + + let mut config = if let Some(_fp) = fingerprint { + // TODO: Use fingerprint data to configure rustls + // For now, fall back to the old browser-based approach + // This requires updates to the patched rustls library to expose configuration APIs + + // Temporary: Use old approach if browser is set + if let Some(browser_val) = browser { + let rustls_browser = match browser_val { Browser::Chrome => RusTLSBrowser { browser_type: BrowserType::Chrome, version: 125, @@ -76,7 +107,7 @@ impl TlsConfigBuilder { .with_browser_emulator(&rustls_browser) .build(); - if browser == Browser::Firefox { + if browser_val == Browser::Firefox { crypto_provider.kx_groups = vec![ X25519, SECP256R1, @@ -99,25 +130,24 @@ impl TlsConfigBuilder { 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()) + .with_ech(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 { + if ignore_tls_errors { config .dangerous() .set_certificate_verifier(Arc::new(NoVerifier::new(Some(rustls_browser)))); } config - } - None => { + } else { + // No browser set, use default config 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(), @@ -126,14 +156,13 @@ impl TlsConfigBuilder { let mut config: rustls::ClientConfig = rustls::ClientConfig::builder_with_provider(crypto_provider) - // TODO - use the ECH extension consistently - .with_ech(self.get_ech_mode()) + .with_ech(get_ech_mode()) .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth(); - if self.ignore_tls_errors { + if ignore_tls_errors { config .dangerous() .set_certificate_verifier(Arc::new(NoVerifier::new(None))); @@ -141,9 +170,34 @@ impl TlsConfigBuilder { config } + } else { + // No fingerprint or browser set, use vanilla config + let crypto_provider: Arc = CryptoProvider::builder().build().into(); + + 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) + .with_ech(get_ech_mode()) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth(); + + if ignore_tls_errors { + config + .dangerous() + .set_certificate_verifier(Arc::new(NoVerifier::new(None))); + } + + config }; - if self.max_http_version == Version::HTTP_3 { + if max_http_version == Version::HTTP_3 { config.alpn_protocols = vec![b"h3".to_vec()]; }; From c3c2855f90dbf81f81b9ffdf961c8d2f257b5d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 26 Jan 2026 13:23:07 +0100 Subject: [PATCH 2/8] fix: make new TLS fingerprints work correctly with `rustls` --- Cargo.lock | 30 ---- Cargo.toml | 2 +- impit-node/test/fingerprints.test.ts | 16 +- impit/Cargo.toml | 1 - impit/examples/custom_fingerprint.rs | 39 +++-- impit/src/fingerprint/database.rs | 2 +- impit/src/fingerprint/database/chrome.rs | 45 +++--- impit/src/fingerprint/database/firefox.rs | 70 +++++---- impit/src/fingerprint/mod.rs | 182 +++++++++++++++++++++- impit/src/fingerprint/types.rs | 9 ++ impit/src/http_headers/mod.rs | 33 ++-- impit/src/tls/ffdhe.rs | 117 -------------- impit/src/tls/mod.rs | 133 +++++----------- 13 files changed, 356 insertions(+), 323 deletions(-) delete mode 100644 impit/src/tls/ffdhe.rs diff --git a/Cargo.lock b/Cargo.lock index 5c6c9394..782c5032 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,6 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.36" -source = "git+https://github.com/apify/rustls?branch=impit-main#c5338dd72cdccbfdb717aeb218fd05c60e3dd139" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index e8131aa0..8f4505bf 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", branch="impit-main" } +rustls = { path = "../rustls/rustls" } [profile.release] strip = true # Automatically strip symbols from the binary. 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/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/custom_fingerprint.rs b/impit/examples/custom_fingerprint.rs index 0d4b58e8..df29418a 100644 --- a/impit/examples/custom_fingerprint.rs +++ b/impit/examples/custom_fingerprint.rs @@ -19,7 +19,9 @@ async fn main() -> Result<(), Box> { .with_browser(Browser::Chrome) .build()?; - let response = impit1.get("https://httpbin.org/headers".to_string(), None, None).await?; + let response = impit1 + .get("https://httpbin.org/headers".to_string(), None, None) + .await?; println!("Status: {}", response.status()); println!("Response: {}\n", response.text().await?); @@ -29,7 +31,9 @@ async fn main() -> Result<(), Box> { .with_fingerprint(database::chrome_125::fingerprint()) .build()?; - let response = impit2.get("https://httpbin.org/headers".to_string(), None, None).await?; + let response = impit2 + .get("https://httpbin.org/headers".to_string(), None, None) + .await?; println!("Status: {}", response.status()); println!("Response: {}\n", response.text().await?); @@ -43,10 +47,7 @@ async fn main() -> Result<(), Box> { CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, ], // Key exchange groups - vec![ - KeyExchangeGroup::X25519, - KeyExchangeGroup::Secp256r1, - ], + vec![KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1], // Signature algorithms vec![ SignatureAlgorithm::EcdsaSecp256r1Sha256, @@ -65,6 +66,8 @@ async fn main() -> Result<(), Box> { true, // supported_versions None, // compress_certificate false, // application_settings + false, // delegated_credentials + None, // record_size_limit vec![ ExtensionType::ServerName, ExtensionType::SupportedGroups, @@ -111,8 +114,14 @@ async fn main() -> Result<(), Box> { let custom_headers = vec![ ("user-agent".to_string(), "CustomBrowser/1.0".to_string()), - ("accept".to_string(), "text/html,application/xhtml+xml".to_string()), - ("accept-encoding".to_string(), "gzip, deflate, br".to_string()), + ( + "accept".to_string(), + "text/html,application/xhtml+xml".to_string(), + ), + ( + "accept-encoding".to_string(), + "gzip, deflate, br".to_string(), + ), ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), ]; @@ -128,7 +137,9 @@ async fn main() -> Result<(), Box> { .with_fingerprint(custom_fp) .build()?; - let response = impit3.get("https://httpbin.org/headers".to_string(), None, None).await?; + let response = impit3 + .get("https://httpbin.org/headers".to_string(), None, None) + .await?; println!("Status: {}", response.status()); println!("Response: {}\n", response.text().await?); @@ -144,16 +155,18 @@ async fn main() -> Result<(), Box> { let hybrid_fp = BrowserFingerprint::new( "HybridBrowser", "1.0", - base_fp.tls().clone(), // Reuse Chrome's TLS fingerprint - base_fp.http2().clone(), // Reuse Chrome's HTTP/2 fingerprint - custom_headers, // Custom headers + base_fp.tls().clone(), // Reuse Chrome's TLS fingerprint + base_fp.http2().clone(), // Reuse Chrome's HTTP/2 fingerprint + custom_headers, // Custom headers ); let impit4 = Impit::::builder() .with_fingerprint(hybrid_fp) .build()?; - let response = impit4.get("https://httpbin.org/headers".to_string(), None, None).await?; + let response = impit4 + .get("https://httpbin.org/headers".to_string(), None, None) + .await?; println!("Status: {}", response.status()); println!("Response: {}", response.text().await?); diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index ab7e7c68..ec27a391 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -5,8 +5,8 @@ mod chrome; mod firefox; -use crate::emulation::Browser; use super::BrowserFingerprint; +use crate::emulation::Browser; 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 index 5c41cca3..71d091f2 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -20,8 +20,9 @@ pub mod chrome_125 { /// Chrome 125 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( - // Cipher suites in Chrome 125 preference order + // 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, @@ -38,38 +39,40 @@ pub mod chrome_125 { CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, ], - // Key exchange groups + // Key exchange groups (GREASE at the end for Chrome fingerprint) vec![ KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Grease, ], - // Signature algorithms + // Signature algorithms - order must match DEFAULT_SIGNATURE_VERIFICATION_ALGOS + // Note: No SHA1 algorithms for Chrome (matches original implementation) vec![ SignatureAlgorithm::EcdsaSecp256r1Sha256, SignatureAlgorithm::RsaPssRsaSha256, - SignatureAlgorithm::RsaPssRsaSha384, - SignatureAlgorithm::RsaPssRsaSha512, SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, SignatureAlgorithm::RsaPkcs1Sha512, - SignatureAlgorithm::EcdsaSecp384r1Sha384, - SignatureAlgorithm::EcdsaSecp521r1Sha512, - SignatureAlgorithm::RsaPkcs1Sha1, ], // 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 + 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, @@ -121,8 +124,8 @@ pub mod chrome_125 { ), // Window sizes Http2WindowSize::new( - 15728640, // connection_window_size - 6291456, // stream_window_size + 15728640, // connection_window_size + 6291456, // stream_window_size ), // Priority Some(Http2Priority::new(255, 0, false)), diff --git a/impit/src/fingerprint/database/firefox.rs b/impit/src/fingerprint/database/firefox.rs index c043caa0..e38d5614 100644 --- a/impit/src/fingerprint/database/firefox.rs +++ b/impit/src/fingerprint/database/firefox.rs @@ -20,23 +20,29 @@ pub mod firefox_128 { /// Firefox 128 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( - // Cipher suites in Firefox 128 preference order + // 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, - 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, + // 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![ @@ -46,7 +52,8 @@ pub mod firefox_128 { KeyExchangeGroup::Ffdhe2048, KeyExchangeGroup::Ffdhe3072, ], - // Signature algorithms + // 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, @@ -57,22 +64,26 @@ pub mod firefox_128 { SignatureAlgorithm::RsaPkcs1Sha256, SignatureAlgorithm::RsaPkcs1Sha384, SignatureAlgorithm::RsaPkcs1Sha512, - SignatureAlgorithm::Ed25519, + 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 - true, // signed_certificate_timestamp - true, // key_share - true, // psk_key_exchange_modes - true, // supported_versions - None, // compress_certificate (Firefox doesn't use this) - false, // application_settings + 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, @@ -81,7 +92,6 @@ pub mod firefox_128 { ExtensionType::StatusRequest, ExtensionType::SupportedGroups, ExtensionType::ApplicationLayerProtocolNegotiation, - ExtensionType::SignedCertificateTimestamp, ExtensionType::KeyShare, ExtensionType::PskKeyExchangeModes, ExtensionType::SupportedVersions, @@ -111,18 +121,18 @@ pub mod firefox_128 { ], // SETTINGS frame values Http2Settings::new( - Some(65536), // header_table_size - Some(false), // enable_push - Some(1000), // max_concurrent_streams - Some(131072), // initial_window_size - Some(16384), // max_frame_size - Some(262144), // max_header_list_size - vec![], // custom settings + Some(65536), // header_table_size + Some(false), // enable_push + Some(1000), // max_concurrent_streams + Some(131072), // initial_window_size + Some(16384), // max_frame_size + Some(262144), // max_header_list_size + vec![], // custom settings ), // Window sizes Http2WindowSize::new( - 12517377, // connection_window_size - 131072, // stream_window_size + 12517377, // connection_window_size + 131072, // stream_window_size ), // Priority Some(Http2Priority::new(200, 0, false)), diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index 8c5a3c23..2bdfbceb 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -3,8 +3,8 @@ //! This module contains all the types needed to define a complete browser fingerprint, //! including TLS, HTTP/2, and HTTP header configurations. -mod types; pub mod database; +mod types; pub use types::*; @@ -331,6 +331,10 @@ pub struct TlsExtensions { supported_versions: bool, compress_certificate: Option>, application_settings: bool, + /// Delegated credentials extension (Firefox-specific) + delegated_credentials: bool, + /// Record size limit extension (Firefox-specific) + record_size_limit: Option, /// Extension order matters for fingerprinting extension_order: Vec, } @@ -350,6 +354,8 @@ impl TlsExtensions { supported_versions: bool, compress_certificate: Option>, application_settings: bool, + delegated_credentials: bool, + record_size_limit: Option, extension_order: Vec, ) -> Self { Self { @@ -364,6 +370,8 @@ impl TlsExtensions { supported_versions, compress_certificate, application_settings, + delegated_credentials, + record_size_limit, extension_order, } } @@ -423,6 +431,16 @@ impl TlsExtensions { self.application_settings } + /// Returns whether delegated_credentials extension is enabled. + pub fn delegated_credentials(&self) -> bool { + self.delegated_credentials + } + + /// Returns the record size limit if set. + pub fn record_size_limit(&self) -> Option { + self.record_size_limit + } + /// Returns the extension order. pub fn extension_order(&self) -> &[ExtensionType] { &self.extension_order @@ -463,3 +481,165 @@ pub enum EchMode { /// 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().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 index 1f2671e2..6d18b495 100644 --- a/impit/src/fingerprint/types.rs +++ b/impit/src/fingerprint/types.rs @@ -1,3 +1,4 @@ +#![allow(non_camel_case_types)] //! Type definitions for browser fingerprints //! //! This module contains enum types used to configure TLS and HTTP/2 fingerprints @@ -19,10 +20,14 @@ pub enum CipherSuite { 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 @@ -37,6 +42,8 @@ pub enum KeyExchangeGroup { Ffdhe4096, Ffdhe6144, Ffdhe8192, + /// GREASE key exchange group for fingerprinting + Grease, } /// Signature algorithms for TLS @@ -58,6 +65,8 @@ pub enum SignatureAlgorithm { // EdDSA algorithms Ed25519, Ed448, + // Legacy ECDSA with SHA-1 (for backwards compatibility) + EcdsaSha1Legacy, } /// TLS extension types diff --git a/impit/src/http_headers/mod.rs b/impit/src/http_headers/mod.rs index 7ac1c775..a4eb37a3 100644 --- a/impit/src/http_headers/mod.rs +++ b/impit/src/http_headers/mod.rs @@ -23,21 +23,22 @@ impl HttpHeaders { impl HttpHeaders { pub fn iter(&self) -> impl Iterator + '_ { // 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 { - match self.context.browser { - Some(Browser::Chrome) => statics::CHROME_HEADERS - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - Some(Browser::Firefox) => statics::FIREFOX_HEADERS - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - None => vec![], - } - }; + let impersonated_headers: Vec<(String, String)> = + if let Some(ref fp) = self.context.fingerprint { + fp.headers().to_vec() + } else { + match self.context.browser { + Some(Browser::Chrome) => statics::CHROME_HEADERS + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + Some(Browser::Firefox) => statics::FIREFOX_HEADERS + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + None => vec![], + } + }; let custom_headers = self .context @@ -48,7 +49,7 @@ impl HttpHeaders { let mut used_header_names: HashSet = HashSet::new(); custom_headers - .chain(impersonated_headers.into_iter()) + .chain(impersonated_headers) .filter_map(move |(name, value)| { if used_header_names.contains(&name.to_lowercase()) { None 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 59557aa8..0fbc1b7c 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -1,4 +1,3 @@ -mod ffdhe; mod statics; use std::sync::Arc; @@ -7,8 +6,7 @@ use crate::emulation::Browser; use crate::fingerprint::{self, 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; @@ -45,7 +43,6 @@ fn get_ech_mode() -> rustls::client::EchMode { } impl TlsConfigBuilder { - pub fn with_browser(&mut self, browser: Option) -> &mut Self { self.browser = browser; self @@ -78,98 +75,54 @@ impl TlsConfigBuilder { let fingerprint = if let Some(fp) = self.tls_fingerprint { // Use provided fingerprint directly Some(fp) - } else if let Some(browser) = browser { - // Fall back to looking up fingerprint from browser enum - Some(fingerprint::database::get_fingerprint(browser).tls().clone()) } else { - None + browser.map(|browser| { + fingerprint::database::get_fingerprint(browser) + .tls() + .clone() + }) }; - let mut config = if let Some(_fp) = fingerprint { - // TODO: Use fingerprint data to configure rustls - // For now, fall back to the old browser-based approach - // This requires updates to the patched rustls library to expose configuration APIs - - // Temporary: Use old approach if browser is set - if let Some(browser_val) = browser { - let rustls_browser = match browser_val { - 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_val == 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(get_ech_mode()) - .unwrap() - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_browser_emulator(&rustls_browser) - .with_no_client_auth(); - - if ignore_tls_errors { - config - .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier::new(Some(rustls_browser)))); - } + let mut config = if let Some(fp) = fingerprint { + // Convert impit fingerprint to rustls fingerprint + let rustls_fingerprint = fp.to_rustls_fingerprint(); - config - } else { - // No browser set, use default config - let crypto_provider: Arc = CryptoProvider::builder().build().into(); - - 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) - .with_ech(get_ech_mode()) - .unwrap() - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_no_client_auth(); - - if ignore_tls_errors { - config - .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier::new(None))); - } + // Save ALPN protocols from the fingerprint before conversion + let alpn_protocols = fp.alpn_protocols().to_vec(); + + // Build crypto provider with the fingerprint + let crypto_provider = CryptoProvider::builder() + .with_tls_fingerprint(rustls_fingerprint.clone()) + .build(); + 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) + .with_ech(get_ech_mode()) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_tls_fingerprint(rustls_fingerprint) + .with_no_client_auth(); + + // Set ALPN protocols from the fingerprint + config.alpn_protocols = alpn_protocols; + + if ignore_tls_errors { config + .dangerous() + .set_certificate_verifier(Arc::new(NoVerifier::with_default_schemes())); } + + config } else { // No fingerprint or browser set, use vanilla config let crypto_provider: Arc = CryptoProvider::builder().build().into(); @@ -191,7 +144,7 @@ impl TlsConfigBuilder { if ignore_tls_errors { config .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier::new(None))); + .set_certificate_verifier(Arc::new(NoVerifier::with_default_schemes())); } config From f0d3280856c032724b69e4a3be7b796561a8de88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 12:03:07 +0100 Subject: [PATCH 3/8] fix: cache `CryptoProvider`s to improve performance --- impit/src/lib.rs | 2 +- impit/src/tls/mod.rs | 134 ++++++++++++++++++++++++++++++------------- 2 files changed, 96 insertions(+), 40 deletions(-) diff --git a/impit/src/lib.rs b/impit/src/lib.rs index 249473ae..78a5c5a6 100644 --- a/impit/src/lib.rs +++ b/impit/src/lib.rs @@ -85,7 +85,7 @@ pub mod emulation { /// /// 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)] + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default)] pub enum Browser { #[default] Chrome, diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index 0fbc1b7c..a986bd6e 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -1,6 +1,7 @@ mod statics; -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; use crate::emulation::Browser; use crate::fingerprint::{self, TlsFingerprint}; @@ -10,6 +11,71 @@ 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(browser: Browser) -> BrowserCacheValue { + { + let cache = get_browser_cache().lock().unwrap(); + if let Some(cached) = cache.get(&browser) { + return cached.clone(); + } + } + + let fp = fingerprint::database::get_fingerprint(browser) + .tls() + .clone(); + let rustls_fp = fp.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(browser, (provider.clone(), verifier.clone())); + } + + (provider, verifier) +} + pub struct TlsConfig {} impl TlsConfig { @@ -48,8 +114,6 @@ impl TlsConfigBuilder { self } - /// Sets the TLS fingerprint directly. - /// This takes precedence over the browser parameter. pub fn with_tls_fingerprint(&mut self, fingerprint: TlsFingerprint) -> &mut Self { self.tls_fingerprint = Some(fingerprint); self @@ -66,54 +130,52 @@ impl TlsConfigBuilder { } pub fn build(self) -> rustls::ClientConfig { - // Save fields before consuming self let ignore_tls_errors = self.ignore_tls_errors; let max_http_version = self.max_http_version; let browser = self.browser; - // Determine which fingerprint to use - let fingerprint = if let Some(fp) = self.tls_fingerprint { - // Use provided fingerprint directly - Some(fp) + let (fingerprint, cache_browser) = if let Some(fp) = self.tls_fingerprint { + (Some(fp), None) + } else if let Some(b) = browser { + let fp = fingerprint::database::get_fingerprint(b).tls().clone(); + (Some(fp), Some(b)) } else { - browser.map(|browser| { - fingerprint::database::get_fingerprint(browser) - .tls() - .clone() - }) + (None, None) }; let mut config = if let Some(fp) = fingerprint { - // Convert impit fingerprint to rustls fingerprint let rustls_fingerprint = fp.to_rustls_fingerprint(); - // Save ALPN protocols from the fingerprint before conversion let alpn_protocols = fp.alpn_protocols().to_vec(); - // Build crypto provider with the fingerprint - let crypto_provider = CryptoProvider::builder() - .with_tls_fingerprint(rustls_fingerprint.clone()) - .build(); - - 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 (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(Arc::new(verifier)) + .with_custom_certificate_verifier(verifier) .with_tls_fingerprint(rustls_fingerprint) .with_no_client_auth(); - // Set ALPN protocols from the fingerprint config.alpn_protocols = alpn_protocols; if ignore_tls_errors { @@ -124,21 +186,15 @@ impl TlsConfigBuilder { config } else { - // No fingerprint or browser set, use vanilla config - let crypto_provider: Arc = CryptoProvider::builder().build().into(); - - 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 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(Arc::new(verifier)) + .with_custom_certificate_verifier(verifier) .with_no_client_auth(); if ignore_tls_errors { From a6eec7d4186ec30693122243a35d5a9f79dd0f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 12:50:14 +0100 Subject: [PATCH 4/8] chore: update `rustls` github patch rev --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 782c5032..235b7f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2556,6 +2556,7 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.36" +source = "git+https://github.com/apify/rustls?rev=8c46c4744be711e87946d967301c8dac648f4049#8c46c4744be711e87946d967301c8dac648f4049" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index 8f4505bf..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 = { path = "../rustls/rustls" } +rustls = { git = "https://github.com/apify/rustls", rev="8c46c4744be711e87946d967301c8dac648f4049" } [profile.release] strip = true # Automatically strip symbols from the binary. From a7b6d0b45ed164f94f42c5692929be0814c2090c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 13:34:35 +0100 Subject: [PATCH 5/8] chore: remove unused parts --- impit/examples/custom_fingerprint.rs | 174 ---------- impit/src/fingerprint/database/chrome.rs | 26 +- impit/src/fingerprint/database/firefox.rs | 26 +- impit/src/fingerprint/mod.rs | 382 ++-------------------- impit/src/http_headers/mod.rs | 2 +- impit/src/impit.rs | 4 +- impit/src/tls/mod.rs | 6 +- 7 files changed, 46 insertions(+), 574 deletions(-) delete mode 100644 impit/examples/custom_fingerprint.rs diff --git a/impit/examples/custom_fingerprint.rs b/impit/examples/custom_fingerprint.rs deleted file mode 100644 index df29418a..00000000 --- a/impit/examples/custom_fingerprint.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Example demonstrating custom browser fingerprint usage -//! -//! This example shows how to: -//! 1. Use pre-defined fingerprints from the database -//! 2. Create a completely custom fingerprint -//! 3. Mix and match components from different fingerprints - -use impit::emulation::Browser; -use impit::fingerprint::database; -use impit::fingerprint::*; -use impit::impit::Impit; -use reqwest::cookie::Jar; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Example 1: Using pre-defined fingerprint with the old API (backward compatible) - println!("Example 1: Using Browser enum (backward compatible)"); - let impit1 = Impit::::builder() - .with_browser(Browser::Chrome) - .build()?; - - let response = impit1 - .get("https://httpbin.org/headers".to_string(), None, None) - .await?; - println!("Status: {}", response.status()); - println!("Response: {}\n", response.text().await?); - - // Example 2: Using pre-defined fingerprint with new API (explicit) - println!("Example 2: Using pre-defined Chrome fingerprint explicitly"); - let impit2 = Impit::::builder() - .with_fingerprint(database::chrome_125::fingerprint()) - .build()?; - - let response = impit2 - .get("https://httpbin.org/headers".to_string(), None, None) - .await?; - println!("Status: {}", response.status()); - println!("Response: {}\n", response.text().await?); - - // Example 3: Creating a completely custom fingerprint - println!("Example 3: Creating a custom fingerprint"); - - let custom_tls = TlsFingerprint::new( - // Minimal cipher suite list - vec![ - CipherSuite::TLS13_AES_128_GCM_SHA256, - CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, - ], - // Key exchange groups - vec![KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1], - // Signature algorithms - vec![ - SignatureAlgorithm::EcdsaSecp256r1Sha256, - SignatureAlgorithm::RsaPssRsaSha256, - ], - // TLS extensions - TlsExtensions::new( - true, // server_name - true, // status_request - true, // supported_groups - true, // signature_algorithms - true, // application_layer_protocol_negotiation - false, // signed_certificate_timestamp - true, // key_share - true, // psk_key_exchange_modes - true, // supported_versions - None, // compress_certificate - false, // application_settings - false, // delegated_credentials - None, // record_size_limit - vec![ - ExtensionType::ServerName, - ExtensionType::SupportedGroups, - ExtensionType::SignatureAlgorithms, - ExtensionType::KeyShare, - ExtensionType::PskKeyExchangeModes, - ExtensionType::SupportedVersions, - ], - ), - // ECH configuration - Some(EchConfig::new( - EchMode::Grease { - hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, - }, - None, - )), - // ALPN protocols - vec![b"h2".to_vec(), b"http/1.1".to_vec()], - ); - - let custom_http2 = Http2Fingerprint::new( - // Custom pseudo-header order - vec![ - ":method".to_string(), - ":path".to_string(), - ":authority".to_string(), - ":scheme".to_string(), - ], - // SETTINGS frame - Http2Settings::new( - Some(65536), - Some(false), - Some(1000), - Some(6291456), - Some(16384), - Some(262144), - vec![], - ), - // Window sizes - Http2WindowSize::new(15728640, 6291456), - // Priority - Some(Http2Priority::new(255, 0, false)), - ); - - let custom_headers = vec![ - ("user-agent".to_string(), "CustomBrowser/1.0".to_string()), - ( - "accept".to_string(), - "text/html,application/xhtml+xml".to_string(), - ), - ( - "accept-encoding".to_string(), - "gzip, deflate, br".to_string(), - ), - ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), - ]; - - let custom_fp = BrowserFingerprint::new( - "CustomBrowser", - "1.0", - custom_tls, - custom_http2, - custom_headers, - ); - - let impit3 = Impit::::builder() - .with_fingerprint(custom_fp) - .build()?; - - let response = impit3 - .get("https://httpbin.org/headers".to_string(), None, None) - .await?; - println!("Status: {}", response.status()); - println!("Response: {}\n", response.text().await?); - - // Example 4: Hybrid approach - use Chrome's TLS/HTTP2 but custom headers - println!("Example 4: Hybrid fingerprint (Chrome TLS/HTTP2 + custom headers)"); - - let base_fp = database::chrome_125::fingerprint(); - let custom_headers = vec![ - ("user-agent".to_string(), "MyApp/1.0 (Custom)".to_string()), - ("x-custom-header".to_string(), "custom-value".to_string()), - ]; - - let hybrid_fp = BrowserFingerprint::new( - "HybridBrowser", - "1.0", - base_fp.tls().clone(), // Reuse Chrome's TLS fingerprint - base_fp.http2().clone(), // Reuse Chrome's HTTP/2 fingerprint - custom_headers, // Custom headers - ); - - let impit4 = Impit::::builder() - .with_fingerprint(hybrid_fp) - .build()?; - - let response = impit4 - .get("https://httpbin.org/headers".to_string(), None, None) - .await?; - println!("Status: {}", response.status()); - println!("Response: {}", response.text().await?); - - Ok(()) -} diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs index 71d091f2..fafde1ed 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -104,32 +104,14 @@ pub mod chrome_125 { /// Chrome 125 HTTP/2 fingerprint fn http2_fingerprint() -> Http2Fingerprint { - Http2Fingerprint::new( - // Pseudo-header ordering - vec![ + Http2Fingerprint { + pseudo_header_order: vec![ ":method".to_string(), ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ], - // SETTINGS frame values - Http2Settings::new( - Some(65536), // header_table_size - Some(false), // enable_push - Some(1000), // max_concurrent_streams - Some(6291456), // initial_window_size - Some(16384), // max_frame_size - Some(262144), // max_header_list_size - vec![], // custom settings - ), - // Window sizes - Http2WindowSize::new( - 15728640, // connection_window_size - 6291456, // stream_window_size - ), - // Priority - Some(Http2Priority::new(255, 0, false)), - ) + ] + } } /// Chrome 125 HTTP headers diff --git a/impit/src/fingerprint/database/firefox.rs b/impit/src/fingerprint/database/firefox.rs index e38d5614..b7dbe9f8 100644 --- a/impit/src/fingerprint/database/firefox.rs +++ b/impit/src/fingerprint/database/firefox.rs @@ -111,32 +111,14 @@ pub mod firefox_128 { /// Firefox 128 HTTP/2 fingerprint fn http2_fingerprint() -> Http2Fingerprint { - Http2Fingerprint::new( - // Pseudo-header ordering (Firefox uses different order than Chrome) - vec![ + Http2Fingerprint { + pseudo_header_order: vec![ ":method".to_string(), ":path".to_string(), ":authority".to_string(), ":scheme".to_string(), - ], - // SETTINGS frame values - Http2Settings::new( - Some(65536), // header_table_size - Some(false), // enable_push - Some(1000), // max_concurrent_streams - Some(131072), // initial_window_size - Some(16384), // max_frame_size - Some(262144), // max_header_list_size - vec![], // custom settings - ), - // Window sizes - Http2WindowSize::new( - 12517377, // connection_window_size - 131072, // stream_window_size - ), - // Priority - Some(Http2Priority::new(200, 0, false)), - ) + ] + } } /// Firefox 128 HTTP headers diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index 2bdfbceb..9e829846 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -9,20 +9,16 @@ mod types; pub use types::*; /// A complete browser fingerprint containing TLS, HTTP/2, and HTTP header configurations. -/// -/// This struct is immutable after creation to ensure consistency and prevent -/// accidental modifications that could break fingerprint accuracy. #[derive(Clone, Debug)] pub struct BrowserFingerprint { - name: String, - version: String, - tls: TlsFingerprint, - http2: Http2Fingerprint, - headers: Vec<(String, String)>, + pub name: String, + pub version: String, + pub tls: TlsFingerprint, + pub http2: Http2Fingerprint, + pub headers: Vec<(String, String)>, } impl BrowserFingerprint { - /// Creates a new browser fingerprint. pub fn new( name: impl Into, version: impl Into, @@ -38,52 +34,19 @@ impl BrowserFingerprint { headers, } } - - /// Returns the browser name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the browser version. - pub fn version(&self) -> &str { - &self.version - } - - /// Returns the TLS fingerprint. - pub fn tls(&self) -> &TlsFingerprint { - &self.tls - } - - /// Returns the HTTP/2 fingerprint. - pub fn http2(&self) -> &Http2Fingerprint { - &self.http2 - } - - /// Returns the HTTP headers. - pub fn headers(&self) -> &[(String, String)] { - &self.headers - } } -/// TLS fingerprint configuration. #[derive(Clone, Debug)] pub struct TlsFingerprint { - /// Cipher suites in preference order - cipher_suites: Vec, - /// Supported key exchange groups in preference order - key_exchange_groups: Vec, - /// Signature algorithms in preference order - signature_algorithms: Vec, - /// TLS extensions configuration - extensions: TlsExtensions, - /// ECH (Encrypted Client Hello) configuration - ech_config: Option, - /// ALPN protocols in preference order - alpn_protocols: Vec>, + 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 { - /// Creates a new TLS fingerprint. #[allow(clippy::too_many_arguments)] pub fn new( cipher_suites: Vec, @@ -102,241 +65,30 @@ impl TlsFingerprint { alpn_protocols, } } - - /// Returns the cipher suites. - pub fn cipher_suites(&self) -> &[CipherSuite] { - &self.cipher_suites - } - - /// Returns the key exchange groups. - pub fn key_exchange_groups(&self) -> &[KeyExchangeGroup] { - &self.key_exchange_groups - } - - /// Returns the signature algorithms. - pub fn signature_algorithms(&self) -> &[SignatureAlgorithm] { - &self.signature_algorithms - } - - /// Returns the TLS extensions configuration. - pub fn extensions(&self) -> &TlsExtensions { - &self.extensions - } - - /// Returns the ECH configuration. - pub fn ech_config(&self) -> Option<&EchConfig> { - self.ech_config.as_ref() - } - - /// Returns the ALPN protocols. - pub fn alpn_protocols(&self) -> &[Vec] { - &self.alpn_protocols - } } -/// HTTP/2 fingerprint configuration. #[derive(Clone, Debug)] pub struct Http2Fingerprint { - /// Pseudo-header ordering - pseudo_header_order: Vec, - /// SETTINGS frame values - settings: Http2Settings, - /// Initial window sizes - window_size: Http2WindowSize, - /// Priority configuration - priority: Option, -} - -impl Http2Fingerprint { - /// Creates a new HTTP/2 fingerprint. - pub fn new( - pseudo_header_order: Vec, - settings: Http2Settings, - window_size: Http2WindowSize, - priority: Option, - ) -> Self { - Self { - pseudo_header_order, - settings, - window_size, - priority, - } - } - - /// Returns the pseudo-header order. - pub fn pseudo_header_order(&self) -> &[String] { - &self.pseudo_header_order - } - - /// Returns the SETTINGS frame values. - pub fn settings(&self) -> &Http2Settings { - &self.settings - } - - /// Returns the window sizes. - pub fn window_size(&self) -> &Http2WindowSize { - &self.window_size - } - - /// Returns the priority configuration. - pub fn priority(&self) -> Option<&Http2Priority> { - self.priority.as_ref() - } -} - -/// HTTP/2 SETTINGS frame configuration. -#[derive(Clone, Debug)] -pub struct Http2Settings { - header_table_size: Option, - enable_push: Option, - max_concurrent_streams: Option, - initial_window_size: Option, - max_frame_size: Option, - max_header_list_size: Option, - /// Custom settings (non-standard settings IDs) - custom: Vec<(u16, u32)>, -} - -impl Http2Settings { - /// Creates a new HTTP/2 settings configuration. - #[allow(clippy::too_many_arguments)] - pub fn new( - header_table_size: Option, - enable_push: Option, - max_concurrent_streams: Option, - initial_window_size: Option, - max_frame_size: Option, - max_header_list_size: Option, - custom: Vec<(u16, u32)>, - ) -> Self { - Self { - header_table_size, - enable_push, - max_concurrent_streams, - initial_window_size, - max_frame_size, - max_header_list_size, - custom, - } - } - - /// Returns the header table size. - pub fn header_table_size(&self) -> Option { - self.header_table_size - } - - /// Returns whether push is enabled. - pub fn enable_push(&self) -> Option { - self.enable_push - } - - /// Returns the maximum concurrent streams. - pub fn max_concurrent_streams(&self) -> Option { - self.max_concurrent_streams - } - - /// Returns the initial window size. - pub fn initial_window_size(&self) -> Option { - self.initial_window_size - } - - /// Returns the maximum frame size. - pub fn max_frame_size(&self) -> Option { - self.max_frame_size - } - - /// Returns the maximum header list size. - pub fn max_header_list_size(&self) -> Option { - self.max_header_list_size - } - - /// Returns the custom settings. - pub fn custom(&self) -> &[(u16, u32)] { - &self.custom - } -} - -/// HTTP/2 window size configuration. -#[derive(Clone, Copy, Debug)] -pub struct Http2WindowSize { - connection_window_size: u32, - stream_window_size: u32, -} - -impl Http2WindowSize { - /// Creates a new window size configuration. - pub fn new(connection_window_size: u32, stream_window_size: u32) -> Self { - Self { - connection_window_size, - stream_window_size, - } - } - - /// Returns the connection window size. - pub fn connection_window_size(&self) -> u32 { - self.connection_window_size - } - - /// Returns the stream window size. - pub fn stream_window_size(&self) -> u32 { - self.stream_window_size - } -} - -/// HTTP/2 priority configuration. -#[derive(Clone, Copy, Debug)] -pub struct Http2Priority { - weight: u8, - depends_on: u32, - exclusive: bool, -} - -impl Http2Priority { - /// Creates a new priority configuration. - pub fn new(weight: u8, depends_on: u32, exclusive: bool) -> Self { - Self { - weight, - depends_on, - exclusive, - } - } - - /// Returns the priority weight. - pub fn weight(&self) -> u8 { - self.weight - } - - /// Returns the stream this depends on. - pub fn depends_on(&self) -> u32 { - self.depends_on - } - - /// Returns whether this dependency is exclusive. - pub fn exclusive(&self) -> bool { - self.exclusive - } + pub pseudo_header_order: Vec, } /// TLS extensions configuration. #[derive(Clone, Debug)] pub struct TlsExtensions { - 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 extension (Firefox-specific) - delegated_credentials: bool, - /// Record size limit extension (Firefox-specific) - record_size_limit: Option, - /// Extension order matters for fingerprinting - extension_order: Vec, + 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 { @@ -375,76 +127,6 @@ impl TlsExtensions { extension_order, } } - - /// Returns whether server_name extension is enabled. - pub fn server_name(&self) -> bool { - self.server_name - } - - /// Returns whether status_request extension is enabled. - pub fn status_request(&self) -> bool { - self.status_request - } - - /// Returns whether supported_groups extension is enabled. - pub fn supported_groups(&self) -> bool { - self.supported_groups - } - - /// Returns whether signature_algorithms extension is enabled. - pub fn signature_algorithms(&self) -> bool { - self.signature_algorithms - } - - /// Returns whether ALPN extension is enabled. - pub fn application_layer_protocol_negotiation(&self) -> bool { - self.application_layer_protocol_negotiation - } - - /// Returns whether signed_certificate_timestamp extension is enabled. - pub fn signed_certificate_timestamp(&self) -> bool { - self.signed_certificate_timestamp - } - - /// Returns whether key_share extension is enabled. - pub fn key_share(&self) -> bool { - self.key_share - } - - /// Returns whether psk_key_exchange_modes extension is enabled. - pub fn psk_key_exchange_modes(&self) -> bool { - self.psk_key_exchange_modes - } - - /// Returns whether supported_versions extension is enabled. - pub fn supported_versions(&self) -> bool { - self.supported_versions - } - - /// Returns the certificate compression algorithms. - pub fn compress_certificate(&self) -> Option<&[CertificateCompressionAlgorithm]> { - self.compress_certificate.as_deref() - } - - /// Returns whether application_settings extension is enabled. - pub fn application_settings(&self) -> bool { - self.application_settings - } - - /// Returns whether delegated_credentials extension is enabled. - pub fn delegated_credentials(&self) -> bool { - self.delegated_credentials - } - - /// Returns the record size limit if set. - pub fn record_size_limit(&self) -> Option { - self.record_size_limit - } - - /// Returns the extension order. - pub fn extension_order(&self) -> &[ExtensionType] { - &self.extension_order - } } /// ECH (Encrypted Client Hello) configuration. @@ -603,20 +285,20 @@ impl TlsFingerprint { // Check if GREASE is needed based on extension order let has_grease = self .extensions - .extension_order() + .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(), + 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().map(|algos| { + let cert_compression = self.extensions.compress_certificate.clone().map(|algos| { algos .iter() .map(|alg| match alg { diff --git a/impit/src/http_headers/mod.rs b/impit/src/http_headers/mod.rs index a4eb37a3..3422ad5e 100644 --- a/impit/src/http_headers/mod.rs +++ b/impit/src/http_headers/mod.rs @@ -25,7 +25,7 @@ impl HttpHeaders { // 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() + fp.headers.to_vec() } else { match self.context.browser { Some(Browser::Chrome) => statics::CHROME_HEADERS diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 82447f84..9159d857 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -252,7 +252,7 @@ impl Impit { // 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()); + tls_config_builder.with_tls_fingerprint(fingerprint.tls.clone()); } else { tls_config_builder.with_browser(config.browser); } @@ -319,7 +319,7 @@ impl Impit { // 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() + fingerprint.http2.pseudo_header_order.to_vec() } else { match config.browser { Some(Browser::Chrome) => statics::CHROME_PSEUDOHEADERS_ORDER diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index a986bd6e..061776fe 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -51,7 +51,7 @@ fn get_or_create_browser_provider_and_verifier(browser: Browser) -> BrowserCache } let fp = fingerprint::database::get_fingerprint(browser) - .tls() + .tls .clone(); let rustls_fp = fp.to_rustls_fingerprint(); @@ -137,7 +137,7 @@ impl TlsConfigBuilder { let (fingerprint, cache_browser) = if let Some(fp) = self.tls_fingerprint { (Some(fp), None) } else if let Some(b) = browser { - let fp = fingerprint::database::get_fingerprint(b).tls().clone(); + let fp = fingerprint::database::get_fingerprint(b).tls.clone(); (Some(fp), Some(b)) } else { (None, None) @@ -146,7 +146,7 @@ impl TlsConfigBuilder { let mut config = if let Some(fp) = fingerprint { let rustls_fingerprint = fp.to_rustls_fingerprint(); - let alpn_protocols = fp.alpn_protocols().to_vec(); + 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) From ab9e3b86b81124d45f22da1f66da9ec80f755fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 13:55:48 +0100 Subject: [PATCH 6/8] feat: drop `impit::emulation::Browser`, use new fingerprint format only --- README.md | 39 +++++++++--------- impit-cli/src/main.rs | 9 +++-- impit-node/src/impit_builder.rs | 22 +++++----- impit/examples/basic.rs | 5 +-- impit/src/fingerprint/database.rs | 13 ------ impit/src/fingerprint/database/chrome.rs | 4 +- impit/src/fingerprint/database/firefox.rs | 4 +- impit/src/fingerprint/mod.rs | 8 ++-- impit/src/fingerprint/types.rs | 12 +++--- impit/src/http_headers/mod.rs | 22 +--------- impit/src/http_headers/statics.rs | 49 ----------------------- impit/src/impit.rs | 31 +------------- impit/src/lib.rs | 15 ------- impit/src/tls/mod.rs | 31 +++++--------- 14 files changed, 68 insertions(+), 196 deletions(-) delete mode 100644 impit/src/http_headers/statics.rs 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/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 index ec27a391..9b3ebf97 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -5,18 +5,5 @@ mod chrome; mod firefox; -use super::BrowserFingerprint; -use crate::emulation::Browser; - pub use chrome::chrome_125; pub use firefox::firefox_128; - -/// Returns a fingerprint for the specified browser. -/// -/// This function provides a convenient way to get a fingerprint using the Browser enum. -pub fn get_fingerprint(browser: Browser) -> BrowserFingerprint { - match browser { - Browser::Chrome => chrome_125::fingerprint(), - Browser::Firefox => firefox_128::fingerprint(), - } -} diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs index fafde1ed..41042967 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -110,7 +110,9 @@ pub mod chrome_125 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ] + ":protocol".to_string(), + ":status".to_string(), + ], } } diff --git a/impit/src/fingerprint/database/firefox.rs b/impit/src/fingerprint/database/firefox.rs index b7dbe9f8..17d2a800 100644 --- a/impit/src/fingerprint/database/firefox.rs +++ b/impit/src/fingerprint/database/firefox.rs @@ -117,7 +117,9 @@ pub mod firefox_128 { ":path".to_string(), ":authority".to_string(), ":scheme".to_string(), - ] + ":protocol".to_string(), + ":status".to_string(), + ], } } diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index 9e829846..21d5f82f 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -36,7 +36,7 @@ impl BrowserFingerprint { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TlsFingerprint { pub cipher_suites: Vec, pub key_exchange_groups: Vec, @@ -73,7 +73,7 @@ pub struct Http2Fingerprint { } /// TLS extensions configuration. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct TlsExtensions { pub server_name: bool, pub status_request: bool, @@ -130,7 +130,7 @@ impl TlsExtensions { } /// ECH (Encrypted Client Hello) configuration. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct EchConfig { mode: EchMode, config_list: Option>, @@ -154,7 +154,7 @@ impl EchConfig { } /// ECH mode configuration. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum EchMode { /// ECH is disabled Disabled, diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs index 6d18b495..6f1953aa 100644 --- a/impit/src/fingerprint/types.rs +++ b/impit/src/fingerprint/types.rs @@ -5,7 +5,7 @@ //! in a type-safe manner. /// TLS cipher suites -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum CipherSuite { // TLS 1.3 cipher suites TLS13_AES_128_GCM_SHA256, @@ -31,7 +31,7 @@ pub enum CipherSuite { } /// Key exchange groups for TLS -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum KeyExchangeGroup { X25519, Secp256r1, @@ -47,7 +47,7 @@ pub enum KeyExchangeGroup { } /// Signature algorithms for TLS -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum SignatureAlgorithm { // RSASSA-PSS algorithms RsaPssRsaSha256, @@ -70,7 +70,7 @@ pub enum SignatureAlgorithm { } /// TLS extension types -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ExtensionType { ServerName, MaxFragmentLength, @@ -103,7 +103,7 @@ pub enum ExtensionType { } /// Certificate compression algorithms -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum CertificateCompressionAlgorithm { Zlib, Brotli, @@ -111,7 +111,7 @@ pub enum CertificateCompressionAlgorithm { } /// HPKE KEM identifiers for ECH -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum HpkeKemId { DhKemP256HkdfSha256, DhKemP384HkdfSha384, diff --git a/impit/src/http_headers/mod.rs b/impit/src/http_headers/mod.rs index 3422ad5e..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, fingerprint::BrowserFingerprint}; +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, } @@ -27,17 +25,7 @@ impl HttpHeaders { if let Some(ref fp) = self.context.fingerprint { fp.headers.to_vec() } else { - match self.context.browser { - Some(Browser::Chrome) => statics::CHROME_HEADERS - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - Some(Browser::Firefox) => statics::FIREFOX_HEADERS - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - None => vec![], - } + vec![] }; let custom_headers = self @@ -96,7 +84,6 @@ impl From for Result { #[derive(Default, Clone)] pub struct HttpHeadersBuilder { host: String, - browser: Option, fingerprint: Option, https: bool, custom_headers: Vec<(String, String)>, @@ -109,11 +96,6 @@ impl HttpHeadersBuilder { self } - pub fn with_browser(&mut self, browser: &Option) -> &mut Self { - self.browser = browser.to_owned(); - self - } - 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 9159d857..0f7d63d6 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -6,11 +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, }; @@ -75,7 +74,6 @@ pub enum RedirectBehavior { /// ``` #[derive(Debug)] pub struct ImpitBuilder { - browser: Option, fingerprint: Option, ignore_tls_errors: bool, vanilla_fallback: bool, @@ -91,7 +89,6 @@ 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, @@ -109,7 +106,6 @@ impl Clone for ImpitBuilder Default for ImpitBuilder { fn default() -> Self { ImpitBuilder { - browser: None, fingerprint: None, ignore_tls_errors: false, vanilla_fallback: true, @@ -125,16 +121,6 @@ impl Default for ImpitBuilder ImpitBuilder { - /// Sets the browser to impersonate. - /// - /// The [`Browser`] enum is used to set the HTTP headers, TLS behaviour and other markers to impersonate a specific browser. - /// - /// If not used, the client will use the default `reqwest` fingerprints. - pub fn with_browser(mut self, browser: Browser) -> Self { - self.browser = Some(browser); - self - } - /// Sets a complete browser fingerprint. /// /// This method allows you to provide a complete fingerprint that includes TLS, HTTP/2, and HTTP header configurations. @@ -253,8 +239,6 @@ impl Impit { // 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()); - } else { - tls_config_builder.with_browser(config.browser); } if config.max_http_version == Version::HTTP_3 { @@ -321,17 +305,7 @@ impl Impit { let pseudo_headers_order: Vec = if let Some(ref fingerprint) = config.fingerprint { fingerprint.http2.pseudo_header_order.to_vec() } else { - match config.browser { - Some(Browser::Chrome) => statics::CHROME_PSEUDOHEADERS_ORDER - .iter() - .map(|s| s.to_string()) - .collect(), - Some(Browser::Firefox) => statics::FIREFOX_PSEUDOHEADERS_ORDER - .iter() - .map(|s| s.to_string()) - .collect(), - None => vec![], - } + vec![] }; if !pseudo_headers_order.is_empty() { @@ -401,7 +375,6 @@ 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") diff --git a/impit/src/lib.rs b/impit/src/lib.rs index 78a5c5a6..d3654667 100644 --- a/impit/src/lib.rs +++ b/impit/src/lib.rs @@ -78,21 +78,6 @@ pub mod errors; /// Browser fingerprint definitions and types. pub mod fingerprint; -/// 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, Eq, Hash, Debug, Clone, Copy, Default)] - pub enum Browser { - #[default] - Chrome, - Firefox, - } -} - /// Various utility functions and types. pub mod utils { pub use crate::response_parsing::decode; diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index 061776fe..63638b4e 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -3,8 +3,7 @@ mod statics; use std::collections::HashMap; use std::sync::{Arc, Mutex, OnceLock}; -use crate::emulation::Browser; -use crate::fingerprint::{self, TlsFingerprint}; +use crate::fingerprint::TlsFingerprint; use reqwest::Version; use rustls::client::danger::NoVerifier; use rustls::client::EchGreaseConfig; @@ -15,9 +14,9 @@ static VANILLA_CRYPTO_PROVIDER: OnceLock> = OnceLock::new(); static VANILLA_VERIFIER: OnceLock> = OnceLock::new(); type BrowserCacheValue = (Arc, Arc); -static BROWSER_CACHE: OnceLock>> = OnceLock::new(); +static BROWSER_CACHE: OnceLock>> = OnceLock::new(); -fn get_browser_cache() -> &'static Mutex> { +fn get_browser_cache() -> &'static Mutex> { BROWSER_CACHE.get_or_init(|| Mutex::new(HashMap::new())) } @@ -42,18 +41,17 @@ fn get_vanilla_verifier() -> Arc { .clone() } -fn get_or_create_browser_provider_and_verifier(browser: Browser) -> BrowserCacheValue { +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(&browser) { + if let Some(cached) = cache.get(&tls_fingerprint) { return cached.clone(); } } - let fp = fingerprint::database::get_fingerprint(browser) - .tls - .clone(); - let rustls_fp = fp.to_rustls_fingerprint(); + let rustls_fp = tls_fingerprint.to_rustls_fingerprint(); let provider: Arc = CryptoProvider::builder() .with_tls_fingerprint(rustls_fp) @@ -70,7 +68,7 @@ fn get_or_create_browser_provider_and_verifier(browser: Browser) -> BrowserCache { let mut cache = get_browser_cache().lock().unwrap(); - cache.insert(browser, (provider.clone(), verifier.clone())); + cache.insert(tls_fingerprint, (provider.clone(), verifier.clone())); } (provider, verifier) @@ -86,7 +84,6 @@ impl TlsConfig { #[derive(Debug, Clone)] pub struct TlsConfigBuilder { - browser: Option, tls_fingerprint: Option, max_http_version: Version, ignore_tls_errors: bool, @@ -95,7 +92,6 @@ 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, @@ -109,11 +105,6 @@ fn get_ech_mode() -> rustls::client::EchMode { } impl TlsConfigBuilder { - pub fn with_browser(&mut self, browser: Option) -> &mut Self { - self.browser = browser; - self - } - pub fn with_tls_fingerprint(&mut self, fingerprint: TlsFingerprint) -> &mut Self { self.tls_fingerprint = Some(fingerprint); self @@ -132,13 +123,9 @@ impl TlsConfigBuilder { pub fn build(self) -> rustls::ClientConfig { let ignore_tls_errors = self.ignore_tls_errors; let max_http_version = self.max_http_version; - let browser = self.browser; let (fingerprint, cache_browser) = if let Some(fp) = self.tls_fingerprint { (Some(fp), None) - } else if let Some(b) = browser { - let fp = fingerprint::database::get_fingerprint(b).tls.clone(); - (Some(fp), Some(b)) } else { (None, None) }; From aac2bbc46c94d6f1d4fe0524c9c23449c881a552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 14:01:07 +0100 Subject: [PATCH 7/8] chore: fix build in Python bindings --- impit-python/src/async_client.rs | 7 ++++--- impit-python/src/client.rs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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, From 4e034208e2a9acf9f7027582deec2312da390443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Tue, 27 Jan 2026 14:06:52 +0100 Subject: [PATCH 8/8] chore: match examples with the new API --- impit/src/impit.rs | 5 ++--- impit/src/lib.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 0f7d63d6..8a7437d5 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -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)) diff --git a/impit/src/lib.rs b/impit/src/lib.rs index d3654667..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();