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}