Skip to main content

leyline/profile/
fingerprint.rs

1mod 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            profile
82                .tls
83                .apply_ja4(raw)
84                .map_err(|why| config_error("JA4_r", why))?;
85        }
86        if let Some(raw) = spec.ja3.as_deref() {
87            profile
88                .tls
89                .apply_ja3(raw)
90                .map_err(|why| config_error("JA3", why))?;
91        }
92        profile
93            .tls
94            .validate(false)
95            .map_err(|why| config_error("TLS", why))?;
96        if let Some(raw) = spec.akamai.as_deref() {
97            profile
98                .h2
99                .apply_akamai(raw)
100                .map_err(|why| config_error("Akamai", why))?;
101        }
102        if spec.header_order.is_some() {
103            profile.meta.header_order = spec.header_order;
104        }
105        Ok(profile)
106    }
107}
108
109fn parse_list(field: &str, radix: u32, what: &str) -> Result<Vec<u16>, String> {
110    field
111        .split([',', '-'])
112        .filter(|item| !item.is_empty())
113        .map(|item| {
114            u16::from_str_radix(item.trim(), radix)
115                .map_err(|e| format!("{what} value {item:?} is not a 16-bit number: {e}"))
116        })
117        .collect()
118}
119
120impl TlsProfile {
121    fn iana_names(
122        &mut self,
123        ids: &[u16],
124        lookup: fn(u16) -> Option<&'static str>,
125        what: &str,
126    ) -> Result<Vec<String>, String> {
127        let mut names = Vec::with_capacity(ids.len());
128        for &id in ids {
129            if is_grease(id) {
130                self.grease = true;
131                continue;
132            }
133            let name = lookup(id)
134                .ok_or_else(|| format!("{what} 0x{id:04x} ({id}) is not in the IANA registry"))?;
135            names.push(name.to_owned());
136        }
137        Ok(names)
138    }
139}