#[cfg(feature = "emulation")]
use std::collections::hash_map::DefaultHasher;
#[cfg(feature = "emulation")]
use std::hash::{Hash, Hasher};
#[cfg(feature = "emulation")]
use crate::error::Result;
#[cfg(feature = "emulation")]
use crate::header::HeaderMap;
#[cfg(feature = "emulation")]
#[derive(Clone, Debug)]
pub enum Emulation {
Chrome136,
Firefox128,
Safari18_4,
Custom(EmulationProfile),
}
#[cfg(feature = "emulation")]
impl Emulation {
pub fn profile(&self) -> EmulationProfile {
match self {
Self::Chrome136 => EmulationProfile::chrome136(),
Self::Firefox128 => EmulationProfile::firefox128(),
Self::Safari18_4 => EmulationProfile::safari18_4(),
Self::Custom(profile) => profile.clone(),
}
}
}
#[cfg(feature = "emulation")]
impl From<Emulation> for EmulationProfile {
fn from(value: Emulation) -> Self {
value.profile()
}
}
#[cfg(feature = "emulation")]
pub type BrowserProfile = EmulationProfile;
#[cfg(not(feature = "emulation"))]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub(crate) struct BrowserProfile;
#[cfg(not(feature = "emulation"))]
impl BrowserProfile {
pub(crate) fn http1_fingerprint(&self) -> Option<&Http1Fingerprint> {
None
}
pub(crate) fn tls_fingerprint(&self) -> Option<&TlsFingerprint> {
None
}
pub(crate) fn http2_fingerprint(&self) -> Option<&Http2Fingerprint> {
None
}
pub(crate) fn tls_identity_hash(&self) -> Option<u64> {
None
}
pub(crate) fn http2_identity_hash(&self) -> Option<u64> {
None
}
pub(crate) fn http3_identity_hash(&self) -> Option<u64> {
None
}
pub(crate) fn connection_identity_hash(&self) -> Option<u64> {
None
}
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum BrowserFamily {
Chrome,
Firefox,
Safari,
Custom(String),
}
#[cfg(feature = "emulation")]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum EmulationFidelity {
BestEffort,
#[default]
High,
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ProfileMetadata {
pub profile_id: String,
pub browser_family: BrowserFamily,
pub browser_version: String,
pub platform: String,
pub captured_at: Option<String>,
pub source: Option<String>,
pub fidelity: EmulationFidelity,
}
#[cfg(feature = "emulation")]
impl Default for ProfileMetadata {
fn default() -> Self {
Self {
profile_id: "custom".to_owned(),
browser_family: BrowserFamily::Custom("custom".to_owned()),
browser_version: "custom".to_owned(),
platform: "unknown".to_owned(),
captured_at: None,
source: None,
fidelity: EmulationFidelity::High,
}
}
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct RequestProfile {
pub default_headers: Vec<(String, String)>,
pub http1: Option<Http1Fingerprint>,
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct ConnectionProfile {
pub tls: Option<TlsFingerprint>,
pub boring_tls: Option<BoringTlsFingerprint>,
pub http2: Option<Http2Fingerprint>,
pub http3: Option<Http3Fingerprint>,
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct EmulationProfile {
pub metadata: ProfileMetadata,
pub request: RequestProfile,
pub connection: ConnectionProfile,
}
#[cfg(feature = "emulation")]
impl EmulationProfile {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> EmulationProfileBuilder {
EmulationProfileBuilder::default()
}
pub fn metadata(&self) -> &ProfileMetadata {
&self.metadata
}
pub fn default_headers(&self) -> &[(String, String)] {
&self.request.default_headers
}
pub fn http1_fingerprint(&self) -> Option<&Http1Fingerprint> {
self.request.http1.as_ref()
}
pub fn tls_fingerprint(&self) -> Option<&TlsFingerprint> {
self.connection.tls.as_ref()
}
pub fn boring_tls_fingerprint(&self) -> Option<&BoringTlsFingerprint> {
self.connection.boring_tls.as_ref()
}
pub fn http2_fingerprint(&self) -> Option<&Http2Fingerprint> {
self.connection.http2.as_ref()
}
pub fn http3_fingerprint(&self) -> Option<&Http3Fingerprint> {
self.connection.http3.as_ref()
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.request
.default_headers
.push((name.into(), value.into()));
self
}
pub fn with_http1_fingerprint(mut self, fingerprint: Http1Fingerprint) -> Self {
self.request.http1 = Some(fingerprint);
self
}
pub fn with_tls_fingerprint(mut self, fingerprint: TlsFingerprint) -> Self {
self.connection.tls = Some(fingerprint);
self
}
pub fn with_boring_tls_fingerprint(mut self, fingerprint: BoringTlsFingerprint) -> Self {
self.connection.boring_tls = Some(fingerprint);
self
}
pub fn with_http2_fingerprint(mut self, fingerprint: Http2Fingerprint) -> Self {
self.connection.http2 = Some(fingerprint);
self
}
pub fn with_http3_fingerprint(mut self, fingerprint: Http3Fingerprint) -> Self {
self.connection.http3 = Some(fingerprint);
self
}
pub(crate) fn tls_identity_hash(&self) -> Option<u64> {
self.connection
.tls
.as_ref()
.zip(self.connection.boring_tls.as_ref())
.map(|value| hash_value(&value))
.or_else(|| self.connection.tls.as_ref().map(hash_value))
.or_else(|| self.connection.boring_tls.as_ref().map(hash_value))
}
pub(crate) fn http2_identity_hash(&self) -> Option<u64> {
self.connection.http2.as_ref().map(hash_value)
}
pub(crate) fn http3_identity_hash(&self) -> Option<u64> {
self.connection.http3.as_ref().map(hash_value)
}
pub(crate) fn connection_identity_hash(&self) -> Option<u64> {
Some(hash_value(&self.connection)).filter(|_| {
self.connection.tls.is_some()
|| self.connection.boring_tls.is_some()
|| self.connection.http2.is_some()
|| self.connection.http3.is_some()
})
}
fn chrome136() -> Self {
Self::builder()
.profile_id("chrome-136-windows")
.browser_family(BrowserFamily::Chrome)
.browser_version("136.0.0.0")
.platform("Windows NT 10.0; Win64; x64")
.source("ugi preset")
.default_header(
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
)
.expect("valid preset header")
.default_header(
"accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
)
.expect("valid preset header")
.default_header("accept-language", "en-US,en;q=0.9")
.expect("valid preset header")
.http1_fingerprint(Http1Fingerprint::chrome())
.tls_fingerprint(TlsFingerprint::chrome())
.boring_tls_fingerprint(BoringTlsFingerprint::chrome())
.http2_fingerprint(Http2Fingerprint::chrome())
.http3_fingerprint(Http3Fingerprint::chrome())
.build()
}
fn firefox128() -> Self {
Self::builder()
.profile_id("firefox-128-windows")
.browser_family(BrowserFamily::Firefox)
.browser_version("128.0")
.platform("Windows NT 10.0; Win64; x64")
.source("ugi preset")
.default_header(
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
)
.expect("valid preset header")
.default_header(
"accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
)
.expect("valid preset header")
.default_header("accept-language", "en-US,en;q=0.5")
.expect("valid preset header")
.http1_fingerprint(Http1Fingerprint::firefox())
.tls_fingerprint(TlsFingerprint::firefox())
.boring_tls_fingerprint(BoringTlsFingerprint::firefox())
.http2_fingerprint(Http2Fingerprint::firefox())
.http3_fingerprint(Http3Fingerprint::firefox())
.build()
}
fn safari18_4() -> Self {
Self::builder()
.profile_id("safari-18.4-macos")
.browser_family(BrowserFamily::Safari)
.browser_version("18.4")
.platform("Macintosh; Intel Mac OS X 10_15_7")
.source("ugi preset")
.default_header(
"user-agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15",
)
.expect("valid preset header")
.default_header(
"accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
)
.expect("valid preset header")
.default_header("accept-language", "en-US,en;q=0.9")
.expect("valid preset header")
.http1_fingerprint(Http1Fingerprint::safari())
.tls_fingerprint(TlsFingerprint::safari())
.boring_tls_fingerprint(BoringTlsFingerprint::safari())
.http2_fingerprint(Http2Fingerprint::safari())
.build()
}
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Default)]
pub struct EmulationProfileBuilder {
metadata: ProfileMetadata,
request: RequestProfile,
connection: ConnectionProfile,
}
#[cfg(feature = "emulation")]
impl EmulationProfileBuilder {
pub fn profile_id(mut self, profile_id: impl Into<String>) -> Self {
self.metadata.profile_id = profile_id.into();
self
}
pub fn browser_family(mut self, browser_family: BrowserFamily) -> Self {
self.metadata.browser_family = browser_family;
self
}
pub fn browser_version(mut self, browser_version: impl Into<String>) -> Self {
self.metadata.browser_version = browser_version.into();
self
}
pub fn platform(mut self, platform: impl Into<String>) -> Self {
self.metadata.platform = platform.into();
self
}
pub fn captured_at(mut self, captured_at: impl Into<String>) -> Self {
self.metadata.captured_at = Some(captured_at.into());
self
}
pub fn source(mut self, source: impl Into<String>) -> Self {
self.metadata.source = Some(source.into());
self
}
pub fn fidelity(mut self, fidelity: EmulationFidelity) -> Self {
self.metadata.fidelity = fidelity;
self
}
pub fn default_header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(name.as_ref(), value.as_ref())?;
self.request
.default_headers
.push((name.as_ref().to_owned(), value.as_ref().to_owned()));
Ok(self)
}
pub fn http1_fingerprint(mut self, fingerprint: Http1Fingerprint) -> Self {
self.request.http1 = Some(fingerprint);
self
}
pub fn tls_fingerprint(mut self, fingerprint: TlsFingerprint) -> Self {
self.connection.tls = Some(fingerprint);
self
}
pub fn boring_tls_fingerprint(mut self, fingerprint: BoringTlsFingerprint) -> Self {
self.connection.boring_tls = Some(fingerprint);
self
}
pub fn http2_fingerprint(mut self, fingerprint: Http2Fingerprint) -> Self {
self.connection.http2 = Some(fingerprint);
self
}
pub fn http3_fingerprint(mut self, fingerprint: Http3Fingerprint) -> Self {
self.connection.http3 = Some(fingerprint);
self
}
pub fn build(self) -> EmulationProfile {
EmulationProfile {
metadata: self.metadata,
request: self.request,
connection: self.connection,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct TlsFingerprint {
pub alpn_protocols: Vec<String>,
pub min_tls_version: Option<String>,
pub max_tls_version: Option<String>,
}
impl TlsFingerprint {
#[cfg(feature = "emulation")]
fn chrome() -> Self {
Self {
alpn_protocols: vec!["h3".into(), "h2".into(), "http/1.1".into()],
min_tls_version: Some("1.2".into()),
max_tls_version: Some("1.3".into()),
}
}
#[cfg(feature = "emulation")]
fn firefox() -> Self {
Self {
alpn_protocols: vec!["h3".into(), "h2".into(), "http/1.1".into()],
min_tls_version: Some("1.2".into()),
max_tls_version: Some("1.3".into()),
}
}
#[cfg(feature = "emulation")]
fn safari() -> Self {
Self {
alpn_protocols: vec!["h2".into(), "http/1.1".into()],
min_tls_version: Some("1.2".into()),
max_tls_version: Some("1.3".into()),
}
}
}
#[cfg(any(feature = "emulation", feature = "btls-backend"))]
#[cfg_attr(not(feature = "emulation"), allow(dead_code))]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum BoringCertCompression {
Brotli,
}
#[cfg(any(feature = "emulation", feature = "btls-backend"))]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct BoringTlsFingerprint {
pub cipher_list: Option<String>,
pub curves_list: Option<String>,
pub sigalgs_list: Option<String>,
pub grease_enabled: Option<bool>,
pub permute_extensions: Option<bool>,
pub enable_ocsp_stapling: bool,
pub enable_signed_cert_timestamps: bool,
pub enable_ech_grease: bool,
pub certificate_compression: Vec<BoringCertCompression>,
}
#[cfg(any(feature = "emulation", feature = "btls-backend"))]
impl BoringTlsFingerprint {
#[cfg(feature = "emulation")]
fn chrome() -> Self {
Self {
cipher_list: Some(
"TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"
.to_owned(),
),
curves_list: Some("X25519:P-256:P-384".to_owned()),
sigalgs_list: Some(
"ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512"
.to_owned(),
),
grease_enabled: Some(true),
permute_extensions: Some(true),
enable_ocsp_stapling: true,
enable_signed_cert_timestamps: true,
enable_ech_grease: true,
certificate_compression: vec![BoringCertCompression::Brotli],
}
}
#[cfg(feature = "emulation")]
fn firefox() -> Self {
Self {
cipher_list: Some(
"TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
.to_owned(),
),
curves_list: Some("X25519:P-256:P-384".to_owned()),
sigalgs_list: Some(
"ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384"
.to_owned(),
),
grease_enabled: Some(true),
permute_extensions: Some(true),
enable_ocsp_stapling: true,
enable_signed_cert_timestamps: true,
enable_ech_grease: true,
certificate_compression: vec![BoringCertCompression::Brotli],
}
}
#[cfg(feature = "emulation")]
fn safari() -> Self {
Self {
cipher_list: Some(
"TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305"
.to_owned(),
),
curves_list: Some("X25519:P-256:P-384".to_owned()),
sigalgs_list: Some(
"ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384"
.to_owned(),
),
grease_enabled: Some(true),
permute_extensions: Some(false),
enable_ocsp_stapling: true,
enable_signed_cert_timestamps: true,
enable_ech_grease: false,
certificate_compression: vec![BoringCertCompression::Brotli],
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct Http1Fingerprint {
pub header_order: Vec<String>,
pub original_header_case: Vec<(String, String)>,
}
impl Http1Fingerprint {
#[cfg(feature = "emulation")]
fn chrome() -> Self {
Self {
header_order: vec![
"host".into(),
"connection".into(),
"user-agent".into(),
"accept".into(),
"accept-language".into(),
"accept-encoding".into(),
],
original_header_case: default_original_header_case(),
}
}
#[cfg(feature = "emulation")]
fn firefox() -> Self {
Self {
header_order: vec![
"host".into(),
"user-agent".into(),
"accept".into(),
"accept-language".into(),
"accept-encoding".into(),
"connection".into(),
],
original_header_case: default_original_header_case(),
}
}
#[cfg(feature = "emulation")]
fn safari() -> Self {
Self {
header_order: vec![
"host".into(),
"user-agent".into(),
"accept".into(),
"accept-language".into(),
"accept-encoding".into(),
],
original_header_case: default_original_header_case(),
}
}
}
#[cfg(feature = "emulation")]
fn default_original_header_case() -> Vec<(String, String)> {
[
("host", "Host"),
("connection", "Connection"),
("user-agent", "User-Agent"),
("accept", "Accept"),
("accept-language", "Accept-Language"),
("accept-encoding", "Accept-Encoding"),
("authorization", "Authorization"),
("content-type", "Content-Type"),
("content-length", "Content-Length"),
("transfer-encoding", "Transfer-Encoding"),
("cookie", "Cookie"),
("proxy-authorization", "Proxy-Authorization"),
("origin", "Origin"),
("referer", "Referer"),
("cache-control", "Cache-Control"),
("pragma", "Pragma"),
("upgrade-insecure-requests", "Upgrade-Insecure-Requests"),
("sec-fetch-site", "Sec-Fetch-Site"),
("sec-fetch-mode", "Sec-Fetch-Mode"),
("sec-fetch-user", "Sec-Fetch-User"),
("sec-fetch-dest", "Sec-Fetch-Dest"),
("sec-ch-ua", "sec-ch-ua"),
("sec-ch-ua-mobile", "sec-ch-ua-mobile"),
("sec-ch-ua-platform", "sec-ch-ua-platform"),
]
.into_iter()
.map(|(lower, original)| (lower.to_owned(), original.to_owned()))
.collect()
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum Http2PriorityPhase {
#[default]
BeforeHeaders,
AfterHeaders,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct Http2PrioritySpec {
pub stream_id: Option<u32>,
pub phase: Http2PriorityPhase,
pub stream_dependency: u32,
pub weight: u16,
pub exclusive: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct Http2Fingerprint {
pub settings_order: Vec<String>,
pub pseudo_header_order: Vec<String>,
pub regular_header_order: Vec<String>,
pub header_table_size: Option<u32>,
pub initial_window_size: Option<u32>,
pub initial_connection_window_size: Option<u32>,
pub max_frame_size: Option<u32>,
pub priorities: Vec<Http2PrioritySpec>,
}
impl Http2Fingerprint {
#[cfg(feature = "emulation")]
fn chrome() -> Self {
Self {
settings_order: vec![
"HEADER_TABLE_SIZE".into(),
"ENABLE_PUSH".into(),
"INITIAL_WINDOW_SIZE".into(),
"MAX_FRAME_SIZE".into(),
],
pseudo_header_order: vec![
":method".into(),
":authority".into(),
":scheme".into(),
":path".into(),
],
regular_header_order: vec![
"user-agent".into(),
"accept".into(),
"accept-language".into(),
"accept-encoding".into(),
],
header_table_size: Some(65_536),
initial_window_size: Some(6_291_456),
initial_connection_window_size: Some(15_663_105),
max_frame_size: Some(16_384),
priorities: Vec::new(),
}
}
#[cfg(feature = "emulation")]
fn firefox() -> Self {
Self {
settings_order: vec![
"HEADER_TABLE_SIZE".into(),
"INITIAL_WINDOW_SIZE".into(),
"MAX_FRAME_SIZE".into(),
"ENABLE_PUSH".into(),
],
pseudo_header_order: vec![
":method".into(),
":path".into(),
":authority".into(),
":scheme".into(),
],
regular_header_order: vec![
"user-agent".into(),
"accept".into(),
"accept-language".into(),
"accept-encoding".into(),
],
header_table_size: Some(65_536),
initial_window_size: Some(131_072),
initial_connection_window_size: Some(1_048_576),
max_frame_size: Some(16_384),
priorities: Vec::new(),
}
}
#[cfg(feature = "emulation")]
fn safari() -> Self {
Self {
settings_order: vec![
"ENABLE_PUSH".into(),
"INITIAL_WINDOW_SIZE".into(),
"HEADER_TABLE_SIZE".into(),
"MAX_FRAME_SIZE".into(),
],
pseudo_header_order: vec![
":method".into(),
":scheme".into(),
":path".into(),
":authority".into(),
],
regular_header_order: vec![
"accept".into(),
"user-agent".into(),
"accept-language".into(),
"accept-encoding".into(),
],
header_table_size: Some(4_096),
initial_window_size: Some(65_535),
initial_connection_window_size: Some(1_048_576),
max_frame_size: Some(16_384),
priorities: Vec::new(),
}
}
}
#[cfg(feature = "emulation")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct Http3Fingerprint {
pub alpn_protocols: Vec<String>,
}
#[cfg(feature = "emulation")]
impl Http3Fingerprint {
fn chrome() -> Self {
Self {
alpn_protocols: vec!["h3".into()],
}
}
fn firefox() -> Self {
Self {
alpn_protocols: vec!["h3".into()],
}
}
}
#[cfg(feature = "emulation")]
pub(crate) fn hash_value<T>(value: &T) -> u64
where
T: Hash,
{
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}