Skip to main content

stygian_browser/transport_realism/
mod.rs

1//! Transport-layer realism expansion (HTTP/2 + HTTP/3 surfaces).
2//!
3//! This module sits on top of the existing
4//! [`tls_validation`][crate::tls_validation] layer (T46) and adds the
5//! HTTP/2 behaviour checks anti-bot vendors observe in the wild:
6//!
7//! - HTTP/2 SETTINGS frame fingerprint (initial values Chrome, Firefox,
8//!   Safari send on every connection).
9//! - HTTP/2 header order (the order browsers send `:method`,
10//!   `:authority`, `:scheme`, `:path`, then standard headers is part of
11//!   the Akamai / `DataDome` fingerprint).
12//! - HTTP/2 pseudo-header order (`:method` before `:authority` before
13//!   `:scheme` before `:path` is the Chrome 136 ordering).
14//! - HTTP/3 perk fingerprint (already produced by the existing
15//!   `tls_validation` module — this module consumes it).
16//!
17//! All checks share the same scoring interface:
18//! [`TransportCompatibility::score`] produces a normalised
19//! compatibility score in `[0.0, 1.0]`, with confidence/coverage
20//! markers that tell the caller how much of the surface was actually
21//! observable. When HTTP/2 observations are unavailable (e.g. the
22//! caller is running a non-HTTP/2 path or a live capture failed),
23//! the score collapses to a deterministic neutral value and the
24//! coverage marker reflects that no HTTP/2 observations were
25//! supplied — see
26//! [`DEFAULT_COVERAGE_WHEN_HTTP2_UNAVAILABLE`] and
27//! [`DEFAULT_CONFIDENCE_WHEN_HTTP2_UNAVAILABLE`].
28//!
29//! ## Feature flag
30//!
31//! This module is **default-on** and is always compiled as part of
32//! the `stygian-browser` crate. The runner surface
33//! ([`AcquisitionRequest::transport_realism`][crate::acquisition::AcquisitionRequest::transport_realism])
34//! is additive — callers that do not pass a context see the same
35//! runner behaviour they saw before this task landed.
36//!
37//! ## Integration with the `AcquisitionRunner`
38//!
39//! [`TransportProfile`] is a typed config field that callers can attach
40//! to an [`AcquisitionRequest`][crate::acquisition::AcquisitionRequest].
41//! When present, the runner records the resulting
42//! [`TransportCompatibility`] on the
43//! [`AcquisitionResult::transport_realism`][crate::acquisition::AcquisitionResult::transport_realism]
44//! field as a [`TransportRealismReport`] so downstream policy mapping
45//! (T83 / T85 / T89 / T93) can consume it as a strategy hint.
46//!
47//! ## Default behaviour
48//!
49//! - `TransportProfile::default()` enables every observable check and
50//!   marks HTTP/2 as required.
51//! - When no HTTP/2 observations are supplied, the score is `0.0` and
52//!   coverage is `0.0` (clearly marked as such). Callers that always
53//!   have HTTP/2 captures should override `require_http2_observations`
54//!   to detect partial instrumentation and short-circuit the run.
55//!
56//! # Example
57//!
58//! ```
59//! use stygian_browser::tls_validation::CHROME_136_HTTP2_SETTINGS;
60//! use stygian_browser::transport_realism::{
61//!     score, TransportObservation, TransportProfile, HEADER_ORDER_CHROME_136,
62//!     PSEUDO_HEADER_ORDER_CHROME_136,
63//! };
64//!
65//! // Live capture that exactly matches Chrome 136 → score 1.0
66//! let obs = TransportObservation::from_settings(CHROME_136_HTTP2_SETTINGS)
67//!     .with_header_order(HEADER_ORDER_CHROME_136.iter().copied())
68//!     .with_pseudo_header_order(PSEUDO_HEADER_ORDER_CHROME_136.iter().copied());
69//! let report = score(&TransportProfile::default(), &obs);
70//! assert!(report.compatibility.score > 0.95);
71//! assert!(report.compatibility.is_high_confidence());
72//! ```
73
74mod observations;
75mod profile;
76mod report;
77mod scoring;
78
79pub use observations::{
80    HEADER_ORDER_CHROME_136, HEADER_ORDER_FIREFOX_130, HeaderOrderMatch, Http2SettingsObservation,
81    PSEUDO_HEADER_ORDER_CHROME_136, TransportObservation, compare_header_order,
82    compare_pseudo_header_order,
83};
84pub use profile::{Http2Expectations, TransportProfile};
85pub use report::{TransportCompatibility, TransportRealismReport};
86pub use scoring::{HTTP2_CHECK_KIND_COUNT, Http2CheckKind, Http2CheckResult, score};
87
88use std::fmt;
89
90/// Default confidence score returned when no HTTP/2 observations are
91/// available.
92///
93/// The score is `0.0` because no comparison could be made — the
94/// caller has no signal that the observed transport is realistic for
95/// the target profile.
96pub const DEFAULT_CONFIDENCE_WHEN_HTTP2_UNAVAILABLE: f64 = 0.0;
97
98/// Default coverage score returned when no HTTP/2 observations are
99/// available.
100///
101/// Coverage is `0.0` because none of the HTTP/2 checks could be
102/// executed. Callers can compare against this constant to detect
103/// "missing instrumentation" rather than "low but real coverage".
104pub const DEFAULT_COVERAGE_WHEN_HTTP2_UNAVAILABLE: f64 = 0.0;
105
106/// Default compatibility score returned when no HTTP/2 observations
107/// are available.
108///
109/// The score is `0.0` because no observation could be compared — the
110/// runner treats the surface as "no signal" rather than "matches".
111pub const DEFAULT_SCORE_WHEN_HTTP2_UNAVAILABLE: f64 = 0.0;
112
113/// Errors produced by transport-realism helpers.
114#[derive(Debug)]
115pub enum TransportRealismError {
116    /// A supplied header order or pseudo-header order is empty.
117    EmptyHeaderOrder,
118    /// A header order observation contained a duplicate header name.
119    DuplicateHeader,
120}
121
122impl fmt::Display for TransportRealismError {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::EmptyHeaderOrder => f.write_str("transport realism: empty header order"),
126            Self::DuplicateHeader => f.write_str("transport realism: duplicate header in order"),
127        }
128    }
129}
130
131impl std::error::Error for TransportRealismError {}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::tls_validation::CHROME_136_HTTP2_SETTINGS;
137
138    #[test]
139    fn module_exports_are_reachable_from_crate_root() {
140        // The crate-root re-exports below would fail to compile if the
141        // module structure diverged from the public API contract.
142        let _profile = TransportProfile::default();
143        let obs = TransportObservation::from_settings(CHROME_136_HTTP2_SETTINGS);
144        let _ = score(&TransportProfile::default(), &obs);
145    }
146
147    #[test]
148    fn default_constants_are_stable() {
149        assert!(DEFAULT_CONFIDENCE_WHEN_HTTP2_UNAVAILABLE.abs() < 1e-9);
150        assert!(DEFAULT_COVERAGE_WHEN_HTTP2_UNAVAILABLE.abs() < 1e-9);
151        assert!(DEFAULT_SCORE_WHEN_HTTP2_UNAVAILABLE.abs() < 1e-9);
152    }
153}