Skip to content

Commit b121182

Browse files
committed
akamai structure added
1 parent 85644a8 commit b121182

File tree

5 files changed

+444
-0
lines changed

5 files changed

+444
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

huginn-net-http/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ huginn-net-db = { workspace = true }
2020
lazy_static = { workspace = true }
2121
pcap-file = { workspace = true }
2222
pnet = { workspace = true }
23+
sha2 = { workspace = true }
2324
thiserror = { workspace = true }
2425
tracing = { workspace = true }
2526
ttl_cache = { workspace = true }

huginn-net-http/src/akamai.rs

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
use std::fmt;
2+
3+
/// HTTP/2 Setting parameter ID
4+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5+
#[repr(u16)]
6+
pub enum SettingId {
7+
HeaderTableSize = 1,
8+
EnablePush = 2,
9+
MaxConcurrentStreams = 3,
10+
InitialWindowSize = 4,
11+
MaxFrameSize = 5,
12+
MaxHeaderListSize = 6,
13+
NoRfc7540Priorities = 9,
14+
Unknown(u16),
15+
}
16+
17+
impl From<u16> for SettingId {
18+
fn from(id: u16) -> Self {
19+
match id {
20+
1 => Self::HeaderTableSize,
21+
2 => Self::EnablePush,
22+
3 => Self::MaxConcurrentStreams,
23+
4 => Self::InitialWindowSize,
24+
5 => Self::MaxFrameSize,
25+
6 => Self::MaxHeaderListSize,
26+
9 => Self::NoRfc7540Priorities,
27+
other => Self::Unknown(other),
28+
}
29+
}
30+
}
31+
32+
impl fmt::Display for SettingId {
33+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34+
match self {
35+
Self::HeaderTableSize => write!(f, "HEADER_TABLE_SIZE"),
36+
Self::EnablePush => write!(f, "ENABLE_PUSH"),
37+
Self::MaxConcurrentStreams => write!(f, "MAX_CONCURRENT_STREAMS"),
38+
Self::InitialWindowSize => write!(f, "INITIAL_WINDOW_SIZE"),
39+
Self::MaxFrameSize => write!(f, "MAX_FRAME_SIZE"),
40+
Self::MaxHeaderListSize => write!(f, "MAX_HEADER_LIST_SIZE"),
41+
Self::NoRfc7540Priorities => write!(f, "NO_RFC7540_PRIORITIES"),
42+
Self::Unknown(id) => write!(f, "UNKNOWN_{id}"),
43+
}
44+
}
45+
}
46+
47+
impl SettingId {
48+
/// Convert to numeric ID for fingerprint generation
49+
#[must_use]
50+
pub const fn as_u16(self) -> u16 {
51+
match self {
52+
Self::HeaderTableSize => 1,
53+
Self::EnablePush => 2,
54+
Self::MaxConcurrentStreams => 3,
55+
Self::InitialWindowSize => 4,
56+
Self::MaxFrameSize => 5,
57+
Self::MaxHeaderListSize => 6,
58+
Self::NoRfc7540Priorities => 9,
59+
Self::Unknown(id) => id,
60+
}
61+
}
62+
}
63+
64+
/// HTTP/2 Setting parameter (ID and value)
65+
#[derive(Debug, Clone, PartialEq, Eq)]
66+
pub struct SettingParameter {
67+
pub id: SettingId,
68+
pub value: u32,
69+
}
70+
71+
impl fmt::Display for SettingParameter {
72+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73+
write!(f, "{}: {}", self.id, self.value)
74+
}
75+
}
76+
77+
/// HTTP/2 Priority information
78+
///
79+
/// Weight in HTTP/2 spec is 0-255, but represents 1-256 (add 1 to value)
80+
#[derive(Debug, Clone, PartialEq, Eq)]
81+
pub struct Http2Priority {
82+
pub stream_id: u32,
83+
pub exclusive: bool,
84+
pub depends_on: u32,
85+
pub weight: u8, // 0-255 in frame, represents 1-256
86+
}
87+
88+
impl fmt::Display for Http2Priority {
89+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90+
write!(
91+
f,
92+
"stream={}, exclusive={}, depends_on={}, weight={}",
93+
self.stream_id,
94+
self.exclusive,
95+
self.depends_on,
96+
self.weight.saturating_add(1) // Display as 1-256
97+
)
98+
}
99+
}
100+
101+
/// Pseudo-header order in HTTP/2 HEADERS frame
102+
///
103+
/// Common orders:
104+
/// - Chrome: :method, :path, :authority, :scheme
105+
/// - Firefox: :method, :path, :authority, :scheme
106+
#[derive(Debug, Clone, PartialEq, Eq)]
107+
pub enum PseudoHeader {
108+
Method,
109+
Path,
110+
Authority,
111+
Scheme,
112+
Status,
113+
Unknown(String),
114+
}
115+
116+
impl From<&str> for PseudoHeader {
117+
fn from(s: &str) -> Self {
118+
match s {
119+
":method" => Self::Method,
120+
":path" => Self::Path,
121+
":authority" => Self::Authority,
122+
":scheme" => Self::Scheme,
123+
":status" => Self::Status,
124+
other => Self::Unknown(other.to_string()),
125+
}
126+
}
127+
}
128+
129+
impl fmt::Display for PseudoHeader {
130+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131+
match self {
132+
Self::Method => write!(f, "m"),
133+
Self::Path => write!(f, "p"),
134+
Self::Authority => write!(f, "a"),
135+
Self::Scheme => write!(f, "s"),
136+
Self::Status => write!(f, "st"),
137+
Self::Unknown(name) => write!(f, "?{name}"),
138+
}
139+
}
140+
}
141+
142+
/// Akamai HTTP/2 Fingerprint
143+
///
144+
/// Based on: https://www.blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf
145+
///
146+
/// Format: `S[;]|WU|P[,]|PS[,]`
147+
/// - S: Settings parameters (id:value;...)
148+
/// - WU: Window Update value
149+
/// - P: Priority frames (stream:exclusive:depends_on:weight,...)
150+
/// - PS: Pseudo-header order (m,p,a,s)
151+
///
152+
/// Example: `1:65536;2:0;3:1000;4:6291456;5:16384;6:262144|15663105|0|m,p,a,s`
153+
#[derive(Debug, Clone, PartialEq, Eq)]
154+
pub struct AkamaiFingerprint {
155+
/// SETTINGS frame parameters (order matters)
156+
pub settings: Vec<SettingParameter>,
157+
/// WINDOW_UPDATE initial value
158+
pub window_update: u32,
159+
/// PRIORITY frames
160+
pub priority_frames: Vec<Http2Priority>,
161+
/// Pseudo-header order from HEADERS frame
162+
pub pseudo_header_order: Vec<PseudoHeader>,
163+
/// Fingerprint string representation
164+
pub fingerprint: String,
165+
/// Hash of the fingerprint (for database lookup)
166+
pub hash: String,
167+
}
168+
169+
impl AkamaiFingerprint {
170+
/// Generate the Akamai fingerprint string
171+
///
172+
/// Format: `settings|window_update|priorities|pseudo_headers`
173+
#[must_use]
174+
pub fn generate_fingerprint_string(
175+
settings: &[SettingParameter],
176+
window_update: u32,
177+
priority_frames: &[Http2Priority],
178+
pseudo_header_order: &[PseudoHeader],
179+
) -> String {
180+
// Settings: id:value;id:value;...
181+
let settings_str = if settings.is_empty() {
182+
String::new()
183+
} else {
184+
settings
185+
.iter()
186+
.map(|s| format!("{}:{}", s.id.as_u16(), s.value))
187+
.collect::<Vec<_>>()
188+
.join(";")
189+
};
190+
191+
// Window Update: value or "00" if not present
192+
let window_str = if window_update == 0 {
193+
"00".to_string()
194+
} else {
195+
window_update.to_string()
196+
};
197+
198+
// Priority: stream:exclusive:depends_on:weight,...
199+
let priority_str = if priority_frames.is_empty() {
200+
"0".to_string()
201+
} else {
202+
priority_frames
203+
.iter()
204+
.map(|p| {
205+
format!(
206+
"{}:{}:{}:{}",
207+
p.stream_id,
208+
u8::from(p.exclusive),
209+
p.depends_on,
210+
p.weight.saturating_add(1) // Weight is 1-256, not 0-255
211+
)
212+
})
213+
.collect::<Vec<_>>()
214+
.join(",")
215+
};
216+
217+
// Pseudo-headers: m,p,a,s
218+
let pseudo_str = pseudo_header_order
219+
.iter()
220+
.map(std::string::ToString::to_string)
221+
.collect::<Vec<_>>()
222+
.join(",");
223+
224+
format!("{settings_str}|{window_str}|{priority_str}|{pseudo_str}")
225+
}
226+
227+
/// Create a new Akamai fingerprint
228+
///
229+
/// # Parameters
230+
/// - `settings`: SETTINGS frame parameters
231+
/// - `window_update`: WINDOW_UPDATE value
232+
/// - `priority_frames`: PRIORITY frames
233+
/// - `pseudo_header_order`: Pseudo-header order from HEADERS frame
234+
#[must_use]
235+
pub fn new(
236+
settings: Vec<SettingParameter>,
237+
window_update: u32,
238+
priority_frames: Vec<Http2Priority>,
239+
pseudo_header_order: Vec<PseudoHeader>,
240+
) -> Self {
241+
let fingerprint = Self::generate_fingerprint_string(
242+
&settings,
243+
window_update,
244+
&priority_frames,
245+
&pseudo_header_order,
246+
);
247+
248+
let hash = Self::hash_fingerprint(&fingerprint);
249+
250+
Self { settings, window_update, priority_frames, pseudo_header_order, fingerprint, hash }
251+
}
252+
253+
/// Hash the fingerprint for database lookup (SHA-256 truncated)
254+
#[must_use]
255+
pub fn hash_fingerprint(fingerprint: &str) -> String {
256+
use sha2::{Digest, Sha256};
257+
let mut hasher = Sha256::new();
258+
hasher.update(fingerprint.as_bytes());
259+
let result = hasher.finalize();
260+
// Truncate to first 16 bytes (32 hex chars) like JA3
261+
format!("{result:x}").chars().take(32).collect::<String>()
262+
}
263+
}
264+
265+
impl fmt::Display for AkamaiFingerprint {
266+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267+
writeln!(f, "Akamai HTTP/2 Fingerprint:")?;
268+
writeln!(f, " Fingerprint: {}", self.fingerprint)?;
269+
writeln!(f, " Hash: {}", self.hash)?;
270+
writeln!(f)?;
271+
writeln!(f, " SETTINGS:")?;
272+
for setting in &self.settings {
273+
writeln!(f, " {setting}")?;
274+
}
275+
writeln!(f)?;
276+
writeln!(f, " WINDOW_UPDATE: {}", self.window_update)?;
277+
writeln!(f)?;
278+
if self.priority_frames.is_empty() {
279+
writeln!(f, " PRIORITY: none")?;
280+
} else {
281+
writeln!(f, " PRIORITY:")?;
282+
for priority in &self.priority_frames {
283+
writeln!(f, " {priority}")?;
284+
}
285+
}
286+
writeln!(f)?;
287+
writeln!(
288+
f,
289+
" Pseudo-headers: {}",
290+
self.pseudo_header_order
291+
.iter()
292+
.map(std::string::ToString::to_string)
293+
.collect::<Vec<_>>()
294+
.join(", ")
295+
)
296+
}
297+
}

huginn-net-http/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
pub use huginn_net_db as db;
44
pub use huginn_net_db::http;
55

6+
pub mod akamai;
67
pub mod http1_parser;
78
pub mod http1_process;
89
pub mod http2_parser;
@@ -23,6 +24,7 @@ pub mod process;
2324
pub mod signature_matcher;
2425

2526
// Re-exports
27+
pub use akamai::{AkamaiFingerprint, Http2Priority, PseudoHeader, SettingId, SettingParameter};
2628
pub use error::*;
2729
pub use http_process::*;
2830
pub use observable::*;

0 commit comments

Comments
 (0)