Skip to main content

hpx_emulation/fingerprint/
diff.rs

1//! Fingerprint diff utility.
2//!
3//! Compare two `BrowserFingerprint` instances and report differences.
4//! Useful for understanding what changed between browser versions.
5
6use super::{BrowserFingerprint, CipherSuite, Curve, SignatureAlgorithm};
7
8/// Represents a single difference between two fingerprints.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum FingerprintDiff {
11    /// Browser name differs.
12    NameChanged {
13        old: &'static str,
14        new: &'static str,
15    },
16    /// Browser version differs.
17    VersionChanged {
18        old: &'static str,
19        new: &'static str,
20    },
21    /// TLS curves list differs.
22    CurvesChanged { old: Vec<Curve>, new: Vec<Curve> },
23    /// TLS cipher suites differ.
24    CipherSuitesChanged {
25        old: Vec<CipherSuite>,
26        new: Vec<CipherSuite>,
27    },
28    /// TLS signature algorithms differ.
29    SignatureAlgorithmsChanged {
30        old: Vec<SignatureAlgorithm>,
31        new: Vec<SignatureAlgorithm>,
32    },
33    /// Extension permutation setting changed.
34    PermuteExtensionsChanged { old: bool, new: bool },
35    /// ECH mode changed.
36    EchModeChanged {
37        old: super::EchMode,
38        new: super::EchMode,
39    },
40    /// PSK setting changed.
41    PreSharedKeyChanged { old: bool, new: bool },
42    /// ALPS new codepoint setting changed.
43    AlpsNewCodepointChanged { old: bool, new: bool },
44    /// HTTP/2 initial window size changed.
45    H2InitialWindowSizeChanged { old: u32, new: u32 },
46    /// HTTP/2 max concurrent streams changed.
47    H2MaxConcurrentStreamsChanged { old: Option<u32>, new: Option<u32> },
48    /// HTTP/2 enable_push changed.
49    H2EnablePushChanged {
50        old: Option<bool>,
51        new: Option<bool>,
52    },
53    /// HTTP headers differ.
54    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
121/// Compares two `BrowserFingerprint` instances and returns a list of differences.
122///
123/// Returns an empty vector if the fingerprints are identical.
124pub 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    // TLS diffs
145    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    // HTTP/2 diffs
195    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    // Headers diff
217    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}