leyline/profile/
fingerprint.rs1mod akamai;
2mod ja3;
3mod ja4;
4
5use crate::iana::is_grease;
6use crate::profile::{BrowserProfile, TlsProfile};
7use crate::{Error, Kind};
8
9#[derive(Debug, Clone, Default)]
10#[non_exhaustive]
11pub struct FingerprintSpec {
12 ja3: Option<String>,
13 ja4_r: Option<String>,
14 akamai: Option<String>,
15 header_order: Option<Vec<String>>,
16 base: Option<BrowserProfile>,
17 name: Option<String>,
18}
19
20impl FingerprintSpec {
21 #[must_use]
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 #[must_use]
27 pub fn ja3(mut self, raw: impl Into<String>) -> Self {
28 self.ja3 = Some(raw.into());
29 self
30 }
31
32 #[must_use]
33 pub fn ja4_r(mut self, raw: impl Into<String>) -> Self {
34 self.ja4_r = Some(raw.into());
35 self
36 }
37
38 #[must_use]
39 pub fn akamai(mut self, raw: impl Into<String>) -> Self {
40 self.akamai = Some(raw.into());
41 self
42 }
43
44 #[must_use]
45 pub fn header_order<I, S>(mut self, order: I) -> Self
46 where
47 I: IntoIterator<Item = S>,
48 S: Into<String>,
49 {
50 self.header_order = Some(order.into_iter().map(Into::into).collect());
51 self
52 }
53
54 #[must_use]
55 pub fn base(mut self, profile: BrowserProfile) -> Self {
56 self.base = Some(profile);
57 self
58 }
59
60 #[must_use]
61 pub fn name(mut self, name: impl Into<String>) -> Self {
62 self.name = Some(name.into());
63 self
64 }
65}
66
67fn config_error(format: &str, why: impl std::fmt::Display) -> Error {
68 Error::new(Kind::Config).with_message(format!("{format} fingerprint: {why}"))
69}
70
71impl BrowserProfile {
72 pub fn from_fingerprint(spec: FingerprintSpec) -> crate::Result<Self> {
73 let mut profile = spec.base.unwrap_or_else(|| (*Self::bare_shared()).clone());
74 if let Some(name) = spec.name {
75 profile.meta.name = name;
76 }
77 if spec.ja4_r.is_some() || spec.ja3.is_some() {
78 profile.tls.fingerprint = None;
79 }
80 if let Some(raw) = spec.ja4_r.as_deref() {
81 ja4::apply(&mut profile.tls, raw).map_err(|why| config_error("JA4_r", why))?;
82 }
83 if let Some(raw) = spec.ja3.as_deref() {
84 ja3::apply(&mut profile.tls, raw).map_err(|why| config_error("JA3", why))?;
85 }
86 crate::profile::permutation::validate(&profile.tls, false)
87 .map_err(|why| config_error("TLS", why))?;
88 if let Some(raw) = spec.akamai.as_deref() {
89 akamai::apply(&mut profile.h2, raw).map_err(|why| config_error("Akamai", why))?;
90 }
91 if spec.header_order.is_some() {
92 profile.meta.header_order = spec.header_order;
93 }
94 Ok(profile)
95 }
96}
97
98fn parse_list(field: &str, radix: u32, what: &str) -> Result<Vec<u16>, String> {
99 field
100 .split([',', '-'])
101 .filter(|item| !item.is_empty())
102 .map(|item| {
103 u16::from_str_radix(item.trim(), radix)
104 .map_err(|_| format!("{what} value {item:?} is not a 16-bit number"))
105 })
106 .collect()
107}
108
109fn iana_names(
110 tls: &mut TlsProfile,
111 ids: &[u16],
112 lookup: fn(u16) -> Option<&'static str>,
113 what: &str,
114) -> Result<Vec<String>, String> {
115 let mut names = Vec::with_capacity(ids.len());
116 for &id in ids {
117 if is_grease(id) {
118 tls.grease = true;
119 continue;
120 }
121 let name = lookup(id)
122 .ok_or_else(|| format!("{what} 0x{id:04x} ({id}) is not in the IANA registry"))?;
123 names.push(name.to_owned());
124 }
125 Ok(names)
126}