1use std::collections::HashMap;
2
3use serde::Deserialize;
4
5use crate::profile::H3Profile;
6use crate::profile::preset::HeaderStyle;
7use crate::profile::registry::ProfileError;
8use crate::{Error, Kind};
9
10#[expect(
11 missing_docs,
12 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
13)]
14#[derive(Debug, Clone, Deserialize)]
15#[serde(deny_unknown_fields)]
16#[non_exhaustive]
17pub struct BrowserProfile {
18 pub meta: ProfileMeta,
19 pub tls: TlsProfile,
20 pub h2: H2Profile,
21 #[serde(default)]
22 pub identity: HashMap<String, PlatformIdentity>,
23 #[serde(default)]
24 pub h3: Option<H3Profile>,
25}
26
27#[expect(
28 missing_docs,
29 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
30)]
31#[derive(Debug, Clone, Deserialize)]
32#[serde(deny_unknown_fields)]
33#[non_exhaustive]
34pub struct ProfileMeta {
35 pub name: String,
36 pub browser: String,
37 pub version: u32,
38 #[serde(default)]
39 pub family: String,
40 #[serde(default)]
41 pub verified_against: String,
42 #[serde(default)]
43 pub captured_against: Option<String>,
44 #[serde(default)]
45 pub chromium_major: Option<u32>,
46 #[serde(default)]
47 pub ch_ua_brand: Option<String>,
48 #[serde(default)]
49 pub header_style: HeaderStyle,
50 #[serde(skip)]
51 pub(crate) header_order: Option<Vec<String>>,
52 #[serde(default, rename = "capture")]
53 _capture: serde::de::IgnoredAny,
54 #[serde(default, rename = "variant")]
55 _variant: serde::de::IgnoredAny,
56 #[serde(default, rename = "hello")]
57 _hello: serde::de::IgnoredAny,
58 #[serde(default, rename = "platform_browser")]
59 _platform_browser: serde::de::IgnoredAny,
60 #[serde(default, rename = "deprecated")]
61 _deprecated: serde::de::IgnoredAny,
62 #[serde(default, rename = "verified_at")]
63 _verified_at: serde::de::IgnoredAny,
64}
65
66#[expect(
67 missing_docs,
68 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
69)]
70#[derive(Debug, Clone, Deserialize)]
71#[serde(deny_unknown_fields)]
72#[non_exhaustive]
73pub struct TlsProfile {
74 pub ciphers: Vec<String>,
75 pub curves: Vec<String>,
76 #[serde(default)]
77 pub key_shares: Option<Vec<String>>,
78 pub sigalgs: Vec<String>,
79 #[serde(default)]
80 pub delegated_credentials: Option<String>,
81 #[serde(default)]
82 pub record_size_limit: Option<u16>,
83 #[serde(default)]
84 pub alps: Option<String>,
85 #[serde(default)]
86 pub alps_new_codepoint: bool,
87 #[serde(default)]
88 pub ocsp_stapling: bool,
89 #[serde(default)]
90 pub signed_cert_timestamps: bool,
91 #[serde(default)]
92 pub cert_compression: Vec<String>,
93 #[serde(default)]
94 pub permute_extensions: bool,
95 #[serde(default)]
96 pub extension_permutation: Option<Vec<u16>>,
97 #[serde(default)]
98 pub extension_tail: Vec<u16>,
99 #[serde(default = "default_grease")]
100 pub grease: bool,
101 #[serde(default)]
102 pub sigalg_grease: bool,
103 #[serde(default)]
104 pub ech_grease: bool,
105 #[serde(default)]
106 pub pre_shared_key: bool,
107 #[serde(default = "default_true")]
108 pub session_tickets: bool,
109 #[serde(default)]
110 pub request_trust_anchors: bool,
111 #[serde(default)]
112 pub fingerprint: Option<TlsFingerprint>,
113 #[serde(default)]
114 pub padding: bool,
115 #[serde(default)]
116 pub min_tls_version: Option<String>,
117 #[serde(default)]
118 pub tls12_extensions: bool,
119}
120
121const fn default_grease() -> bool {
122 true
123}
124
125const fn default_true() -> bool {
126 true
127}
128
129#[expect(
130 missing_docs,
131 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
132)]
133#[derive(Debug, Clone, Default, Deserialize)]
134#[serde(deny_unknown_fields)]
135#[non_exhaustive]
136pub struct TlsFingerprint {
137 #[serde(default)]
138 pub ja4: Option<String>,
139 #[serde(default)]
140 pub resumed_ja4: Option<String>,
141 #[serde(default)]
142 pub platforms: HashMap<String, TlsFingerprint>,
143}
144
145#[expect(
146 missing_docs,
147 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
148)]
149#[derive(Debug, Clone, Deserialize)]
150#[serde(deny_unknown_fields)]
151#[non_exhaustive]
152pub struct H2Profile {
153 #[serde(default)]
154 pub header_table_size: Option<u32>,
155 #[serde(default)]
156 pub enable_push: Option<bool>,
157 #[serde(default)]
158 pub max_concurrent_streams: Option<u32>,
159 #[serde(default)]
160 pub initial_stream_window_size: Option<u32>,
161 #[serde(default)]
162 pub initial_connection_window_size: Option<u32>,
163 #[serde(default)]
164 pub max_frame_size: Option<u32>,
165 #[serde(default)]
166 pub max_header_list_size: Option<u32>,
167 #[serde(default)]
168 pub unknown_setting8: Option<u32>,
169 #[serde(default)]
170 pub unknown_setting9: Option<u32>,
171 pub pseudo_order: Vec<String>,
172 pub settings_order: Vec<String>,
173 #[serde(default)]
174 pub default_priority: Option<H2PriorityProfile>,
175 #[serde(default)]
176 pub fingerprint: Option<H2Fingerprint>,
177 #[serde(default)]
178 pub platforms: HashMap<String, H2PlatformOverride>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
182#[serde(deny_unknown_fields)]
183#[non_exhaustive]
184pub struct H2PriorityProfile {
185 pub exclusive: bool,
186 pub stream_dependency: u32,
187 pub weight: u8,
188}
189
190#[expect(
191 missing_docs,
192 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
193)]
194#[derive(Debug, Clone, Default, Deserialize)]
195#[serde(deny_unknown_fields)]
196#[non_exhaustive]
197pub struct H2PlatformOverride {
198 #[serde(default)]
199 pub header_table_size: Option<u32>,
200 #[serde(default)]
201 pub enable_push: Option<bool>,
202 #[serde(default)]
203 pub max_concurrent_streams: Option<u32>,
204 #[serde(default)]
205 pub initial_stream_window_size: Option<u32>,
206 #[serde(default)]
207 pub initial_connection_window_size: Option<u32>,
208 #[serde(default)]
209 pub max_frame_size: Option<u32>,
210 #[serde(default)]
211 pub max_header_list_size: Option<u32>,
212 #[serde(default)]
213 pub unknown_setting8: Option<u32>,
214 #[serde(default)]
215 pub unknown_setting9: Option<u32>,
216 #[serde(default)]
217 pub omit_settings: Vec<String>,
218 #[serde(default)]
219 pub pseudo_order: Option<Vec<String>>,
220 #[serde(default)]
221 pub settings_order: Option<Vec<String>>,
222 #[serde(default)]
223 pub fingerprint: Option<H2Fingerprint>,
224}
225
226#[expect(
227 missing_docs,
228 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
229)]
230#[derive(Debug, Clone, Default, Deserialize)]
231#[serde(deny_unknown_fields)]
232#[non_exhaustive]
233pub struct H2Fingerprint {
234 #[serde(default)]
235 pub akamai: Option<String>,
236}
237
238#[expect(
239 missing_docs,
240 reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
241)]
242#[derive(Debug, Clone, Deserialize)]
243#[serde(deny_unknown_fields)]
244#[non_exhaustive]
245pub struct PlatformIdentity {
246 pub user_agent: String,
247 #[serde(default)]
248 pub sec_ch_ua: String,
249 #[serde(default)]
250 pub accept_language: Option<String>,
251}
252
253impl BrowserProfile {
254 pub fn from_toml(toml_str: &str) -> Result<Self, ProfileError> {
255 let mut profile: Self = toml::from_str(toml_str).map_err(ProfileError::parse)?;
256 profile.derive_sec_ch_ua();
257 crate::profile::permutation::validate(&profile.tls, false)
258 .map_err(|why| ProfileError::parse(format!("{}: {why}", profile.meta.name)))?;
259 if let Some(h3) = &profile.h3 {
260 h3.validate()
261 .map_err(|why| ProfileError::parse(format!("{}: {why}", profile.meta.name)))?;
262 }
263 for warning in profile.load_warnings() {
264 tracing::warn!(target: "leyline::profile", "{warning}");
265 }
266 Ok(profile)
267 }
268
269 fn derive_sec_ch_ua(&mut self) {
270 let Some(brand) = self.meta.ch_ua_brand.as_deref() else {
271 return;
272 };
273 let derived =
274 crate::profile::brand::sec_ch_ua(self.meta.version, Some((brand, self.meta.version)));
275 for identity in self.identity.values_mut() {
276 if identity.sec_ch_ua.is_empty() {
277 identity.sec_ch_ua.clone_from(&derived);
278 }
279 }
280 }
281
282 bench_pub! {
283 fn load_warnings(&self) -> Vec<String> {
284 let mut warnings = Vec::new();
285 let capture_unrecorded = self
286 .meta
287 .captured_against
288 .as_deref()
289 .is_none_or(|s| s.trim().is_empty());
290 if capture_unrecorded {
291 warnings.push(format!(
292 "{}: [meta] captured_against is missing — the exact browser build this \
293 profile was captured against is unrecorded",
294 self.meta.name
295 ));
296 }
297 warnings
298 }
299 }
300
301 bench_pub! {
302 fn identity_for(&self, platform: crate::profile::Platform) -> Option<&PlatformIdentity> {
303 self.identity.get(platform.identity_key())
304 }
305 }
306
307 pub fn expected_ja4(&self) -> Option<&str> {
308 self.tls.fingerprint.as_ref()?.ja4.as_deref()
309 }
310
311 pub fn expected_h2_fingerprint(&self) -> Option<&str> {
312 self.h2.fingerprint.as_ref()?.akamai.as_deref()
313 }
314}
315
316impl H2Profile {
317 bench_pub! {
318 fn resolve_for_platform(
319 &self,
320 platform: crate::profile::Platform,
321 ) -> Result<H2Profile, Error> {
322 let Some(over) = self.platforms.get(platform.identity_key()) else {
323 return Ok(self.clone());
324 };
325 let mut out = self.clone();
326
327 if let Some(v) = over.header_table_size {
328 out.header_table_size = Some(v);
329 }
330 if let Some(v) = over.enable_push {
331 out.enable_push = Some(v);
332 }
333 if let Some(v) = over.max_concurrent_streams {
334 out.max_concurrent_streams = Some(v);
335 }
336 if let Some(v) = over.initial_stream_window_size {
337 out.initial_stream_window_size = Some(v);
338 }
339 if let Some(v) = over.initial_connection_window_size {
340 out.initial_connection_window_size = Some(v);
341 }
342 if let Some(v) = over.max_frame_size {
343 out.max_frame_size = Some(v);
344 }
345 if let Some(v) = over.max_header_list_size {
346 out.max_header_list_size = Some(v);
347 }
348 if let Some(v) = over.unknown_setting8 {
349 out.unknown_setting8 = Some(v);
350 }
351 if let Some(v) = over.unknown_setting9 {
352 out.unknown_setting9 = Some(v);
353 }
354 if let Some(ref order) = over.pseudo_order {
355 out.pseudo_order = order.clone();
356 }
357 if let Some(ref order) = over.settings_order {
358 out.settings_order = order.clone();
359 }
360
361 for name in &over.omit_settings {
362 match name.as_str() {
363 "header_table_size" => out.header_table_size = None,
364 "enable_push" => out.enable_push = None,
365 "max_concurrent_streams" => out.max_concurrent_streams = None,
366 "initial_stream_window_size" => out.initial_stream_window_size = None,
367 "max_frame_size" => out.max_frame_size = None,
368 "max_header_list_size" => out.max_header_list_size = None,
369 "unknown_setting8" => out.unknown_setting8 = None,
370 "unknown_setting9" => out.unknown_setting9 = None,
371 other => {
372 return Err(Error::new(Kind::Config).with_message(format!(
373 "unknown name in [h2.platforms.{}].omit_settings: {other:?}",
374 platform.identity_key()
375 )));
376 }
377 }
378 }
379
380 Ok(out)
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests;