hpx_emulation/fingerprint/
diff.rs1use super::{BrowserFingerprint, CipherSuite, Curve, SignatureAlgorithm};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum FingerprintDiff {
11 NameChanged {
13 old: &'static str,
14 new: &'static str,
15 },
16 VersionChanged {
18 old: &'static str,
19 new: &'static str,
20 },
21 CurvesChanged { old: Vec<Curve>, new: Vec<Curve> },
23 CipherSuitesChanged {
25 old: Vec<CipherSuite>,
26 new: Vec<CipherSuite>,
27 },
28 SignatureAlgorithmsChanged {
30 old: Vec<SignatureAlgorithm>,
31 new: Vec<SignatureAlgorithm>,
32 },
33 PermuteExtensionsChanged { old: bool, new: bool },
35 EchModeChanged {
37 old: super::EchMode,
38 new: super::EchMode,
39 },
40 PreSharedKeyChanged { old: bool, new: bool },
42 AlpsNewCodepointChanged { old: bool, new: bool },
44 H2InitialWindowSizeChanged { old: u32, new: u32 },
46 H2MaxConcurrentStreamsChanged { old: Option<u32>, new: Option<u32> },
48 H2EnablePushChanged {
50 old: Option<bool>,
51 new: Option<bool>,
52 },
53 HeadersChanged,
55}
56
57impl std::fmt::Display for FingerprintDiff {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 FingerprintDiff::NameChanged { old, new } => {
61 write!(f, "Name: {old} -> {new}")
62 }
63 FingerprintDiff::VersionChanged { old, new } => {
64 write!(f, "Version: {old} -> {new}")
65 }
66 FingerprintDiff::CurvesChanged { old, new } => {
67 write!(
68 f,
69 "Curves: {} -> {}",
70 format_curves(old),
71 format_curves(new)
72 )
73 }
74 FingerprintDiff::CipherSuitesChanged { old, new } => {
75 write!(
76 f,
77 "Cipher suites: {} suites -> {} suites",
78 old.len(),
79 new.len()
80 )
81 }
82 FingerprintDiff::SignatureAlgorithmsChanged { old, new } => {
83 write!(f, "Signature algorithms: {} -> {}", old.len(), new.len())
84 }
85 FingerprintDiff::PermuteExtensionsChanged { old, new } => {
86 write!(f, "Permute extensions: {old} -> {new}")
87 }
88 FingerprintDiff::EchModeChanged { old: _, new: _ } => {
89 write!(f, "ECH mode changed")
90 }
91 FingerprintDiff::PreSharedKeyChanged { old, new } => {
92 write!(f, "PSK: {old} -> {new}")
93 }
94 FingerprintDiff::AlpsNewCodepointChanged { old, new } => {
95 write!(f, "ALPS new codepoint: {old} -> {new}")
96 }
97 FingerprintDiff::H2InitialWindowSizeChanged { old, new } => {
98 write!(f, "H2 initial window size: {old} -> {new}")
99 }
100 FingerprintDiff::H2MaxConcurrentStreamsChanged { old, new } => {
101 write!(f, "H2 max concurrent streams: {old:?} -> {new:?}")
102 }
103 FingerprintDiff::H2EnablePushChanged { old, new } => {
104 write!(f, "H2 enable push: {old:?} -> {new:?}")
105 }
106 FingerprintDiff::HeadersChanged => {
107 write!(f, "HTTP headers differ")
108 }
109 }
110 }
111}
112
113fn format_curves(curves: &[Curve]) -> String {
114 curves
115 .iter()
116 .map(|c| c.openssl_name())
117 .collect::<Vec<_>>()
118 .join(":")
119}
120
121pub fn diff_fingerprints(
125 old: &BrowserFingerprint,
126 new: &BrowserFingerprint,
127) -> Vec<FingerprintDiff> {
128 let mut diffs = Vec::new();
129
130 if old.name != new.name {
131 diffs.push(FingerprintDiff::NameChanged {
132 old: old.name,
133 new: new.name,
134 });
135 }
136
137 if old.version != new.version {
138 diffs.push(FingerprintDiff::VersionChanged {
139 old: old.version,
140 new: new.version,
141 });
142 }
143
144 if old.tls.curves != new.tls.curves {
146 diffs.push(FingerprintDiff::CurvesChanged {
147 old: old.tls.curves.clone(),
148 new: new.tls.curves.clone(),
149 });
150 }
151
152 if old.tls.cipher_suites != new.tls.cipher_suites {
153 diffs.push(FingerprintDiff::CipherSuitesChanged {
154 old: old.tls.cipher_suites.clone(),
155 new: new.tls.cipher_suites.clone(),
156 });
157 }
158
159 if old.tls.signature_algorithms != new.tls.signature_algorithms {
160 diffs.push(FingerprintDiff::SignatureAlgorithmsChanged {
161 old: old.tls.signature_algorithms.clone(),
162 new: new.tls.signature_algorithms.clone(),
163 });
164 }
165
166 if old.tls.permute_extensions != new.tls.permute_extensions {
167 diffs.push(FingerprintDiff::PermuteExtensionsChanged {
168 old: old.tls.permute_extensions,
169 new: new.tls.permute_extensions,
170 });
171 }
172
173 if old.tls.ech_mode != new.tls.ech_mode {
174 diffs.push(FingerprintDiff::EchModeChanged {
175 old: old.tls.ech_mode,
176 new: new.tls.ech_mode,
177 });
178 }
179
180 if old.tls.pre_shared_key != new.tls.pre_shared_key {
181 diffs.push(FingerprintDiff::PreSharedKeyChanged {
182 old: old.tls.pre_shared_key,
183 new: new.tls.pre_shared_key,
184 });
185 }
186
187 if old.tls.alps_use_new_codepoint != new.tls.alps_use_new_codepoint {
188 diffs.push(FingerprintDiff::AlpsNewCodepointChanged {
189 old: old.tls.alps_use_new_codepoint,
190 new: new.tls.alps_use_new_codepoint,
191 });
192 }
193
194 if old.http2.initial_window_size != new.http2.initial_window_size {
196 diffs.push(FingerprintDiff::H2InitialWindowSizeChanged {
197 old: old.http2.initial_window_size,
198 new: new.http2.initial_window_size,
199 });
200 }
201
202 if old.http2.max_concurrent_streams != new.http2.max_concurrent_streams {
203 diffs.push(FingerprintDiff::H2MaxConcurrentStreamsChanged {
204 old: old.http2.max_concurrent_streams,
205 new: new.http2.max_concurrent_streams,
206 });
207 }
208
209 if old.http2.enable_push != new.http2.enable_push {
210 diffs.push(FingerprintDiff::H2EnablePushChanged {
211 old: old.http2.enable_push,
212 new: new.http2.enable_push,
213 });
214 }
215
216 if old.headers != new.headers {
218 diffs.push(FingerprintDiff::HeadersChanged);
219 }
220
221 diffs
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::fingerprint::{EchMode, Http2Fingerprint, TlsFingerprint};
228
229 fn make_test_fp(version: &'static str) -> BrowserFingerprint {
230 BrowserFingerprint::new(
231 "chrome",
232 version,
233 TlsFingerprint::default(),
234 Http2Fingerprint::default(),
235 vec![("user-agent", "test")],
236 )
237 }
238
239 #[test]
240 fn test_identical_fingerprints() {
241 let a = make_test_fp("133");
242 let b = make_test_fp("133");
243 assert!(diff_fingerprints(&a, &b).is_empty());
244 }
245
246 #[test]
247 fn test_version_diff() {
248 let a = make_test_fp("133");
249 let b = make_test_fp("134");
250 let diffs = diff_fingerprints(&a, &b);
251 assert_eq!(diffs.len(), 1);
252 assert!(matches!(
253 diffs[0],
254 FingerprintDiff::VersionChanged {
255 old: "133",
256 new: "134"
257 }
258 ));
259 }
260
261 #[test]
262 fn test_curves_diff() {
263 let a = make_test_fp("133");
264 let mut b = make_test_fp("134");
265 b.tls.curves = vec![Curve::X25519MLKEM768, Curve::X25519];
266 let diffs = diff_fingerprints(&a, &b);
267 assert!(
268 diffs
269 .iter()
270 .any(|d| matches!(d, FingerprintDiff::CurvesChanged { .. }))
271 );
272 }
273
274 #[test]
275 fn test_ech_diff() {
276 let a = make_test_fp("133");
277 let mut b = make_test_fp("133");
278 b.tls.ech_mode = EchMode::Grease;
279 let diffs = diff_fingerprints(&a, &b);
280 assert!(
281 diffs
282 .iter()
283 .any(|d| matches!(d, FingerprintDiff::EchModeChanged { .. }))
284 );
285 }
286}