1use serde::{Deserialize, Serialize};
35use std::fmt;
36use std::sync::LazyLock;
37
38pub(crate) const fn rng(seed: u64, step: u64) -> u64 {
43 let x = seed.wrapping_add(step.wrapping_mul(0x9e37_79b9_7f4a_7c15));
44 let x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
45 let x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
46 x ^ (x >> 31)
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub struct CipherSuiteId(pub u16);
66
67impl CipherSuiteId {
68 pub const TLS_AES_128_GCM_SHA256: Self = Self(0x1301);
70 pub const TLS_AES_256_GCM_SHA384: Self = Self(0x1302);
72 pub const TLS_CHACHA20_POLY1305_SHA256: Self = Self(0x1303);
74 pub const TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: Self = Self(0xc02b);
76 pub const TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: Self = Self(0xc02f);
78 pub const TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: Self = Self(0xc02c);
80 pub const TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: Self = Self(0xc030);
82 pub const TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Self = Self(0xcca9);
84 pub const TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Self = Self(0xcca8);
86 pub const TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: Self = Self(0xc013);
88 pub const TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: Self = Self(0xc014);
90 pub const TLS_RSA_WITH_AES_128_GCM_SHA256: Self = Self(0x009c);
92 pub const TLS_RSA_WITH_AES_256_GCM_SHA384: Self = Self(0x009d);
94 pub const TLS_RSA_WITH_AES_128_CBC_SHA: Self = Self(0x002f);
96 pub const TLS_RSA_WITH_AES_256_CBC_SHA: Self = Self(0x0035);
98}
99
100impl fmt::Display for CipherSuiteId {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}", self.0)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[non_exhaustive]
118pub enum TlsVersion {
119 Tls12,
121 Tls13,
123}
124
125impl TlsVersion {
126 pub const fn iana_value(self) -> u16 {
136 match self {
137 Self::Tls12 => 0x0303,
138 Self::Tls13 => 0x0304,
139 }
140 }
141}
142
143impl fmt::Display for TlsVersion {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{}", self.iana_value())
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160pub struct TlsExtensionId(pub u16);
161
162impl TlsExtensionId {
163 pub const SERVER_NAME: Self = Self(0);
165 pub const EXTENDED_MASTER_SECRET: Self = Self(23);
167 pub const ENCRYPT_THEN_MAC: Self = Self(22);
169 pub const SESSION_TICKET: Self = Self(35);
171 pub const SIGNATURE_ALGORITHMS: Self = Self(13);
173 pub const SUPPORTED_VERSIONS: Self = Self(43);
175 pub const PSK_KEY_EXCHANGE_MODES: Self = Self(45);
177 pub const KEY_SHARE: Self = Self(51);
179 pub const SUPPORTED_GROUPS: Self = Self(10);
181 pub const EC_POINT_FORMATS: Self = Self(11);
183 pub const ALPN: Self = Self(16);
185 pub const STATUS_REQUEST: Self = Self(5);
187 pub const SIGNED_CERTIFICATE_TIMESTAMP: Self = Self(18);
189 pub const COMPRESS_CERTIFICATE: Self = Self(27);
191 pub const APPLICATION_SETTINGS: Self = Self(17513);
193 pub const RENEGOTIATION_INFO: Self = Self(0xff01);
195 pub const DELEGATED_CREDENTIALS: Self = Self(34);
197 pub const RECORD_SIZE_LIMIT: Self = Self(28);
199 pub const PADDING: Self = Self(21);
201 pub const PRE_SHARED_KEY: Self = Self(41);
203 pub const POST_HANDSHAKE_AUTH: Self = Self(49);
205}
206
207impl fmt::Display for TlsExtensionId {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 write!(f, "{}", self.0)
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
224#[non_exhaustive]
225pub enum SupportedGroup {
226 X25519,
228 SecP256r1,
230 SecP384r1,
232 SecP521r1,
234 X25519Kyber768,
236 Ffdhe2048,
238 Ffdhe3072,
240}
241
242impl SupportedGroup {
243 pub const fn iana_value(self) -> u16 {
253 match self {
254 Self::X25519 => 0x001d,
255 Self::SecP256r1 => 0x0017,
256 Self::SecP384r1 => 0x0018,
257 Self::SecP521r1 => 0x0019,
258 Self::X25519Kyber768 => 0x6399,
259 Self::Ffdhe2048 => 0x0100,
260 Self::Ffdhe3072 => 0x0101,
261 }
262 }
263}
264
265impl fmt::Display for SupportedGroup {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 write!(f, "{}", self.iana_value())
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
282pub struct SignatureAlgorithm(pub u16);
283
284impl SignatureAlgorithm {
285 pub const ECDSA_SECP256R1_SHA256: Self = Self(0x0403);
287 pub const RSA_PSS_RSAE_SHA256: Self = Self(0x0804);
289 pub const RSA_PKCS1_SHA256: Self = Self(0x0401);
291 pub const ECDSA_SECP384R1_SHA384: Self = Self(0x0503);
293 pub const RSA_PSS_RSAE_SHA384: Self = Self(0x0805);
295 pub const RSA_PKCS1_SHA384: Self = Self(0x0501);
297 pub const RSA_PSS_RSAE_SHA512: Self = Self(0x0806);
299 pub const RSA_PKCS1_SHA512: Self = Self(0x0601);
301 pub const ECDSA_SECP521R1_SHA512: Self = Self(0x0603);
303 pub const RSA_PKCS1_SHA1: Self = Self(0x0201);
305 pub const ECDSA_SHA1: Self = Self(0x0203);
307}
308
309impl fmt::Display for SignatureAlgorithm {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 write!(f, "{}", self.0)
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
325#[non_exhaustive]
326pub enum AlpnProtocol {
327 H2,
329 Http11,
331}
332
333impl AlpnProtocol {
334 pub const fn as_str(self) -> &'static str {
344 match self {
345 Self::H2 => "h2",
346 Self::Http11 => "http/1.1",
347 }
348 }
349}
350
351impl fmt::Display for AlpnProtocol {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 f.write_str(self.as_str())
354 }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374#[non_exhaustive]
375pub struct TlsProfile {
376 pub name: String,
378 pub cipher_suites: Vec<CipherSuiteId>,
380 pub tls_versions: Vec<TlsVersion>,
382 pub extensions: Vec<TlsExtensionId>,
384 pub supported_groups: Vec<SupportedGroup>,
386 pub signature_algorithms: Vec<SignatureAlgorithm>,
388 pub alpn_protocols: Vec<AlpnProtocol>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct Ja3Hash {
412 pub raw: String,
414 pub hash: String,
416}
417
418impl fmt::Display for Ja3Hash {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 f.write_str(&self.hash)
421 }
422}
423
424#[allow(
429 clippy::many_single_char_names,
430 clippy::too_many_lines,
431 clippy::indexing_slicing
432)]
433fn md5_hex(data: &[u8]) -> String {
434 const S: [u32; 64] = [
436 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
437 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
438 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
439 ];
440
441 const K: [u32; 64] = [
443 0xd76a_a478,
444 0xe8c7_b756,
445 0x2420_70db,
446 0xc1bd_ceee,
447 0xf57c_0faf,
448 0x4787_c62a,
449 0xa830_4613,
450 0xfd46_9501,
451 0x6980_98d8,
452 0x8b44_f7af,
453 0xffff_5bb1,
454 0x895c_d7be,
455 0x6b90_1122,
456 0xfd98_7193,
457 0xa679_438e,
458 0x49b4_0821,
459 0xf61e_2562,
460 0xc040_b340,
461 0x265e_5a51,
462 0xe9b6_c7aa,
463 0xd62f_105d,
464 0x0244_1453,
465 0xd8a1_e681,
466 0xe7d3_fbc8,
467 0x21e1_cde6,
468 0xc337_07d6,
469 0xf4d5_0d87,
470 0x455a_14ed,
471 0xa9e3_e905,
472 0xfcef_a3f8,
473 0x676f_02d9,
474 0x8d2a_4c8a,
475 0xfffa_3942,
476 0x8771_f681,
477 0x6d9d_6122,
478 0xfde5_380c,
479 0xa4be_ea44,
480 0x4bde_cfa9,
481 0xf6bb_4b60,
482 0xbebf_bc70,
483 0x289b_7ec6,
484 0xeaa1_27fa,
485 0xd4ef_3085,
486 0x0488_1d05,
487 0xd9d4_d039,
488 0xe6db_99e5,
489 0x1fa2_7cf8,
490 0xc4ac_5665,
491 0xf429_2244,
492 0x432a_ff97,
493 0xab94_23a7,
494 0xfc93_a039,
495 0x655b_59c3,
496 0x8f0c_cc92,
497 0xffef_f47d,
498 0x8584_5dd1,
499 0x6fa8_7e4f,
500 0xfe2c_e6e0,
501 0xa301_4314,
502 0x4e08_11a1,
503 0xf753_7e82,
504 0xbd3a_f235,
505 0x2ad7_d2bb,
506 0xeb86_d391,
507 ];
508
509 let orig_len_bits = (data.len() as u64).wrapping_mul(8);
511 let mut msg = data.to_vec();
512 msg.push(0x80);
513 while msg.len() % 64 != 56 {
514 msg.push(0);
515 }
516 msg.extend_from_slice(&orig_len_bits.to_le_bytes());
517
518 let mut a0: u32 = 0x6745_2301;
519 let mut b0: u32 = 0xefcd_ab89;
520 let mut c0: u32 = 0x98ba_dcfe;
521 let mut d0: u32 = 0x1032_5476;
522
523 for chunk in msg.chunks_exact(64) {
524 let mut m = [0u32; 16];
525 for (word, quad) in m.iter_mut().zip(chunk.chunks_exact(4)) {
526 if let Ok(bytes) = <[u8; 4]>::try_from(quad) {
529 *word = u32::from_le_bytes(bytes);
530 }
531 }
532
533 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
534
535 for i in 0..64 {
536 let (f, g) = match i {
537 0..=15 => ((b & c) | ((!b) & d), i),
538 16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
539 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
540 _ => (c ^ (b | (!d)), (7 * i) % 16),
541 };
542 let f = f.wrapping_add(a).wrapping_add(K[i]).wrapping_add(m[g]);
543 a = d;
544 d = c;
545 c = b;
546 b = b.wrapping_add(f.rotate_left(S[i]));
547 }
548
549 a0 = a0.wrapping_add(a);
550 b0 = b0.wrapping_add(b);
551 c0 = c0.wrapping_add(c);
552 d0 = d0.wrapping_add(d);
553 }
554
555 let digest = [
556 a0.to_le_bytes(),
557 b0.to_le_bytes(),
558 c0.to_le_bytes(),
559 d0.to_le_bytes(),
560 ];
561 let mut hex = String::with_capacity(32);
562 for group in &digest {
563 for &byte in group {
564 use fmt::Write;
565 let _ = write!(hex, "{byte:02x}");
566 }
567 }
568 hex
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586pub struct Ja4 {
587 pub fingerprint: String,
589}
590
591impl fmt::Display for Ja4 {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 f.write_str(&self.fingerprint)
594 }
595}
596
597fn truncate_hex(s: &str, n: usize) -> &str {
603 let end = s.len().min(n);
606 &s[..end]
607}
608
609const GREASE_VALUES: &[u16] = &[
611 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba,
612 0xcaca, 0xdada, 0xeaea, 0xfafa,
613];
614
615fn is_grease(v: u16) -> bool {
617 GREASE_VALUES.contains(&v)
618}
619
620impl TlsProfile {
621 pub fn ja3(&self) -> Ja3Hash {
639 let tls_ver = self
641 .tls_versions
642 .iter()
643 .map(|v| v.iana_value())
644 .max()
645 .unwrap_or(TlsVersion::Tls12.iana_value());
646
647 let ciphers: Vec<String> = self
649 .cipher_suites
650 .iter()
651 .filter(|c| !is_grease(c.0))
652 .map(|c| c.0.to_string())
653 .collect();
654
655 let extensions: Vec<String> = self
657 .extensions
658 .iter()
659 .filter(|e| !is_grease(e.0))
660 .map(|e| e.0.to_string())
661 .collect();
662
663 let curves: Vec<String> = self
665 .supported_groups
666 .iter()
667 .filter(|g| !is_grease(g.iana_value()))
668 .map(|g| g.iana_value().to_string())
669 .collect();
670
671 let ec_point_formats = "0";
673
674 let raw = format!(
675 "{tls_ver},{},{},{},{ec_point_formats}",
676 ciphers.join("-"),
677 extensions.join("-"),
678 curves.join("-"),
679 );
680
681 let hash = md5_hex(raw.as_bytes());
682 Ja3Hash { raw, hash }
683 }
684
685 pub fn ja4(&self) -> Ja4 {
705 let proto = 't';
707
708 let version = if self.tls_versions.contains(&TlsVersion::Tls13) {
710 "13"
711 } else {
712 "12"
713 };
714
715 let sni = 'd';
718
719 let cipher_count = self
721 .cipher_suites
722 .iter()
723 .filter(|c| !is_grease(c.0))
724 .count()
725 .min(99);
726 let ext_count = self
727 .extensions
728 .iter()
729 .filter(|e| !is_grease(e.0))
730 .count()
731 .min(99);
732
733 let alpn_tag = match self.alpn_protocols.first() {
736 Some(AlpnProtocol::H2) => "h2",
737 Some(AlpnProtocol::Http11) => "h1",
738 None => "00",
739 };
740
741 let section_a = format!("{proto}{version}{sni}{cipher_count:02}{ext_count:02}_{alpn_tag}",);
743
744 let mut sorted_ciphers: Vec<u16> = self
747 .cipher_suites
748 .iter()
749 .filter(|c| !is_grease(c.0))
750 .map(|c| c.0)
751 .collect();
752 sorted_ciphers.sort_unstable();
753 let cipher_str: String = sorted_ciphers
754 .iter()
755 .map(|c| format!("{c:04x}"))
756 .collect::<Vec<_>>()
757 .join(",");
758 let cipher_hash_full = md5_hex(cipher_str.as_bytes());
759 let cipher_hash = truncate_hex(&cipher_hash_full, 12);
760
761 let mut sorted_exts: Vec<u16> = self
764 .extensions
765 .iter()
766 .filter(|e| {
767 !is_grease(e.0)
768 && e.0 != TlsExtensionId::SERVER_NAME.0
769 && e.0 != TlsExtensionId::ALPN.0
770 })
771 .map(|e| e.0)
772 .collect();
773 sorted_exts.sort_unstable();
774 let ext_str: String = sorted_exts
775 .iter()
776 .map(|e| format!("{e:04x}"))
777 .collect::<Vec<_>>()
778 .join(",");
779 let ext_hash_full = md5_hex(ext_str.as_bytes());
780 let ext_hash = truncate_hex(&ext_hash_full, 12);
781
782 Ja4 {
783 fingerprint: format!("{section_a}_{cipher_hash}_{ext_hash}"),
784 }
785 }
786
787 pub fn random_weighted(seed: u64) -> &'static Self {
808 let os_roll = rng(seed, 97) % 100;
810
811 let browser_roll = rng(seed, 201) % 100;
813
814 match os_roll {
815 0..=69 | 90..=99 => match browser_roll {
817 0..=64 => &CHROME_131,
818 65..=80 => &EDGE_131,
819 _ => &FIREFOX_133,
820 },
821 _ => match browser_roll {
823 0..=55 => &CHROME_131,
824 56..=91 => &SAFARI_18,
825 _ => &FIREFOX_133,
826 },
827 }
828 }
829}
830
831pub static CHROME_131: LazyLock<TlsProfile> = LazyLock::new(|| TlsProfile {
847 name: "Chrome 131".to_string(),
848 cipher_suites: vec![
849 CipherSuiteId::TLS_AES_128_GCM_SHA256,
850 CipherSuiteId::TLS_AES_256_GCM_SHA384,
851 CipherSuiteId::TLS_CHACHA20_POLY1305_SHA256,
852 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
853 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
854 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
855 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
856 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
857 CipherSuiteId::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
858 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
859 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
860 CipherSuiteId::TLS_RSA_WITH_AES_128_GCM_SHA256,
861 CipherSuiteId::TLS_RSA_WITH_AES_256_GCM_SHA384,
862 CipherSuiteId::TLS_RSA_WITH_AES_128_CBC_SHA,
863 CipherSuiteId::TLS_RSA_WITH_AES_256_CBC_SHA,
864 ],
865 tls_versions: vec![TlsVersion::Tls12, TlsVersion::Tls13],
866 extensions: vec![
867 TlsExtensionId::SERVER_NAME,
868 TlsExtensionId::EXTENDED_MASTER_SECRET,
869 TlsExtensionId::RENEGOTIATION_INFO,
870 TlsExtensionId::SUPPORTED_GROUPS,
871 TlsExtensionId::EC_POINT_FORMATS,
872 TlsExtensionId::SESSION_TICKET,
873 TlsExtensionId::ALPN,
874 TlsExtensionId::STATUS_REQUEST,
875 TlsExtensionId::SIGNATURE_ALGORITHMS,
876 TlsExtensionId::SIGNED_CERTIFICATE_TIMESTAMP,
877 TlsExtensionId::KEY_SHARE,
878 TlsExtensionId::PSK_KEY_EXCHANGE_MODES,
879 TlsExtensionId::SUPPORTED_VERSIONS,
880 TlsExtensionId::COMPRESS_CERTIFICATE,
881 TlsExtensionId::APPLICATION_SETTINGS,
882 TlsExtensionId::PADDING,
883 ],
884 supported_groups: vec![
885 SupportedGroup::X25519Kyber768,
886 SupportedGroup::X25519,
887 SupportedGroup::SecP256r1,
888 SupportedGroup::SecP384r1,
889 ],
890 signature_algorithms: vec![
891 SignatureAlgorithm::ECDSA_SECP256R1_SHA256,
892 SignatureAlgorithm::RSA_PSS_RSAE_SHA256,
893 SignatureAlgorithm::RSA_PKCS1_SHA256,
894 SignatureAlgorithm::ECDSA_SECP384R1_SHA384,
895 SignatureAlgorithm::RSA_PSS_RSAE_SHA384,
896 SignatureAlgorithm::RSA_PKCS1_SHA384,
897 SignatureAlgorithm::RSA_PSS_RSAE_SHA512,
898 SignatureAlgorithm::RSA_PKCS1_SHA512,
899 ],
900 alpn_protocols: vec![AlpnProtocol::H2, AlpnProtocol::Http11],
901});
902
903pub static FIREFOX_133: LazyLock<TlsProfile> = LazyLock::new(|| TlsProfile {
917 name: "Firefox 133".to_string(),
918 cipher_suites: vec![
919 CipherSuiteId::TLS_AES_128_GCM_SHA256,
920 CipherSuiteId::TLS_CHACHA20_POLY1305_SHA256,
921 CipherSuiteId::TLS_AES_256_GCM_SHA384,
922 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
923 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
924 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
925 CipherSuiteId::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
926 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
927 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
928 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
929 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
930 CipherSuiteId::TLS_RSA_WITH_AES_128_GCM_SHA256,
931 CipherSuiteId::TLS_RSA_WITH_AES_256_GCM_SHA384,
932 CipherSuiteId::TLS_RSA_WITH_AES_128_CBC_SHA,
933 CipherSuiteId::TLS_RSA_WITH_AES_256_CBC_SHA,
934 ],
935 tls_versions: vec![TlsVersion::Tls12, TlsVersion::Tls13],
936 extensions: vec![
937 TlsExtensionId::SERVER_NAME,
938 TlsExtensionId::EXTENDED_MASTER_SECRET,
939 TlsExtensionId::RENEGOTIATION_INFO,
940 TlsExtensionId::SUPPORTED_GROUPS,
941 TlsExtensionId::EC_POINT_FORMATS,
942 TlsExtensionId::SESSION_TICKET,
943 TlsExtensionId::ALPN,
944 TlsExtensionId::STATUS_REQUEST,
945 TlsExtensionId::DELEGATED_CREDENTIALS,
946 TlsExtensionId::KEY_SHARE,
947 TlsExtensionId::SUPPORTED_VERSIONS,
948 TlsExtensionId::SIGNATURE_ALGORITHMS,
949 TlsExtensionId::PSK_KEY_EXCHANGE_MODES,
950 TlsExtensionId::RECORD_SIZE_LIMIT,
951 TlsExtensionId::POST_HANDSHAKE_AUTH,
952 TlsExtensionId::PADDING,
953 ],
954 supported_groups: vec![
955 SupportedGroup::X25519,
956 SupportedGroup::SecP256r1,
957 SupportedGroup::SecP384r1,
958 SupportedGroup::SecP521r1,
959 SupportedGroup::Ffdhe2048,
960 SupportedGroup::Ffdhe3072,
961 ],
962 signature_algorithms: vec![
963 SignatureAlgorithm::ECDSA_SECP256R1_SHA256,
964 SignatureAlgorithm::ECDSA_SECP384R1_SHA384,
965 SignatureAlgorithm::ECDSA_SECP521R1_SHA512,
966 SignatureAlgorithm::RSA_PSS_RSAE_SHA256,
967 SignatureAlgorithm::RSA_PSS_RSAE_SHA384,
968 SignatureAlgorithm::RSA_PSS_RSAE_SHA512,
969 SignatureAlgorithm::RSA_PKCS1_SHA256,
970 SignatureAlgorithm::RSA_PKCS1_SHA384,
971 SignatureAlgorithm::RSA_PKCS1_SHA512,
972 SignatureAlgorithm::ECDSA_SHA1,
973 SignatureAlgorithm::RSA_PKCS1_SHA1,
974 ],
975 alpn_protocols: vec![AlpnProtocol::H2, AlpnProtocol::Http11],
976});
977
978pub static SAFARI_18: LazyLock<TlsProfile> = LazyLock::new(|| TlsProfile {
991 name: "Safari 18".to_string(),
992 cipher_suites: vec![
993 CipherSuiteId::TLS_AES_128_GCM_SHA256,
994 CipherSuiteId::TLS_AES_256_GCM_SHA384,
995 CipherSuiteId::TLS_CHACHA20_POLY1305_SHA256,
996 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
997 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
998 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
999 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
1000 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
1001 CipherSuiteId::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
1002 CipherSuiteId::TLS_RSA_WITH_AES_256_GCM_SHA384,
1003 CipherSuiteId::TLS_RSA_WITH_AES_128_GCM_SHA256,
1004 CipherSuiteId::TLS_RSA_WITH_AES_256_CBC_SHA,
1005 CipherSuiteId::TLS_RSA_WITH_AES_128_CBC_SHA,
1006 ],
1007 tls_versions: vec![TlsVersion::Tls12, TlsVersion::Tls13],
1008 extensions: vec![
1009 TlsExtensionId::SERVER_NAME,
1010 TlsExtensionId::EXTENDED_MASTER_SECRET,
1011 TlsExtensionId::RENEGOTIATION_INFO,
1012 TlsExtensionId::SUPPORTED_GROUPS,
1013 TlsExtensionId::EC_POINT_FORMATS,
1014 TlsExtensionId::ALPN,
1015 TlsExtensionId::STATUS_REQUEST,
1016 TlsExtensionId::SIGNATURE_ALGORITHMS,
1017 TlsExtensionId::SIGNED_CERTIFICATE_TIMESTAMP,
1018 TlsExtensionId::KEY_SHARE,
1019 TlsExtensionId::PSK_KEY_EXCHANGE_MODES,
1020 TlsExtensionId::SUPPORTED_VERSIONS,
1021 TlsExtensionId::PADDING,
1022 ],
1023 supported_groups: vec![
1024 SupportedGroup::X25519,
1025 SupportedGroup::SecP256r1,
1026 SupportedGroup::SecP384r1,
1027 SupportedGroup::SecP521r1,
1028 ],
1029 signature_algorithms: vec![
1030 SignatureAlgorithm::ECDSA_SECP256R1_SHA256,
1031 SignatureAlgorithm::RSA_PSS_RSAE_SHA256,
1032 SignatureAlgorithm::RSA_PKCS1_SHA256,
1033 SignatureAlgorithm::ECDSA_SECP384R1_SHA384,
1034 SignatureAlgorithm::RSA_PSS_RSAE_SHA384,
1035 SignatureAlgorithm::RSA_PKCS1_SHA384,
1036 SignatureAlgorithm::RSA_PSS_RSAE_SHA512,
1037 SignatureAlgorithm::RSA_PKCS1_SHA512,
1038 ],
1039 alpn_protocols: vec![AlpnProtocol::H2, AlpnProtocol::Http11],
1040});
1041
1042pub static EDGE_131: LazyLock<TlsProfile> = LazyLock::new(|| TlsProfile {
1055 name: "Edge 131".to_string(),
1056 cipher_suites: vec![
1057 CipherSuiteId::TLS_AES_128_GCM_SHA256,
1058 CipherSuiteId::TLS_AES_256_GCM_SHA384,
1059 CipherSuiteId::TLS_CHACHA20_POLY1305_SHA256,
1060 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
1061 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
1062 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
1063 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
1064 CipherSuiteId::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
1065 CipherSuiteId::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
1066 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
1067 CipherSuiteId::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
1068 CipherSuiteId::TLS_RSA_WITH_AES_128_GCM_SHA256,
1069 CipherSuiteId::TLS_RSA_WITH_AES_256_GCM_SHA384,
1070 CipherSuiteId::TLS_RSA_WITH_AES_128_CBC_SHA,
1071 CipherSuiteId::TLS_RSA_WITH_AES_256_CBC_SHA,
1072 ],
1073 tls_versions: vec![TlsVersion::Tls12, TlsVersion::Tls13],
1074 extensions: vec![
1075 TlsExtensionId::SERVER_NAME,
1076 TlsExtensionId::EXTENDED_MASTER_SECRET,
1077 TlsExtensionId::RENEGOTIATION_INFO,
1078 TlsExtensionId::SUPPORTED_GROUPS,
1079 TlsExtensionId::EC_POINT_FORMATS,
1080 TlsExtensionId::SESSION_TICKET,
1081 TlsExtensionId::ALPN,
1082 TlsExtensionId::STATUS_REQUEST,
1083 TlsExtensionId::SIGNATURE_ALGORITHMS,
1084 TlsExtensionId::SIGNED_CERTIFICATE_TIMESTAMP,
1085 TlsExtensionId::KEY_SHARE,
1086 TlsExtensionId::PSK_KEY_EXCHANGE_MODES,
1087 TlsExtensionId::SUPPORTED_VERSIONS,
1088 TlsExtensionId::COMPRESS_CERTIFICATE,
1089 TlsExtensionId::PADDING,
1090 ],
1091 supported_groups: vec![
1092 SupportedGroup::X25519Kyber768,
1093 SupportedGroup::X25519,
1094 SupportedGroup::SecP256r1,
1095 SupportedGroup::SecP384r1,
1096 ],
1097 signature_algorithms: vec![
1098 SignatureAlgorithm::ECDSA_SECP256R1_SHA256,
1099 SignatureAlgorithm::RSA_PSS_RSAE_SHA256,
1100 SignatureAlgorithm::RSA_PKCS1_SHA256,
1101 SignatureAlgorithm::ECDSA_SECP384R1_SHA384,
1102 SignatureAlgorithm::RSA_PSS_RSAE_SHA384,
1103 SignatureAlgorithm::RSA_PKCS1_SHA384,
1104 SignatureAlgorithm::RSA_PSS_RSAE_SHA512,
1105 SignatureAlgorithm::RSA_PKCS1_SHA512,
1106 ],
1107 alpn_protocols: vec![AlpnProtocol::H2, AlpnProtocol::Http11],
1108});
1109
1110pub fn chrome_tls_args(profile: &TlsProfile) -> Vec<String> {
1141 let has_12 = profile.tls_versions.contains(&TlsVersion::Tls12);
1142 let has_13 = profile.tls_versions.contains(&TlsVersion::Tls13);
1143
1144 let mut args = Vec::new();
1145
1146 match (has_12, has_13) {
1147 (true, false) => {
1149 args.push("--ssl-version-max=tls1.2".to_string());
1150 }
1151 (false, true) => {
1153 args.push("--ssl-version-min=tls1.3".to_string());
1154 }
1155 _ => {}
1157 }
1158
1159 args
1160}
1161
1162#[cfg(feature = "tls-config")]
1169mod rustls_config {
1170 #[allow(clippy::wildcard_imports)]
1171 use super::*;
1172 use std::sync::Arc;
1173
1174 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1194 pub struct TlsControl {
1195 pub strict_cipher_suites: bool,
1197 pub strict_supported_groups: bool,
1199 pub fallback_to_provider_groups: bool,
1201 pub allow_legacy_compat_suites: bool,
1203 }
1204
1205 impl Default for TlsControl {
1206 fn default() -> Self {
1207 Self::compatible()
1208 }
1209 }
1210
1211 impl TlsControl {
1212 #[must_use]
1214 pub const fn compatible() -> Self {
1215 Self {
1216 strict_cipher_suites: false,
1217 strict_supported_groups: false,
1218 fallback_to_provider_groups: true,
1219 allow_legacy_compat_suites: true,
1220 }
1221 }
1222
1223 #[must_use]
1225 pub const fn strict() -> Self {
1226 Self {
1227 strict_cipher_suites: true,
1228 strict_supported_groups: false,
1229 fallback_to_provider_groups: true,
1230 allow_legacy_compat_suites: true,
1231 }
1232 }
1233
1234 #[must_use]
1236 pub const fn strict_all() -> Self {
1237 Self {
1238 strict_cipher_suites: true,
1239 strict_supported_groups: true,
1240 fallback_to_provider_groups: false,
1241 allow_legacy_compat_suites: true,
1242 }
1243 }
1244
1245 #[must_use]
1251 pub fn for_profile(profile: &TlsProfile) -> Self {
1252 let name = profile.name.to_ascii_lowercase();
1253 if name.contains("chrome")
1254 || name.contains("edge")
1255 || name.contains("firefox")
1256 || name.contains("safari")
1257 {
1258 Self::strict()
1259 } else {
1260 Self::compatible()
1261 }
1262 }
1263 }
1264
1265 const fn is_legacy_compat_suite(id: u16) -> bool {
1266 matches!(id, 0xc013 | 0xc014 | 0x009c | 0x009d | 0x002f | 0x0035)
1267 }
1268
1269 #[derive(Debug, thiserror::Error)]
1272 #[non_exhaustive]
1273 pub enum TlsConfigError {
1274 #[error("no supported cipher suites in profile '{0}'")]
1277 NoCipherSuites(String),
1278
1279 #[error(
1281 "unsupported cipher suite {cipher_suite_id:#06x} in profile '{profile}' under strict mode"
1282 )]
1283 UnsupportedCipherSuite {
1284 profile: String,
1286 cipher_suite_id: u16,
1288 },
1289
1290 #[error(
1292 "unsupported supported_group {group_id:#06x} in profile '{profile}' under strict mode"
1293 )]
1294 UnsupportedSupportedGroup {
1295 profile: String,
1297 group_id: u16,
1299 },
1300
1301 #[error("no supported key-exchange groups in profile '{0}'")]
1303 NoSupportedGroups(String),
1304
1305 #[error("rustls configuration: {0}")]
1307 Rustls(#[from] rustls::Error),
1308 }
1309
1310 #[derive(Debug, Clone)]
1316 pub struct TlsClientConfig(Arc<rustls::ClientConfig>);
1317
1318 impl TlsClientConfig {
1319 pub fn inner(&self) -> &rustls::ClientConfig {
1321 &self.0
1322 }
1323
1324 pub fn into_inner(self) -> Arc<rustls::ClientConfig> {
1326 self.0
1327 }
1328 }
1329
1330 impl From<TlsClientConfig> for Arc<rustls::ClientConfig> {
1331 fn from(cfg: TlsClientConfig) -> Self {
1332 cfg.0
1333 }
1334 }
1335
1336 impl TlsProfile {
1337 pub fn to_rustls_config(&self) -> Result<TlsClientConfig, TlsConfigError> {
1366 self.to_rustls_config_with_control(TlsControl::default())
1367 }
1368
1369 pub fn to_rustls_config_with_control(
1390 &self,
1391 control: TlsControl,
1392 ) -> Result<TlsClientConfig, TlsConfigError> {
1393 let default = rustls::crypto::aws_lc_rs::default_provider();
1394
1395 let suite_map: std::collections::HashMap<u16, rustls::SupportedCipherSuite> = default
1397 .cipher_suites
1398 .iter()
1399 .map(|cs| (u16::from(cs.suite()), *cs))
1400 .collect();
1401
1402 let mut ordered_suites: Vec<rustls::SupportedCipherSuite> = Vec::new();
1403 for id in &self.cipher_suites {
1404 if let Some(cs) = suite_map.get(&id.0).copied() {
1405 ordered_suites.push(cs);
1406 } else if control.allow_legacy_compat_suites && is_legacy_compat_suite(id.0) {
1407 tracing::warn!(
1408 cipher_suite_id = id.0,
1409 profile = %self.name,
1410 "legacy profile suite has no rustls equivalent, skipping"
1411 );
1412 } else if control.strict_cipher_suites {
1413 return Err(TlsConfigError::UnsupportedCipherSuite {
1414 profile: self.name.clone(),
1415 cipher_suite_id: id.0,
1416 });
1417 } else {
1418 tracing::warn!(
1419 cipher_suite_id = id.0,
1420 profile = %self.name,
1421 "cipher suite not supported by rustls aws-lc-rs backend, skipping"
1422 );
1423 }
1424 }
1425
1426 if ordered_suites.is_empty() {
1427 return Err(TlsConfigError::NoCipherSuites(self.name.clone()));
1428 }
1429
1430 let group_map: std::collections::HashMap<
1432 u16,
1433 &'static dyn rustls::crypto::SupportedKxGroup,
1434 > = default
1435 .kx_groups
1436 .iter()
1437 .map(|g| (u16::from(g.name()), *g))
1438 .collect();
1439
1440 let mut ordered_groups: Vec<&'static dyn rustls::crypto::SupportedKxGroup> = Vec::new();
1441 for sg in &self.supported_groups {
1442 if let Some(group) = group_map.get(&sg.iana_value()).copied() {
1443 ordered_groups.push(group);
1444 } else if control.strict_supported_groups {
1445 return Err(TlsConfigError::UnsupportedSupportedGroup {
1446 profile: self.name.clone(),
1447 group_id: sg.iana_value(),
1448 });
1449 } else {
1450 tracing::warn!(
1451 group_id = sg.iana_value(),
1452 profile = %self.name,
1453 "key-exchange group not supported by rustls, skipping"
1454 );
1455 }
1456 }
1457
1458 let kx_groups = if ordered_groups.is_empty() && control.fallback_to_provider_groups {
1460 default.kx_groups.clone()
1461 } else if ordered_groups.is_empty() {
1462 return Err(TlsConfigError::NoSupportedGroups(self.name.clone()));
1463 } else {
1464 ordered_groups
1465 };
1466
1467 let provider = rustls::crypto::CryptoProvider {
1469 cipher_suites: ordered_suites,
1470 kx_groups,
1471 ..default
1472 };
1473
1474 let versions: Vec<&'static rustls::SupportedProtocolVersion> = self
1476 .tls_versions
1477 .iter()
1478 .map(|v| match v {
1479 TlsVersion::Tls12 => &rustls::version::TLS12,
1480 TlsVersion::Tls13 => &rustls::version::TLS13,
1481 })
1482 .collect();
1483
1484 let mut root_store = rustls::RootCertStore::empty();
1486 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1487
1488 let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(provider))
1490 .with_protocol_versions(&versions)?
1491 .with_root_certificates(root_store)
1492 .with_no_client_auth();
1493
1494 config.alpn_protocols = self
1496 .alpn_protocols
1497 .iter()
1498 .map(|p| p.as_str().as_bytes().to_vec())
1499 .collect();
1500
1501 Ok(TlsClientConfig(Arc::new(config)))
1502 }
1503 }
1504}
1505
1506#[cfg(feature = "tls-config")]
1507pub use rustls_config::{TlsClientConfig, TlsConfigError};
1508
1509#[cfg(feature = "tls-config")]
1510pub use rustls_config::TlsControl;
1511
1512#[cfg(feature = "tls-config")]
1519mod reqwest_client {
1520 #[allow(clippy::wildcard_imports)]
1521 use super::*;
1522 use std::sync::Arc;
1523
1524 #[derive(Debug, thiserror::Error)]
1526 #[non_exhaustive]
1527 pub enum TlsClientError {
1528 #[error(transparent)]
1530 TlsConfig(#[from] super::rustls_config::TlsConfigError),
1531
1532 #[error("reqwest client: {0}")]
1534 Reqwest(#[from] reqwest::Error),
1535 }
1536
1537 pub fn default_user_agent(profile: &TlsProfile) -> &'static str {
1553 let name = profile.name.to_ascii_lowercase();
1554 if name.contains("firefox") {
1555 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0"
1556 } else if name.contains("safari") && !name.contains("chrome") {
1557 "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15"
1558 } else if name.contains("edge") {
1559 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
1560 } else {
1561 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
1563 }
1564 }
1565
1566 pub fn profile_for_device(device: &crate::fingerprint::DeviceProfile) -> &'static TlsProfile {
1577 use crate::fingerprint::DeviceProfile;
1578 match device {
1579 DeviceProfile::DesktopWindows | DeviceProfile::MobileAndroid => &CHROME_131,
1580 DeviceProfile::DesktopMac | DeviceProfile::MobileIOS => &SAFARI_18,
1581 DeviceProfile::DesktopLinux => &FIREFOX_133,
1582 }
1583 }
1584
1585 pub fn browser_headers(profile: &TlsProfile) -> reqwest::header::HeaderMap {
1604 use reqwest::header::{
1605 ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, HeaderMap, HeaderValue,
1606 UPGRADE_INSECURE_REQUESTS,
1607 };
1608
1609 let mut map = HeaderMap::new();
1610 let name = profile.name.to_ascii_lowercase();
1611
1612 let is_firefox = name.contains("firefox");
1613 let is_safari = name.contains("safari") && !name.contains("chrome");
1614 let is_chromium = !(is_firefox || is_safari);
1615
1616 let accept = if is_chromium {
1618 "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"
1620 } else {
1621 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
1622 };
1623
1624 let accept_encoding = "gzip, deflate, br";
1626
1627 let accept_language = "en-US,en;q=0.9";
1631
1632 if is_chromium {
1634 let (brand, version) = if name.contains("edge") {
1635 ("\"Microsoft Edge\";v=\"131\"", "131")
1636 } else {
1637 ("\"Google Chrome\";v=\"131\"", "131")
1638 };
1639
1640 let sec_ch_ua =
1641 format!("{brand}, \"Chromium\";v=\"{version}\", \"Not_A Brand\";v=\"24\"");
1642
1643 if let Ok(v) = HeaderValue::from_str(&sec_ch_ua) {
1646 map.insert("sec-ch-ua", v);
1647 }
1648 map.insert("sec-ch-ua-mobile", HeaderValue::from_static("?0"));
1649 map.insert(
1650 "sec-ch-ua-platform",
1651 HeaderValue::from_static("\"Windows\""),
1652 );
1653 map.insert("sec-fetch-dest", HeaderValue::from_static("document"));
1654 map.insert("sec-fetch-mode", HeaderValue::from_static("navigate"));
1655 map.insert("sec-fetch-site", HeaderValue::from_static("none"));
1656 map.insert("sec-fetch-user", HeaderValue::from_static("?1"));
1657 map.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
1658 }
1659
1660 if let Ok(v) = HeaderValue::from_str(accept) {
1661 map.insert(ACCEPT, v);
1662 }
1663 map.insert(ACCEPT_ENCODING, HeaderValue::from_static(accept_encoding));
1664 map.insert(ACCEPT_LANGUAGE, HeaderValue::from_static(accept_language));
1665 map.insert(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
1666
1667 map
1668 }
1669
1670 pub fn build_profiled_client(
1696 profile: &TlsProfile,
1697 proxy_url: Option<&str>,
1698 ) -> Result<reqwest::Client, TlsClientError> {
1699 build_profiled_client_with_control(profile, proxy_url, TlsControl::default())
1700 }
1701
1702 pub fn build_profiled_client_preset(
1716 profile: &TlsProfile,
1717 proxy_url: Option<&str>,
1718 ) -> Result<reqwest::Client, TlsClientError> {
1719 build_profiled_client_with_control(profile, proxy_url, TlsControl::for_profile(profile))
1720 }
1721
1722 pub fn build_profiled_client_with_control(
1740 profile: &TlsProfile,
1741 proxy_url: Option<&str>,
1742 control: TlsControl,
1743 ) -> Result<reqwest::Client, TlsClientError> {
1744 let tls_config = profile.to_rustls_config_with_control(control)?;
1745
1746 let rustls_cfg =
1748 Arc::try_unwrap(tls_config.into_inner()).unwrap_or_else(|arc| (*arc).clone());
1749
1750 let mut builder = reqwest::Client::builder()
1751 .use_preconfigured_tls(rustls_cfg)
1752 .user_agent(default_user_agent(profile))
1753 .default_headers(browser_headers(profile))
1754 .cookie_store(true)
1755 .gzip(true)
1756 .brotli(true);
1757
1758 if let Some(url) = proxy_url {
1759 builder = builder.proxy(reqwest::Proxy::all(url)?);
1760 }
1761
1762 Ok(builder.build()?)
1763 }
1764
1765 pub fn build_profiled_client_strict(
1779 profile: &TlsProfile,
1780 proxy_url: Option<&str>,
1781 ) -> Result<reqwest::Client, TlsClientError> {
1782 build_profiled_client_with_control(profile, proxy_url, TlsControl::strict())
1783 }
1784}
1785
1786#[cfg(feature = "tls-config")]
1787pub use reqwest_client::{
1788 TlsClientError, browser_headers, build_profiled_client, build_profiled_client_preset,
1789 build_profiled_client_strict, build_profiled_client_with_control, default_user_agent,
1790 profile_for_device,
1791};
1792
1793#[cfg(test)]
1796#[allow(clippy::panic, clippy::unwrap_used)]
1797mod tests {
1798 use super::*;
1799
1800 #[test]
1801 fn md5_known_vectors() {
1802 assert_eq!(md5_hex(b""), "d41d8cd98f00b204e9800998ecf8427e");
1804 assert_eq!(md5_hex(b"a"), "0cc175b9c0f1b6a831c399e269772661");
1805 assert_eq!(md5_hex(b"abc"), "900150983cd24fb0d6963f7d28e17f72");
1806 assert_eq!(
1807 md5_hex(b"message digest"),
1808 "f96b697d7cb7938d525a2f31aaf161d0"
1809 );
1810 }
1811
1812 #[test]
1813 fn chrome_131_ja3_structure() {
1814 let ja3 = CHROME_131.ja3();
1815 assert!(
1819 ja3.raw.starts_with("772,"),
1820 "JA3 raw should start with '772,' but was: {}",
1821 ja3.raw
1822 );
1823 assert_eq!(ja3.raw.matches(',').count(), 4);
1825 assert_eq!(ja3.hash.len(), 32);
1827 assert!(ja3.hash.chars().all(|c| c.is_ascii_hexdigit()));
1828 }
1829
1830 #[test]
1831 fn firefox_133_ja3_differs_from_chrome() {
1832 let chrome_ja3 = CHROME_131.ja3();
1833 let firefox_ja3 = FIREFOX_133.ja3();
1834 assert_ne!(chrome_ja3.hash, firefox_ja3.hash);
1835 assert_ne!(chrome_ja3.raw, firefox_ja3.raw);
1836 }
1837
1838 #[test]
1839 fn safari_18_ja3_is_valid() {
1840 let ja3 = SAFARI_18.ja3();
1841 assert!(ja3.raw.starts_with("772,"));
1842 assert_eq!(ja3.hash.len(), 32);
1843 }
1844
1845 #[test]
1846 fn edge_131_ja3_differs_from_chrome() {
1847 let chrome_ja3 = CHROME_131.ja3();
1849 let edge_ja3 = EDGE_131.ja3();
1850 assert_ne!(chrome_ja3.hash, edge_ja3.hash);
1851 }
1852
1853 #[test]
1854 fn chrome_131_ja4_format() {
1855 let ja4 = CHROME_131.ja4();
1856 assert!(
1858 ja4.fingerprint.starts_with("t13d"),
1859 "JA4 should start with 't13d' but was: {}",
1860 ja4.fingerprint
1861 );
1862 assert_eq!(
1864 ja4.fingerprint.matches('_').count(),
1865 3,
1866 "JA4 should have three underscores: {}",
1867 ja4.fingerprint
1868 );
1869 }
1870
1871 #[test]
1872 fn ja4_firefox_differs_from_chrome() {
1873 let chrome_ja4 = CHROME_131.ja4();
1874 let firefox_ja4 = FIREFOX_133.ja4();
1875 assert_ne!(chrome_ja4.fingerprint, firefox_ja4.fingerprint);
1876 }
1877
1878 #[test]
1879 fn random_weighted_distribution() {
1880 let mut chrome_count = 0u32;
1881 let mut firefox_count = 0u32;
1882 let mut edge_count = 0u32;
1883 let mut safari_count = 0u32;
1884
1885 let total = 10_000u32;
1886 for i in 0..total {
1887 let profile = TlsProfile::random_weighted(u64::from(i));
1888 match profile.name.as_str() {
1889 "Chrome 131" => chrome_count += 1,
1890 "Firefox 133" => firefox_count += 1,
1891 "Edge 131" => edge_count += 1,
1892 "Safari 18" => safari_count += 1,
1893 other => unreachable!("unexpected profile: {other}"),
1894 }
1895 }
1896
1897 assert!(
1899 chrome_count > total * 40 / 100,
1900 "Chrome share too low: {chrome_count}/{total}"
1901 );
1902 assert!(
1904 firefox_count > total * 5 / 100,
1905 "Firefox share too low: {firefox_count}/{total}"
1906 );
1907 assert!(
1909 edge_count > total * 5 / 100,
1910 "Edge share too low: {edge_count}/{total}"
1911 );
1912 assert!(
1914 safari_count > total * 3 / 100,
1915 "Safari share too low: {safari_count}/{total}"
1916 );
1917 }
1918
1919 #[test]
1920 fn serde_roundtrip() {
1921 let profile: &TlsProfile = &CHROME_131;
1922 let json = serde_json::to_string(profile).unwrap();
1923 let deserialized: TlsProfile = serde_json::from_str(&json).unwrap();
1924 assert_eq!(profile, &deserialized);
1925 }
1926
1927 #[test]
1928 fn ja3hash_display() {
1929 let ja3 = CHROME_131.ja3();
1930 assert_eq!(format!("{ja3}"), ja3.hash);
1931 }
1932
1933 #[test]
1934 fn ja4_display() {
1935 let ja4 = CHROME_131.ja4();
1936 assert_eq!(format!("{ja4}"), ja4.fingerprint);
1937 }
1938
1939 #[test]
1940 fn cipher_suite_display() {
1941 let cs = CipherSuiteId::TLS_AES_128_GCM_SHA256;
1942 assert_eq!(format!("{cs}"), "4865"); }
1944
1945 #[test]
1946 fn tls_version_display() {
1947 assert_eq!(format!("{}", TlsVersion::Tls13), "772");
1948 }
1949
1950 #[test]
1951 fn alpn_protocol_as_str() {
1952 assert_eq!(AlpnProtocol::H2.as_str(), "h2");
1953 assert_eq!(AlpnProtocol::Http11.as_str(), "http/1.1");
1954 }
1955
1956 #[test]
1957 fn supported_group_values() {
1958 assert_eq!(SupportedGroup::X25519.iana_value(), 0x001d);
1959 assert_eq!(SupportedGroup::SecP256r1.iana_value(), 0x0017);
1960 assert_eq!(SupportedGroup::X25519Kyber768.iana_value(), 0x6399);
1961 }
1962
1963 #[test]
1966 fn chrome_131_tls_args_empty() {
1967 let args = chrome_tls_args(&CHROME_131);
1969 assert!(args.is_empty(), "expected no flags, got: {args:?}");
1970 }
1971
1972 #[test]
1973 fn tls12_only_profile_caps_version() {
1974 let profile = TlsProfile {
1975 name: "TLS12-only".to_string(),
1976 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
1977 tls_versions: vec![TlsVersion::Tls12],
1978 extensions: vec![],
1979 supported_groups: vec![],
1980 signature_algorithms: vec![],
1981 alpn_protocols: vec![],
1982 };
1983 let args = chrome_tls_args(&profile);
1984 assert_eq!(args, vec!["--ssl-version-max=tls1.2"]);
1985 }
1986
1987 #[test]
1988 fn tls13_only_profile_raises_floor() {
1989 let profile = TlsProfile {
1990 name: "TLS13-only".to_string(),
1991 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
1992 tls_versions: vec![TlsVersion::Tls13],
1993 extensions: vec![],
1994 supported_groups: vec![],
1995 signature_algorithms: vec![],
1996 alpn_protocols: vec![],
1997 };
1998 let args = chrome_tls_args(&profile);
1999 assert_eq!(args, vec!["--ssl-version-min=tls1.3"]);
2000 }
2001
2002 #[test]
2003 fn builder_tls_profile_integration() {
2004 let cfg = crate::BrowserConfig::builder()
2005 .tls_profile(&CHROME_131)
2006 .build();
2007 let tls_flags: Vec<_> = cfg
2009 .effective_args()
2010 .into_iter()
2011 .filter(|a| a.starts_with("--ssl-version"))
2012 .collect();
2013 assert!(tls_flags.is_empty(), "unexpected TLS flags: {tls_flags:?}");
2014 }
2015
2016 #[cfg(feature = "tls-config")]
2019 mod rustls_tests {
2020 use super::super::*;
2021
2022 #[test]
2023 fn chrome_131_config_builds_successfully() {
2024 let config = CHROME_131.to_rustls_config().unwrap();
2025 let inner = config.inner();
2027 assert!(
2029 !inner.alpn_protocols.is_empty(),
2030 "ALPN protocols should be set"
2031 );
2032 }
2033
2034 #[test]
2035 #[allow(clippy::indexing_slicing)]
2036 fn alpn_order_matches_profile() {
2037 let config = CHROME_131.to_rustls_config().unwrap();
2038 let alpn = &config.inner().alpn_protocols;
2039 assert_eq!(alpn.len(), 2);
2040 assert_eq!(alpn[0], b"h2");
2041 assert_eq!(alpn[1], b"http/1.1");
2042 }
2043
2044 #[test]
2045 fn all_builtin_profiles_produce_valid_configs() {
2046 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
2047 let result = profile.to_rustls_config();
2048 assert!(
2049 result.is_ok(),
2050 "profile '{}' should produce a valid config: {:?}",
2051 profile.name,
2052 result.err()
2053 );
2054 }
2055 }
2056
2057 #[test]
2058 fn unsupported_only_suites_returns_error() {
2059 let profile = TlsProfile {
2060 name: "Bogus".to_string(),
2061 cipher_suites: vec![CipherSuiteId(0xFFFF)],
2062 tls_versions: vec![TlsVersion::Tls13],
2063 extensions: vec![],
2064 supported_groups: vec![],
2065 signature_algorithms: vec![],
2066 alpn_protocols: vec![],
2067 };
2068 let err = profile.to_rustls_config().unwrap_err();
2069 assert!(
2070 err.to_string().contains("no supported cipher suites"),
2071 "expected NoCipherSuites, got: {err}"
2072 );
2073 }
2074
2075 #[test]
2076 fn strict_mode_rejects_unknown_cipher_suite() {
2077 let profile = TlsProfile {
2078 name: "StrictCipherTest".to_string(),
2079 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256, CipherSuiteId(0xFFFF)],
2080 tls_versions: vec![TlsVersion::Tls13],
2081 extensions: vec![],
2082 supported_groups: vec![SupportedGroup::X25519],
2083 signature_algorithms: vec![],
2084 alpn_protocols: vec![],
2085 };
2086
2087 let err = profile
2088 .to_rustls_config_with_control(TlsControl::strict())
2089 .unwrap_err();
2090
2091 match err {
2092 TlsConfigError::UnsupportedCipherSuite {
2093 cipher_suite_id, ..
2094 } => {
2095 assert_eq!(cipher_suite_id, 0xFFFF);
2096 }
2097 other => panic!("expected UnsupportedCipherSuite, got: {other}"),
2098 }
2099 }
2100
2101 #[test]
2102 fn compatible_mode_skips_unknown_cipher_suite() {
2103 let mut profile = (*CHROME_131).clone();
2104 profile.cipher_suites.push(CipherSuiteId(0xFFFF));
2105
2106 let cfg = profile.to_rustls_config_with_control(TlsControl::compatible());
2107 assert!(cfg.is_ok(), "compatible mode should skip unknown suite");
2108 }
2109
2110 #[test]
2111 fn control_for_builtin_profiles_is_strict() {
2112 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
2113 let control = TlsControl::for_profile(profile);
2114 assert!(
2115 control.strict_cipher_suites,
2116 "builtin profile '{}' should use strict cipher checking",
2117 profile.name
2118 );
2119 }
2120 }
2121
2122 #[test]
2123 fn control_for_custom_profile_is_compatible() {
2124 let profile = TlsProfile {
2125 name: "Custom Backend".to_string(),
2126 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
2127 tls_versions: vec![TlsVersion::Tls13],
2128 extensions: vec![],
2129 supported_groups: vec![SupportedGroup::X25519],
2130 signature_algorithms: vec![],
2131 alpn_protocols: vec![],
2132 };
2133
2134 let control = TlsControl::for_profile(&profile);
2135 assert!(!control.strict_cipher_suites);
2136 assert!(!control.strict_supported_groups);
2137 assert!(control.fallback_to_provider_groups);
2138 }
2139
2140 #[test]
2141 fn strict_all_without_groups_returns_error() {
2142 let profile = TlsProfile {
2143 name: "StrictGroupTest".to_string(),
2144 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
2145 tls_versions: vec![TlsVersion::Tls13],
2146 extensions: vec![],
2147 supported_groups: vec![],
2148 signature_algorithms: vec![],
2149 alpn_protocols: vec![],
2150 };
2151
2152 let err = profile
2153 .to_rustls_config_with_control(TlsControl::strict_all())
2154 .unwrap_err();
2155
2156 match err {
2157 TlsConfigError::NoSupportedGroups(name) => {
2158 assert_eq!(name, "StrictGroupTest");
2159 }
2160 other => panic!("expected NoSupportedGroups, got: {other}"),
2161 }
2162 }
2163
2164 #[test]
2165 fn into_arc_conversion() {
2166 let config = CHROME_131.to_rustls_config().unwrap();
2167 let arc: std::sync::Arc<rustls::ClientConfig> = config.into();
2168 assert!(!arc.alpn_protocols.is_empty());
2170 }
2171 }
2172
2173 #[cfg(feature = "tls-config")]
2176 mod reqwest_tests {
2177 use super::super::*;
2178
2179 #[test]
2180 fn build_profiled_client_no_proxy() {
2181 let client = build_profiled_client(&CHROME_131, None);
2182 assert!(
2183 client.is_ok(),
2184 "should build a client without error: {:?}",
2185 client.err()
2186 );
2187 }
2188
2189 #[test]
2190 fn build_profiled_client_all_profiles() {
2191 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
2192 let result = build_profiled_client(profile, None);
2193 assert!(
2194 result.is_ok(),
2195 "profile '{}' should produce a valid client: {:?}",
2196 profile.name,
2197 result.err()
2198 );
2199 }
2200 }
2201
2202 #[test]
2203 fn build_profiled_client_strict_no_proxy() {
2204 let client = build_profiled_client_strict(&CHROME_131, None);
2205 assert!(
2206 client.is_ok(),
2207 "strict mode should build for built-in profile: {:?}",
2208 client.err()
2209 );
2210 }
2211
2212 #[test]
2213 fn build_profiled_client_preset_all_profiles() {
2214 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
2215 let result = build_profiled_client_preset(profile, None);
2216 assert!(
2217 result.is_ok(),
2218 "preset builder should work for profile '{}': {:?}",
2219 profile.name,
2220 result.err()
2221 );
2222 }
2223 }
2224
2225 #[test]
2226 fn build_profiled_client_with_control_rejects_unknown_cipher_suite() {
2227 let mut profile = (*CHROME_131).clone();
2228 profile.cipher_suites.push(CipherSuiteId(0xFFFF));
2229
2230 let client = build_profiled_client_with_control(&profile, None, TlsControl::strict());
2231
2232 assert!(
2233 client.is_err(),
2234 "strict mode should reject unsupported cipher suite"
2235 );
2236 }
2237
2238 #[test]
2239 fn default_user_agent_matches_browser() {
2240 assert!(default_user_agent(&CHROME_131).contains("Chrome/131"));
2241 assert!(default_user_agent(&FIREFOX_133).contains("Firefox/133"));
2242 assert!(default_user_agent(&SAFARI_18).contains("Safari/605"));
2243 assert!(default_user_agent(&EDGE_131).contains("Edg/131"));
2244 }
2245
2246 #[test]
2247 fn profile_for_device_mapping() {
2248 use crate::fingerprint::DeviceProfile;
2249
2250 assert_eq!(
2251 profile_for_device(&DeviceProfile::DesktopWindows).name,
2252 "Chrome 131"
2253 );
2254 assert_eq!(
2255 profile_for_device(&DeviceProfile::DesktopMac).name,
2256 "Safari 18"
2257 );
2258 assert_eq!(
2259 profile_for_device(&DeviceProfile::DesktopLinux).name,
2260 "Firefox 133"
2261 );
2262 assert_eq!(
2263 profile_for_device(&DeviceProfile::MobileAndroid).name,
2264 "Chrome 131"
2265 );
2266 assert_eq!(
2267 profile_for_device(&DeviceProfile::MobileIOS).name,
2268 "Safari 18"
2269 );
2270 }
2271
2272 #[test]
2273 fn browser_headers_chrome_has_sec_ch_ua() {
2274 let headers = browser_headers(&CHROME_131);
2275 assert!(
2276 headers.contains_key("sec-ch-ua"),
2277 "Chrome profile should have sec-ch-ua"
2278 );
2279 assert!(
2280 headers.contains_key("sec-fetch-dest"),
2281 "Chrome profile should have sec-fetch-dest"
2282 );
2283 let accept = headers.get("accept").unwrap().to_str().unwrap();
2284 assert!(
2285 accept.contains("image/avif"),
2286 "Chrome accept should include avif"
2287 );
2288 }
2289
2290 #[test]
2291 fn browser_headers_firefox_no_sec_ch_ua() {
2292 let headers = browser_headers(&FIREFOX_133);
2293 assert!(
2294 !headers.contains_key("sec-ch-ua"),
2295 "Firefox profile should not have sec-ch-ua"
2296 );
2297 let accept = headers.get("accept").unwrap().to_str().unwrap();
2298 assert!(
2299 accept.contains("text/html"),
2300 "Firefox accept should include text/html"
2301 );
2302 }
2303
2304 #[test]
2305 fn browser_headers_all_profiles_have_accept() {
2306 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
2307 let headers = browser_headers(profile);
2308 assert!(
2309 headers.contains_key("accept"),
2310 "profile '{}' must have accept header",
2311 profile.name
2312 );
2313 assert!(
2314 headers.contains_key("accept-encoding"),
2315 "profile '{}' must have accept-encoding",
2316 profile.name
2317 );
2318 assert!(
2319 headers.contains_key("accept-language"),
2320 "profile '{}' must have accept-language",
2321 profile.name
2322 );
2323 }
2324 }
2325
2326 #[test]
2327 fn browser_headers_edge_uses_edge_brand() {
2328 let headers = browser_headers(&EDGE_131);
2329 let sec_ch_ua = headers.get("sec-ch-ua").unwrap().to_str().unwrap();
2330 assert!(
2331 sec_ch_ua.contains("Microsoft Edge"),
2332 "Edge sec-ch-ua should identify Edge: {sec_ch_ua}"
2333 );
2334 }
2335 }
2336}