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, thiserror::Error)]
1177 #[non_exhaustive]
1178 pub enum TlsConfigError {
1179 #[error("no supported cipher suites in profile '{0}'")]
1182 NoCipherSuites(String),
1183
1184 #[error("rustls configuration: {0}")]
1186 Rustls(#[from] rustls::Error),
1187 }
1188
1189 #[derive(Debug, Clone)]
1195 pub struct TlsClientConfig(Arc<rustls::ClientConfig>);
1196
1197 impl TlsClientConfig {
1198 pub fn inner(&self) -> &rustls::ClientConfig {
1200 &self.0
1201 }
1202
1203 pub fn into_inner(self) -> Arc<rustls::ClientConfig> {
1205 self.0
1206 }
1207 }
1208
1209 impl From<TlsClientConfig> for Arc<rustls::ClientConfig> {
1210 fn from(cfg: TlsClientConfig) -> Self {
1211 cfg.0
1212 }
1213 }
1214
1215 impl TlsProfile {
1216 pub fn to_rustls_config(&self) -> Result<TlsClientConfig, TlsConfigError> {
1245 let default = rustls::crypto::aws_lc_rs::default_provider();
1246
1247 let suite_map: std::collections::HashMap<u16, rustls::SupportedCipherSuite> = default
1249 .cipher_suites
1250 .iter()
1251 .map(|cs| (u16::from(cs.suite()), *cs))
1252 .collect();
1253
1254 let ordered_suites: Vec<rustls::SupportedCipherSuite> = self
1255 .cipher_suites
1256 .iter()
1257 .filter_map(|id| {
1258 suite_map.get(&id.0).copied().or_else(|| {
1259 tracing::warn!(
1260 cipher_suite_id = id.0,
1261 profile = %self.name,
1262 "cipher suite not supported by rustls aws-lc-rs backend, skipping"
1263 );
1264 None
1265 })
1266 })
1267 .collect();
1268
1269 if ordered_suites.is_empty() {
1270 return Err(TlsConfigError::NoCipherSuites(self.name.clone()));
1271 }
1272
1273 let group_map: std::collections::HashMap<
1275 u16,
1276 &'static dyn rustls::crypto::SupportedKxGroup,
1277 > = default
1278 .kx_groups
1279 .iter()
1280 .map(|g| (u16::from(g.name()), *g))
1281 .collect();
1282
1283 let ordered_groups: Vec<&'static dyn rustls::crypto::SupportedKxGroup> = self
1284 .supported_groups
1285 .iter()
1286 .filter_map(|sg| {
1287 group_map.get(&sg.iana_value()).copied().or_else(|| {
1288 tracing::warn!(
1289 group_id = sg.iana_value(),
1290 profile = %self.name,
1291 "key-exchange group not supported by rustls, skipping"
1292 );
1293 None
1294 })
1295 })
1296 .collect();
1297
1298 let kx_groups = if ordered_groups.is_empty() {
1300 default.kx_groups.clone()
1301 } else {
1302 ordered_groups
1303 };
1304
1305 let provider = rustls::crypto::CryptoProvider {
1307 cipher_suites: ordered_suites,
1308 kx_groups,
1309 ..default
1310 };
1311
1312 let versions: Vec<&'static rustls::SupportedProtocolVersion> = self
1314 .tls_versions
1315 .iter()
1316 .map(|v| match v {
1317 TlsVersion::Tls12 => &rustls::version::TLS12,
1318 TlsVersion::Tls13 => &rustls::version::TLS13,
1319 })
1320 .collect();
1321
1322 let mut root_store = rustls::RootCertStore::empty();
1324 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1325
1326 let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(provider))
1328 .with_protocol_versions(&versions)?
1329 .with_root_certificates(root_store)
1330 .with_no_client_auth();
1331
1332 config.alpn_protocols = self
1334 .alpn_protocols
1335 .iter()
1336 .map(|p| p.as_str().as_bytes().to_vec())
1337 .collect();
1338
1339 Ok(TlsClientConfig(Arc::new(config)))
1340 }
1341 }
1342}
1343
1344#[cfg(feature = "tls-config")]
1345pub use rustls_config::{TlsClientConfig, TlsConfigError};
1346
1347#[cfg(feature = "tls-config")]
1354mod reqwest_client {
1355 #[allow(clippy::wildcard_imports)]
1356 use super::*;
1357 use std::sync::Arc;
1358
1359 #[derive(Debug, thiserror::Error)]
1361 #[non_exhaustive]
1362 pub enum TlsClientError {
1363 #[error(transparent)]
1365 TlsConfig(#[from] super::rustls_config::TlsConfigError),
1366
1367 #[error("reqwest client: {0}")]
1369 Reqwest(#[from] reqwest::Error),
1370 }
1371
1372 pub fn default_user_agent(profile: &TlsProfile) -> &'static str {
1388 let name = profile.name.to_ascii_lowercase();
1389 if name.contains("firefox") {
1390 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0"
1391 } else if name.contains("safari") && !name.contains("chrome") {
1392 "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"
1393 } else if name.contains("edge") {
1394 "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"
1395 } else {
1396 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
1398 }
1399 }
1400
1401 pub fn profile_for_device(device: &crate::fingerprint::DeviceProfile) -> &'static TlsProfile {
1412 use crate::fingerprint::DeviceProfile;
1413 match device {
1414 DeviceProfile::DesktopWindows | DeviceProfile::MobileAndroid => &CHROME_131,
1415 DeviceProfile::DesktopMac | DeviceProfile::MobileIOS => &SAFARI_18,
1416 DeviceProfile::DesktopLinux => &FIREFOX_133,
1417 }
1418 }
1419
1420 pub fn build_profiled_client(
1444 profile: &TlsProfile,
1445 proxy_url: Option<&str>,
1446 ) -> Result<reqwest::Client, TlsClientError> {
1447 let tls_config = profile.to_rustls_config()?;
1448
1449 let rustls_cfg =
1451 Arc::try_unwrap(tls_config.into_inner()).unwrap_or_else(|arc| (*arc).clone());
1452
1453 let mut builder = reqwest::Client::builder()
1454 .use_preconfigured_tls(rustls_cfg)
1455 .user_agent(default_user_agent(profile))
1456 .cookie_store(true)
1457 .gzip(true)
1458 .brotli(true);
1459
1460 if let Some(url) = proxy_url {
1461 builder = builder.proxy(reqwest::Proxy::all(url)?);
1462 }
1463
1464 Ok(builder.build()?)
1465 }
1466}
1467
1468#[cfg(feature = "tls-config")]
1469pub use reqwest_client::{
1470 TlsClientError, build_profiled_client, default_user_agent, profile_for_device,
1471};
1472
1473#[cfg(test)]
1476#[allow(clippy::panic, clippy::unwrap_used)]
1477mod tests {
1478 use super::*;
1479
1480 #[test]
1481 fn md5_known_vectors() {
1482 assert_eq!(md5_hex(b""), "d41d8cd98f00b204e9800998ecf8427e");
1484 assert_eq!(md5_hex(b"a"), "0cc175b9c0f1b6a831c399e269772661");
1485 assert_eq!(md5_hex(b"abc"), "900150983cd24fb0d6963f7d28e17f72");
1486 assert_eq!(
1487 md5_hex(b"message digest"),
1488 "f96b697d7cb7938d525a2f31aaf161d0"
1489 );
1490 }
1491
1492 #[test]
1493 fn chrome_131_ja3_structure() {
1494 let ja3 = CHROME_131.ja3();
1495 assert!(
1499 ja3.raw.starts_with("772,"),
1500 "JA3 raw should start with '772,' but was: {}",
1501 ja3.raw
1502 );
1503 assert_eq!(ja3.raw.matches(',').count(), 4);
1505 assert_eq!(ja3.hash.len(), 32);
1507 assert!(ja3.hash.chars().all(|c| c.is_ascii_hexdigit()));
1508 }
1509
1510 #[test]
1511 fn firefox_133_ja3_differs_from_chrome() {
1512 let chrome_ja3 = CHROME_131.ja3();
1513 let firefox_ja3 = FIREFOX_133.ja3();
1514 assert_ne!(chrome_ja3.hash, firefox_ja3.hash);
1515 assert_ne!(chrome_ja3.raw, firefox_ja3.raw);
1516 }
1517
1518 #[test]
1519 fn safari_18_ja3_is_valid() {
1520 let ja3 = SAFARI_18.ja3();
1521 assert!(ja3.raw.starts_with("772,"));
1522 assert_eq!(ja3.hash.len(), 32);
1523 }
1524
1525 #[test]
1526 fn edge_131_ja3_differs_from_chrome() {
1527 let chrome_ja3 = CHROME_131.ja3();
1529 let edge_ja3 = EDGE_131.ja3();
1530 assert_ne!(chrome_ja3.hash, edge_ja3.hash);
1531 }
1532
1533 #[test]
1534 fn chrome_131_ja4_format() {
1535 let ja4 = CHROME_131.ja4();
1536 assert!(
1538 ja4.fingerprint.starts_with("t13d"),
1539 "JA4 should start with 't13d' but was: {}",
1540 ja4.fingerprint
1541 );
1542 assert_eq!(
1544 ja4.fingerprint.matches('_').count(),
1545 3,
1546 "JA4 should have three underscores: {}",
1547 ja4.fingerprint
1548 );
1549 }
1550
1551 #[test]
1552 fn ja4_firefox_differs_from_chrome() {
1553 let chrome_ja4 = CHROME_131.ja4();
1554 let firefox_ja4 = FIREFOX_133.ja4();
1555 assert_ne!(chrome_ja4.fingerprint, firefox_ja4.fingerprint);
1556 }
1557
1558 #[test]
1559 fn random_weighted_distribution() {
1560 let mut chrome_count = 0u32;
1561 let mut firefox_count = 0u32;
1562 let mut edge_count = 0u32;
1563 let mut safari_count = 0u32;
1564
1565 let total = 10_000u32;
1566 for i in 0..total {
1567 let profile = TlsProfile::random_weighted(u64::from(i));
1568 match profile.name.as_str() {
1569 "Chrome 131" => chrome_count += 1,
1570 "Firefox 133" => firefox_count += 1,
1571 "Edge 131" => edge_count += 1,
1572 "Safari 18" => safari_count += 1,
1573 other => unreachable!("unexpected profile: {other}"),
1574 }
1575 }
1576
1577 assert!(
1579 chrome_count > total * 40 / 100,
1580 "Chrome share too low: {chrome_count}/{total}"
1581 );
1582 assert!(
1584 firefox_count > total * 5 / 100,
1585 "Firefox share too low: {firefox_count}/{total}"
1586 );
1587 assert!(
1589 edge_count > total * 5 / 100,
1590 "Edge share too low: {edge_count}/{total}"
1591 );
1592 assert!(
1594 safari_count > total * 3 / 100,
1595 "Safari share too low: {safari_count}/{total}"
1596 );
1597 }
1598
1599 #[test]
1600 fn serde_roundtrip() {
1601 let profile: &TlsProfile = &CHROME_131;
1602 let json = serde_json::to_string(profile).unwrap();
1603 let deserialized: TlsProfile = serde_json::from_str(&json).unwrap();
1604 assert_eq!(profile, &deserialized);
1605 }
1606
1607 #[test]
1608 fn ja3hash_display() {
1609 let ja3 = CHROME_131.ja3();
1610 assert_eq!(format!("{ja3}"), ja3.hash);
1611 }
1612
1613 #[test]
1614 fn ja4_display() {
1615 let ja4 = CHROME_131.ja4();
1616 assert_eq!(format!("{ja4}"), ja4.fingerprint);
1617 }
1618
1619 #[test]
1620 fn cipher_suite_display() {
1621 let cs = CipherSuiteId::TLS_AES_128_GCM_SHA256;
1622 assert_eq!(format!("{cs}"), "4865"); }
1624
1625 #[test]
1626 fn tls_version_display() {
1627 assert_eq!(format!("{}", TlsVersion::Tls13), "772");
1628 }
1629
1630 #[test]
1631 fn alpn_protocol_as_str() {
1632 assert_eq!(AlpnProtocol::H2.as_str(), "h2");
1633 assert_eq!(AlpnProtocol::Http11.as_str(), "http/1.1");
1634 }
1635
1636 #[test]
1637 fn supported_group_values() {
1638 assert_eq!(SupportedGroup::X25519.iana_value(), 0x001d);
1639 assert_eq!(SupportedGroup::SecP256r1.iana_value(), 0x0017);
1640 assert_eq!(SupportedGroup::X25519Kyber768.iana_value(), 0x6399);
1641 }
1642
1643 #[test]
1646 fn chrome_131_tls_args_empty() {
1647 let args = chrome_tls_args(&CHROME_131);
1649 assert!(args.is_empty(), "expected no flags, got: {args:?}");
1650 }
1651
1652 #[test]
1653 fn tls12_only_profile_caps_version() {
1654 let profile = TlsProfile {
1655 name: "TLS12-only".to_string(),
1656 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
1657 tls_versions: vec![TlsVersion::Tls12],
1658 extensions: vec![],
1659 supported_groups: vec![],
1660 signature_algorithms: vec![],
1661 alpn_protocols: vec![],
1662 };
1663 let args = chrome_tls_args(&profile);
1664 assert_eq!(args, vec!["--ssl-version-max=tls1.2"]);
1665 }
1666
1667 #[test]
1668 fn tls13_only_profile_raises_floor() {
1669 let profile = TlsProfile {
1670 name: "TLS13-only".to_string(),
1671 cipher_suites: vec![CipherSuiteId::TLS_AES_128_GCM_SHA256],
1672 tls_versions: vec![TlsVersion::Tls13],
1673 extensions: vec![],
1674 supported_groups: vec![],
1675 signature_algorithms: vec![],
1676 alpn_protocols: vec![],
1677 };
1678 let args = chrome_tls_args(&profile);
1679 assert_eq!(args, vec!["--ssl-version-min=tls1.3"]);
1680 }
1681
1682 #[test]
1683 fn builder_tls_profile_integration() {
1684 let cfg = crate::BrowserConfig::builder()
1685 .tls_profile(&CHROME_131)
1686 .build();
1687 let tls_flags: Vec<_> = cfg
1689 .effective_args()
1690 .into_iter()
1691 .filter(|a| a.starts_with("--ssl-version"))
1692 .collect();
1693 assert!(tls_flags.is_empty(), "unexpected TLS flags: {tls_flags:?}");
1694 }
1695
1696 #[cfg(feature = "tls-config")]
1699 mod rustls_tests {
1700 use super::super::*;
1701
1702 #[test]
1703 fn chrome_131_config_builds_successfully() {
1704 let config = CHROME_131.to_rustls_config().unwrap();
1705 let inner = config.inner();
1707 assert!(
1709 !inner.alpn_protocols.is_empty(),
1710 "ALPN protocols should be set"
1711 );
1712 }
1713
1714 #[test]
1715 #[allow(clippy::indexing_slicing)]
1716 fn alpn_order_matches_profile() {
1717 let config = CHROME_131.to_rustls_config().unwrap();
1718 let alpn = &config.inner().alpn_protocols;
1719 assert_eq!(alpn.len(), 2);
1720 assert_eq!(alpn[0], b"h2");
1721 assert_eq!(alpn[1], b"http/1.1");
1722 }
1723
1724 #[test]
1725 fn all_builtin_profiles_produce_valid_configs() {
1726 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
1727 let result = profile.to_rustls_config();
1728 assert!(
1729 result.is_ok(),
1730 "profile '{}' should produce a valid config: {:?}",
1731 profile.name,
1732 result.err()
1733 );
1734 }
1735 }
1736
1737 #[test]
1738 fn unsupported_only_suites_returns_error() {
1739 let profile = TlsProfile {
1740 name: "Bogus".to_string(),
1741 cipher_suites: vec![CipherSuiteId(0xFFFF)],
1742 tls_versions: vec![TlsVersion::Tls13],
1743 extensions: vec![],
1744 supported_groups: vec![],
1745 signature_algorithms: vec![],
1746 alpn_protocols: vec![],
1747 };
1748 let err = profile.to_rustls_config().unwrap_err();
1749 assert!(
1750 err.to_string().contains("no supported cipher suites"),
1751 "expected NoCipherSuites, got: {err}"
1752 );
1753 }
1754
1755 #[test]
1756 fn into_arc_conversion() {
1757 let config = CHROME_131.to_rustls_config().unwrap();
1758 let arc: std::sync::Arc<rustls::ClientConfig> = config.into();
1759 assert!(!arc.alpn_protocols.is_empty());
1761 }
1762 }
1763
1764 #[cfg(feature = "tls-config")]
1767 mod reqwest_tests {
1768 use super::super::*;
1769
1770 #[test]
1771 fn build_profiled_client_no_proxy() {
1772 let client = build_profiled_client(&CHROME_131, None);
1773 assert!(
1774 client.is_ok(),
1775 "should build a client without error: {:?}",
1776 client.err()
1777 );
1778 }
1779
1780 #[test]
1781 fn build_profiled_client_all_profiles() {
1782 for profile in [&*CHROME_131, &*FIREFOX_133, &*SAFARI_18, &*EDGE_131] {
1783 let result = build_profiled_client(profile, None);
1784 assert!(
1785 result.is_ok(),
1786 "profile '{}' should produce a valid client: {:?}",
1787 profile.name,
1788 result.err()
1789 );
1790 }
1791 }
1792
1793 #[test]
1794 fn default_user_agent_matches_browser() {
1795 assert!(default_user_agent(&CHROME_131).contains("Chrome/131"));
1796 assert!(default_user_agent(&FIREFOX_133).contains("Firefox/133"));
1797 assert!(default_user_agent(&SAFARI_18).contains("Safari/605"));
1798 assert!(default_user_agent(&EDGE_131).contains("Edg/131"));
1799 }
1800
1801 #[test]
1802 fn profile_for_device_mapping() {
1803 use crate::fingerprint::DeviceProfile;
1804
1805 assert_eq!(
1806 profile_for_device(&DeviceProfile::DesktopWindows).name,
1807 "Chrome 131"
1808 );
1809 assert_eq!(
1810 profile_for_device(&DeviceProfile::DesktopMac).name,
1811 "Safari 18"
1812 );
1813 assert_eq!(
1814 profile_for_device(&DeviceProfile::DesktopLinux).name,
1815 "Firefox 133"
1816 );
1817 assert_eq!(
1818 profile_for_device(&DeviceProfile::MobileAndroid).name,
1819 "Chrome 131"
1820 );
1821 assert_eq!(
1822 profile_for_device(&DeviceProfile::MobileIOS).name,
1823 "Safari 18"
1824 );
1825 }
1826 }
1827}