stygian_browser/transport_realism/
profile.rs1use serde::{Deserialize, Serialize};
10
11use crate::tls_validation::CHROME_136_HTTP2_SETTINGS;
12
13use super::observations::{HEADER_ORDER_CHROME_136, PSEUDO_HEADER_ORDER_CHROME_136};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(transparent)]
25pub struct Http2Expectations {
26 bits: u8,
29}
30
31impl Http2Expectations {
32 pub const SETTINGS: u8 = 1 << 0;
34 pub const PSEUDO_HEADER_ORDER: u8 = 1 << 1;
36 pub const HEADER_ORDER: u8 = 1 << 2;
38 pub const ALL: u8 = Self::SETTINGS | Self::PSEUDO_HEADER_ORDER | Self::HEADER_ORDER;
40
41 #[must_use]
43 pub const fn from_bits(bits: u8) -> Self {
44 Self { bits }
45 }
46
47 #[must_use]
49 pub const fn is_empty(self) -> bool {
50 self.bits == 0
51 }
52
53 #[must_use]
55 pub const fn contains(self, flag: u8) -> bool {
56 (self.bits & flag) == flag
57 }
58
59 #[must_use]
61 pub const fn count(self) -> usize {
62 (self.bits & Self::SETTINGS != 0) as usize
63 + (self.bits & Self::PSEUDO_HEADER_ORDER != 0) as usize
64 + (self.bits & Self::HEADER_ORDER != 0) as usize
65 }
66}
67
68impl Default for Http2Expectations {
69 fn default() -> Self {
70 Self { bits: Self::ALL }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct TransportProfile {
98 #[serde(default = "default_profile_name")]
102 pub name: String,
103 #[serde(default = "default_http2_settings")]
105 pub expected_http2_settings: Vec<(u32, u32)>,
106 #[serde(default = "default_pseudo_header_order")]
108 pub expected_pseudo_header_order: Vec<String>,
109 #[serde(default = "default_header_order")]
111 pub expected_header_order: Vec<String>,
112 #[serde(default)]
115 pub expectations: Http2Expectations,
116 #[serde(default)]
120 pub require_http2_observations: bool,
121}
122
123impl TransportProfile {
124 pub const SETTINGS: u8 = Http2Expectations::SETTINGS;
127 pub const PSEUDO_HEADER_ORDER: u8 = Http2Expectations::PSEUDO_HEADER_ORDER;
129 pub const HEADER_ORDER: u8 = Http2Expectations::HEADER_ORDER;
131}
132
133fn default_profile_name() -> String {
134 "chrome-136".to_string()
135}
136
137fn default_http2_settings() -> Vec<(u32, u32)> {
138 CHROME_136_HTTP2_SETTINGS.to_vec()
139}
140
141fn default_pseudo_header_order() -> Vec<String> {
142 PSEUDO_HEADER_ORDER_CHROME_136
143 .iter()
144 .map(|s| (*s).to_string())
145 .collect()
146}
147
148fn default_header_order() -> Vec<String> {
149 HEADER_ORDER_CHROME_136
150 .iter()
151 .map(|s| (*s).to_string())
152 .collect()
153}
154
155impl Default for TransportProfile {
156 fn default() -> Self {
157 Self {
158 name: default_profile_name(),
159 expected_http2_settings: default_http2_settings(),
160 expected_pseudo_header_order: default_pseudo_header_order(),
161 expected_header_order: default_header_order(),
162 expectations: Http2Expectations::default(),
163 require_http2_observations: false,
164 }
165 }
166}
167
168impl TransportProfile {
169 #[must_use]
175 pub fn chrome_136_reference() -> Self {
176 Self::default()
177 }
178
179 #[must_use]
181 pub fn with_name(mut self, name: impl Into<String>) -> Self {
182 self.name = name.into();
183 self
184 }
185
186 #[must_use]
188 pub fn with_http2_settings(mut self, settings: Vec<(u32, u32)>) -> Self {
189 self.expected_http2_settings = settings;
190 self
191 }
192
193 #[must_use]
195 pub fn with_pseudo_header_order(mut self, order: Vec<String>) -> Self {
196 self.expected_pseudo_header_order = order;
197 self
198 }
199
200 #[must_use]
202 pub fn with_header_order(mut self, order: Vec<String>) -> Self {
203 self.expected_header_order = order;
204 self
205 }
206
207 #[must_use]
209 pub const fn with_require_http2_observations(mut self, require: bool) -> Self {
210 self.require_http2_observations = require;
211 self
212 }
213
214 #[must_use]
216 pub const fn with_expectations(mut self, expectations: Http2Expectations) -> Self {
217 self.expectations = expectations;
218 self
219 }
220
221 #[must_use]
223 pub const fn with_expectation_bits(mut self, bits: u8) -> Self {
224 self.expectations = Http2Expectations::from_bits(bits);
225 self
226 }
227
228 #[must_use]
230 pub const fn has_any_http2_expectation(&self) -> bool {
231 !self.expectations.is_empty()
232 }
233
234 #[must_use]
236 pub const fn expected_http2_check_count(&self) -> usize {
237 self.expectations.count()
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::tls_validation::CHROME_136_HTTP2_SETTINGS;
245
246 #[test]
247 fn default_profile_matches_chrome_136() {
248 let profile = TransportProfile::default();
249 assert_eq!(profile.name, "chrome-136");
250 assert_eq!(profile.expected_http2_settings, CHROME_136_HTTP2_SETTINGS);
251 assert!(profile.has_any_http2_expectation());
252 assert_eq!(profile.expected_http2_check_count(), 3);
253 assert!(!profile.require_http2_observations);
254 assert!(profile.expectations.contains(TransportProfile::SETTINGS));
255 assert!(
256 profile
257 .expectations
258 .contains(TransportProfile::PSEUDO_HEADER_ORDER)
259 );
260 assert!(
261 profile
262 .expectations
263 .contains(TransportProfile::HEADER_ORDER)
264 );
265 }
266
267 #[test]
268 fn chrome_136_reference_matches_default() {
269 assert_eq!(
270 TransportProfile::chrome_136_reference(),
271 TransportProfile::default()
272 );
273 }
274
275 #[test]
276 fn with_name_replaces_name_only() {
277 let profile = TransportProfile::default().with_name("firefox-130");
278 assert_eq!(profile.name, "firefox-130");
279 assert!(profile.has_any_http2_expectation());
280 }
281
282 #[test]
283 fn with_expectations_toggles_all_three_flags() {
284 let profile = TransportProfile::default().with_expectation_bits(0);
285 assert!(!profile.has_any_http2_expectation());
286 assert_eq!(profile.expected_http2_check_count(), 0);
287 }
288
289 #[test]
290 fn require_http2_observations_round_trips_via_serde()
291 -> std::result::Result<(), Box<dyn std::error::Error>> {
292 let profile = TransportProfile::default().with_require_http2_observations(true);
293 let json = serde_json::to_string(&profile)?;
294 let back: TransportProfile = serde_json::from_str(&json)?;
295 assert_eq!(profile, back);
296 Ok(())
297 }
298
299 #[test]
300 fn json_round_trip_default_profile() -> std::result::Result<(), Box<dyn std::error::Error>> {
301 let p = TransportProfile::default();
302 let json = serde_json::to_string(&p)?;
303 let back: TransportProfile = serde_json::from_str(&json)?;
304 assert_eq!(p, back);
305 Ok(())
306 }
307
308 #[test]
309 fn expectations_bitmask_count_matches_set_bits() {
310 let empty = Http2Expectations::from_bits(0);
311 assert!(empty.is_empty());
312 assert_eq!(empty.count(), 0);
313
314 let settings_only = Http2Expectations::from_bits(TransportProfile::SETTINGS);
315 assert_eq!(settings_only.count(), 1);
316 assert!(settings_only.contains(TransportProfile::SETTINGS));
317
318 let all = Http2Expectations::from_bits(Http2Expectations::ALL);
319 assert_eq!(all.count(), 3);
320 }
321}