1use serde::Deserialize;
2
3use crate::audit::AuditData;
4use crate::core::{Error, Result};
5
6mod headers;
7
8pub use headers::HeaderOutcome;
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11#[non_exhaustive]
12pub struct Observed {
13 pub ja4: Option<String>,
14 pub ja3: Option<String>,
15 pub h2_fingerprint: Option<String>,
16 pub headers: Vec<(String, String)>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum FieldOutcome {
22 Match,
23 Mismatch { expected: String, observed: String },
24 Informational { expected: String, observed: String },
25 NotReported,
26 Absent { expected: String },
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct FingerprintReport {
32 pub ja4: FieldOutcome,
33 pub ja3: FieldOutcome,
34 pub h2_fingerprint: FieldOutcome,
35 pub headers: Vec<HeaderOutcome>,
36 pub header_order: FieldOutcome,
37}
38
39#[derive(Deserialize, Default)]
40struct EchoAll {
41 #[serde(default)]
42 tls: EchoTls,
43 #[serde(default)]
44 http2: EchoHttp2,
45 #[serde(default)]
46 http1: headers::EchoHttp1,
47}
48
49#[derive(Deserialize, Default)]
50struct EchoTls {
51 ja4: Option<String>,
52 ja3_hash: Option<String>,
53}
54
55#[derive(Deserialize, Default)]
56struct EchoHttp2 {
57 akamai_fingerprint: Option<String>,
58 #[serde(default)]
59 sent_frames: Vec<headers::EchoFrame>,
60}
61
62impl Observed {
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn ja4(mut self, value: impl Into<String>) -> Self {
68 self.ja4 = Some(value.into());
69 self
70 }
71
72 pub fn ja3(mut self, value: impl Into<String>) -> Self {
73 self.ja3 = Some(value.into());
74 self
75 }
76
77 pub fn h2_fingerprint(mut self, value: impl Into<String>) -> Self {
78 self.h2_fingerprint = Some(value.into());
79 self
80 }
81
82 pub fn header(mut self, name: &str, value: impl Into<String>) -> Self {
83 self.headers.push((name.to_ascii_lowercase(), value.into()));
84 self
85 }
86
87 pub fn from_json(text: &str) -> Result<Observed> {
88 let echo: EchoAll = serde_json::from_str(text).map_err(Error::from_json)?;
89 Ok(Observed {
90 headers: headers::echoed(&echo.http2.sent_frames, &echo.http1),
91 ja4: echo.tls.ja4,
92 ja3: echo.tls.ja3_hash,
93 h2_fingerprint: echo.http2.akamai_fingerprint,
94 })
95 }
96}
97
98impl FieldOutcome {
99 pub fn is_mismatch(&self) -> bool {
100 matches!(
101 self,
102 FieldOutcome::Mismatch { .. } | FieldOutcome::Absent { .. }
103 )
104 }
105}
106
107impl FingerprintReport {
108 pub fn is_match(&self) -> bool {
109 [
110 &self.ja4,
111 &self.ja3,
112 &self.h2_fingerprint,
113 &self.header_order,
114 ]
115 .iter()
116 .all(|outcome| !outcome.is_mismatch())
117 && self.headers.iter().all(|h| !h.outcome.is_mismatch())
118 }
119}
120
121impl AuditData {
122 pub fn compare(&self, observed: &Observed) -> FingerprintReport {
123 FingerprintReport {
124 ja4: outcome(&self.ja4, observed.ja4.as_deref(), false),
125 ja3: outcome(&self.ja3, observed.ja3.as_deref(), self.permutes_extensions),
126 h2_fingerprint: outcome(
127 &self.h2_fingerprint,
128 observed.h2_fingerprint.as_deref(),
129 false,
130 ),
131 headers: headers::compare(&self.request_headers, &observed.headers),
132 header_order: headers::order(&self.request_headers, &observed.headers),
133 }
134 }
135}
136
137fn outcome(expected: &str, observed: Option<&str>, order_varies: bool) -> FieldOutcome {
138 let Some(observed) = observed else {
139 return FieldOutcome::NotReported;
140 };
141 if expected.eq_ignore_ascii_case(observed.trim()) {
142 return FieldOutcome::Match;
143 }
144 let expected = expected.to_owned();
145 let observed = observed.to_owned();
146 if order_varies {
147 FieldOutcome::Informational { expected, observed }
148 } else {
149 FieldOutcome::Mismatch { expected, observed }
150 }
151}
152
153impl std::fmt::Display for FieldOutcome {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 FieldOutcome::Match => f.write_str("match"),
157 FieldOutcome::Mismatch { expected, observed } => {
158 write!(f, "mismatch (expected {expected}, observed {observed})")
159 }
160 FieldOutcome::Informational { expected, observed } => {
161 write!(
162 f,
163 "informational (expected {expected}, observed {observed})"
164 )
165 }
166 FieldOutcome::NotReported => f.write_str("not reported"),
167 FieldOutcome::Absent { expected } => write!(f, "absent (expected {expected})"),
168 }
169 }
170}
171
172impl std::fmt::Display for FingerprintReport {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 writeln!(f, "ja4: {}", self.ja4)?;
175 writeln!(f, "ja3: {}", self.ja3)?;
176 write!(f, "h2: {}", self.h2_fingerprint)?;
177 write!(f, "\nheader order: {}", self.header_order)?;
178 for header in &self.headers {
179 write!(f, "\nheader {}: {}", header.name, header.outcome)?;
180 }
181 Ok(())
182 }
183}