millipede_fingerprint/
header_generator.rs1use std::{
2 collections::hash_map::DefaultHasher,
3 hash::{Hash, Hasher},
4 sync::OnceLock,
5};
6
7const HEADER_PROFILES_JSON: &str = include_str!("../data/header_profiles.json");
8
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub struct HeaderProfile {
12 pub user_agent: String,
14 pub headers: Vec<(String, String)>,
16}
17
18fn bundled_profiles() -> &'static [HeaderProfile] {
19 static PROFILES: OnceLock<Vec<HeaderProfile>> = OnceLock::new();
20 PROFILES
21 .get_or_init(|| {
22 serde_json::from_str(HEADER_PROFILES_JSON)
23 .expect("bundled header_profiles.json must parse")
24 })
25 .as_slice()
26}
27
28#[derive(Debug, Clone)]
30pub struct HeaderGenerator {
31 profiles: Vec<HeaderProfile>,
32}
33
34impl HeaderGenerator {
35 pub fn new() -> Self {
37 Self {
38 profiles: bundled_profiles().to_vec(),
39 }
40 }
41
42 pub fn from_profiles(profiles: Vec<HeaderProfile>) -> Self {
48 assert!(!profiles.is_empty(), "header profiles must not be empty");
49 Self { profiles }
50 }
51
52 pub fn generate(&self, seed: &str) -> HeaderProfile {
54 let mut hasher = DefaultHasher::new();
55 seed.as_bytes().hash(&mut hasher);
56 let index = hasher.finish() as usize % self.profiles.len();
57 self.profiles[index].clone()
58 }
59}
60
61impl Default for HeaderGenerator {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use std::collections::HashSet;
70
71 use super::HeaderGenerator;
72
73 #[test]
74 fn same_seed_is_deterministic_across_fresh_instances() {
75 assert_eq!(
76 HeaderGenerator::new().generate("session-A"),
77 HeaderGenerator::new().generate("session-A")
78 );
79 }
80
81 #[test]
82 fn same_seed_is_byte_identical_when_serialized() {
83 let first = serde_json::to_vec(&HeaderGenerator::new().generate("session-A")).unwrap();
84 let second = serde_json::to_vec(&HeaderGenerator::new().generate("session-A")).unwrap();
85
86 assert_eq!(first, second);
87 }
88
89 #[test]
90 fn different_seeds_can_select_different_profiles() {
91 let generator = HeaderGenerator::new();
92 let profiles = (0..20)
93 .map(|index| generator.generate(&format!("session-{index}")))
94 .collect::<Vec<_>>();
95
96 assert!(profiles.iter().any(|profile| profile != &profiles[0]));
97 }
98
99 #[test]
100 fn bundled_dataset_has_multiple_distinct_profiles() {
101 let generator = HeaderGenerator::new();
102 let user_agents = (0..200)
103 .map(|index| generator.generate(&format!("session-{index}")).user_agent)
104 .collect::<HashSet<_>>();
105
106 assert!(user_agents.len() >= 6);
107 }
108
109 #[test]
110 #[should_panic(expected = "header profiles must not be empty")]
111 fn from_profiles_rejects_empty() {
112 HeaderGenerator::from_profiles(vec![]);
113 }
114
115 #[test]
116 fn generated_profile_is_well_formed() {
117 let profile = HeaderGenerator::new().generate("well-formed");
118
119 assert!(!profile.user_agent.is_empty());
120 assert!(!profile.headers.is_empty());
121 assert!(
122 profile
123 .headers
124 .iter()
125 .all(|(name, value)| !name.is_empty() && !value.is_empty())
126 );
127 }
128}