Skip to content

Commit 793a3c8

Browse files
committed
akamai extractor added
1 parent b121182 commit 793a3c8

File tree

3 files changed

+332
-0
lines changed

3 files changed

+332
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use crate::akamai::{AkamaiFingerprint, Http2Priority, PseudoHeader, SettingId, SettingParameter};
2+
use crate::http2_parser::{Http2Frame, Http2FrameType};
3+
use crate::http_common::HttpHeader;
4+
use hpack_patched::Decoder;
5+
6+
/// Extract Akamai HTTP/2 fingerprint from HTTP/2 frames
7+
///
8+
/// This function analyzes HTTP/2 connection frames (SETTINGS, WINDOW_UPDATE, PRIORITY, HEADERS)
9+
/// to generate an Akamai fingerprint following the Blackhat EU 2017 specification.
10+
///
11+
/// # Parameters
12+
/// - `frames`: Slice of HTTP/2 frames captured from the connection start
13+
///
14+
/// # Returns
15+
/// - `Some(AkamaiFingerprint)` if enough frames are present
16+
/// - `None` if insufficient data or parsing errors
17+
///
18+
/// # Example
19+
/// ```no_run
20+
/// use huginn_net_http::akamai_extractor::extract_akamai_fingerprint;
21+
/// # use huginn_net_http::http2_parser::Http2Frame;
22+
/// # let frames: Vec<Http2Frame> = vec![];
23+
/// if let Some(fingerprint) = extract_akamai_fingerprint(&frames) {
24+
/// println!("Akamai: {}", fingerprint.fingerprint);
25+
/// }
26+
/// ```
27+
#[must_use]
28+
pub fn extract_akamai_fingerprint(frames: &[Http2Frame]) -> Option<AkamaiFingerprint> {
29+
let settings = extract_settings_parameters(frames);
30+
let window_update = extract_window_update(frames);
31+
let priority_frames = extract_priority_frames(frames);
32+
let pseudo_header_order = extract_pseudo_header_order(frames);
33+
34+
// Require at least SETTINGS frame to generate fingerprint
35+
if settings.is_empty() {
36+
return None;
37+
}
38+
39+
Some(AkamaiFingerprint::new(
40+
settings,
41+
window_update,
42+
priority_frames,
43+
pseudo_header_order,
44+
))
45+
}
46+
47+
/// Extract SETTINGS frame parameters
48+
///
49+
/// SETTINGS frame format (RFC 7540):
50+
/// Each setting is 6 bytes: [id:16][value:32]
51+
fn extract_settings_parameters(frames: &[Http2Frame]) -> Vec<SettingParameter> {
52+
frames
53+
.iter()
54+
.find(|f| f.frame_type == Http2FrameType::Settings && f.stream_id == 0)
55+
.map(|frame| parse_settings_payload(&frame.payload))
56+
.unwrap_or_default()
57+
}
58+
59+
#[doc(hidden)]
60+
pub fn parse_settings_payload(payload: &[u8]) -> Vec<SettingParameter> {
61+
let mut settings = Vec::new();
62+
let mut offset: usize = 0;
63+
64+
while offset.saturating_add(6) <= payload.len() {
65+
if let (Some(&id_h), Some(&id_l), Some(&v0), Some(&v1), Some(&v2), Some(&v3)) = (
66+
payload.get(offset),
67+
payload.get(offset.saturating_add(1)),
68+
payload.get(offset.saturating_add(2)),
69+
payload.get(offset.saturating_add(3)),
70+
payload.get(offset.saturating_add(4)),
71+
payload.get(offset.saturating_add(5)),
72+
) {
73+
let id = u16::from_be_bytes([id_h, id_l]);
74+
let value = u32::from_be_bytes([v0, v1, v2, v3]);
75+
76+
settings.push(SettingParameter { id: SettingId::from(id), value });
77+
}
78+
79+
offset = offset.saturating_add(6);
80+
}
81+
82+
settings
83+
}
84+
85+
/// Extract WINDOW_UPDATE value
86+
///
87+
/// WINDOW_UPDATE frame format (RFC 7540):
88+
/// [R:1][Window Size Increment:31]
89+
fn extract_window_update(frames: &[Http2Frame]) -> u32 {
90+
frames
91+
.iter()
92+
.find(|f| f.frame_type == Http2FrameType::WindowUpdate && f.stream_id == 0)
93+
.and_then(|frame| parse_window_update_payload(&frame.payload))
94+
.unwrap_or(0)
95+
}
96+
97+
#[doc(hidden)]
98+
pub fn parse_window_update_payload(payload: &[u8]) -> Option<u32> {
99+
if payload.len() < 4 {
100+
return None;
101+
}
102+
103+
// Clear reserved bit (first bit)
104+
let increment = u32::from_be_bytes([payload[0] & 0x7F, payload[1], payload[2], payload[3]]);
105+
106+
Some(increment)
107+
}
108+
109+
/// Extract PRIORITY frames
110+
///
111+
/// PRIORITY frame format (RFC 7540):
112+
/// [E:1][Stream Dependency:31][Weight:8]
113+
fn extract_priority_frames(frames: &[Http2Frame]) -> Vec<Http2Priority> {
114+
frames
115+
.iter()
116+
.filter(|f| f.frame_type == Http2FrameType::Priority)
117+
.filter_map(|frame| parse_priority_payload(frame.stream_id, &frame.payload))
118+
.collect()
119+
}
120+
121+
#[doc(hidden)]
122+
pub fn parse_priority_payload(stream_id: u32, payload: &[u8]) -> Option<Http2Priority> {
123+
if payload.len() < 5 {
124+
return None;
125+
}
126+
127+
let exclusive = (payload[0] & 0x80) != 0;
128+
let depends_on = u32::from_be_bytes([payload[0] & 0x7F, payload[1], payload[2], payload[3]]);
129+
let weight = payload[4];
130+
131+
Some(Http2Priority { stream_id, exclusive, depends_on, weight })
132+
}
133+
134+
/// Extract pseudo-header order from HEADERS frame
135+
///
136+
/// Pseudo-headers in HTTP/2:
137+
/// - `:method`
138+
/// - `:path`
139+
/// - `:authority`
140+
/// - `:scheme`
141+
/// - `:status` (responses only)
142+
fn extract_pseudo_header_order(frames: &[Http2Frame]) -> Vec<PseudoHeader> {
143+
// Find first HEADERS frame
144+
let headers_frame = frames
145+
.iter()
146+
.find(|f| f.frame_type == Http2FrameType::Headers && f.stream_id > 0);
147+
148+
if let Some(frame) = headers_frame {
149+
if let Ok(headers) = decode_headers(&frame.payload) {
150+
return headers
151+
.iter()
152+
.filter(|h| h.name.starts_with(':'))
153+
.map(|h| PseudoHeader::from(h.name.as_str()))
154+
.collect();
155+
}
156+
}
157+
158+
Vec::new()
159+
}
160+
161+
/// Decode HPACK-encoded headers
162+
fn decode_headers(payload: &[u8]) -> Result<Vec<HttpHeader>, hpack_patched::decoder::DecoderError> {
163+
let mut decoder = Decoder::new();
164+
let mut headers = Vec::new();
165+
166+
match decoder.decode(payload) {
167+
Ok(header_list) => {
168+
for (position, (name, value)) in header_list.into_iter().enumerate() {
169+
if let (Ok(name_str), Ok(value_str)) =
170+
(String::from_utf8(name), String::from_utf8(value))
171+
{
172+
let source = if name_str.starts_with(':') {
173+
crate::http_common::HeaderSource::Http2PseudoHeader
174+
} else {
175+
crate::http_common::HeaderSource::Http2Header
176+
};
177+
178+
headers.push(HttpHeader {
179+
name: name_str,
180+
value: Some(value_str),
181+
position,
182+
source,
183+
});
184+
}
185+
}
186+
Ok(headers)
187+
}
188+
Err(e) => Err(e),
189+
}
190+
}

huginn-net-http/src/lib.rs

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

66
pub mod akamai;
7+
pub mod akamai_extractor;
78
pub mod http1_parser;
89
pub mod http1_process;
910
pub mod http2_parser;
@@ -25,6 +26,7 @@ pub mod signature_matcher;
2526

2627
// Re-exports
2728
pub use akamai::{AkamaiFingerprint, Http2Priority, PseudoHeader, SettingId, SettingParameter};
29+
pub use akamai_extractor::extract_akamai_fingerprint;
2830
pub use error::*;
2931
pub use http_process::*;
3032
pub use observable::*;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use huginn_net_http::akamai_extractor::{
2+
parse_priority_payload, parse_settings_payload, parse_window_update_payload,
3+
};
4+
use huginn_net_http::{SettingId, SettingParameter};
5+
6+
#[test]
7+
fn test_parse_settings_payload() {
8+
let payload = vec![
9+
0x00, 0x01, // ID: HEADER_TABLE_SIZE (1)
10+
0x00, 0x00, 0x10, 0x00, // Value: 4096
11+
0x00, 0x02, // ID: ENABLE_PUSH (2)
12+
0x00, 0x00, 0x00, 0x00, // Value: 0
13+
];
14+
15+
let settings = parse_settings_payload(&payload);
16+
17+
assert_eq!(settings.len(), 2);
18+
assert_eq!(settings[0].id, SettingId::HeaderTableSize);
19+
assert_eq!(settings[0].value, 4096);
20+
assert_eq!(settings[1].id, SettingId::EnablePush);
21+
assert_eq!(settings[1].value, 0);
22+
}
23+
24+
#[test]
25+
fn test_parse_settings_payload_chrome() {
26+
let payload = vec![
27+
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // HEADER_TABLE_SIZE: 65536
28+
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // ENABLE_PUSH: 0
29+
0x00, 0x03, 0x00, 0x00, 0x03, 0xE8, // MAX_CONCURRENT_STREAMS: 1000
30+
0x00, 0x04, 0x00, 0x60, 0x00, 0x00, // INITIAL_WINDOW_SIZE: 6291456 (0x600000)
31+
0x00, 0x05, 0x00, 0x00, 0x40, 0x00, // MAX_FRAME_SIZE: 16384
32+
0x00, 0x06, 0x00, 0x04, 0x00, 0x00, // MAX_HEADER_LIST_SIZE: 262144
33+
];
34+
35+
let settings = parse_settings_payload(&payload);
36+
37+
assert_eq!(settings.len(), 6);
38+
assert_eq!(settings[0], SettingParameter { id: SettingId::HeaderTableSize, value: 65536 });
39+
assert_eq!(settings[1], SettingParameter { id: SettingId::EnablePush, value: 0 });
40+
assert_eq!(
41+
settings[2],
42+
SettingParameter { id: SettingId::MaxConcurrentStreams, value: 1000 }
43+
);
44+
assert_eq!(
45+
settings[3],
46+
SettingParameter { id: SettingId::InitialWindowSize, value: 6291456 }
47+
);
48+
assert_eq!(settings[4], SettingParameter { id: SettingId::MaxFrameSize, value: 16384 });
49+
assert_eq!(
50+
settings[5],
51+
SettingParameter { id: SettingId::MaxHeaderListSize, value: 262144 }
52+
);
53+
}
54+
55+
#[test]
56+
fn test_parse_window_update_payload() {
57+
let payload = vec![0x00, 0xEF, 0x00, 0x01]; // 15663105 = 0xEF0001
58+
let result = parse_window_update_payload(&payload);
59+
assert!(result.is_some(), "Failed to parse valid WINDOW_UPDATE payload");
60+
if let Some(increment) = result {
61+
assert_eq!(increment, 15663105);
62+
}
63+
}
64+
65+
#[test]
66+
fn test_parse_window_update_payload_firefox() {
67+
let payload = vec![0x00, 0xBF, 0x00, 0x01]; // 12517377 = 0xBF0001
68+
let result = parse_window_update_payload(&payload);
69+
assert!(result.is_some(), "Failed to parse valid Firefox WINDOW_UPDATE payload");
70+
if let Some(increment) = result {
71+
assert_eq!(increment, 12517377);
72+
}
73+
}
74+
75+
#[test]
76+
fn test_parse_window_update_payload_too_short() {
77+
let payload = vec![0x00, 0xEE, 0xFC];
78+
let result = parse_window_update_payload(&payload);
79+
assert!(result.is_none());
80+
}
81+
82+
#[test]
83+
fn test_parse_priority_payload() {
84+
let payload = vec![
85+
0x00, 0x00, 0x00, 0x00, // depends_on: 0 (no exclusive bit)
86+
220, // weight: 220
87+
];
88+
89+
let result = parse_priority_payload(1, &payload);
90+
assert!(result.is_some(), "Failed to parse valid PRIORITY payload");
91+
if let Some(priority) = result {
92+
assert_eq!(priority.stream_id, 1);
93+
assert!(!priority.exclusive);
94+
assert_eq!(priority.depends_on, 0);
95+
assert_eq!(priority.weight, 220);
96+
}
97+
}
98+
99+
#[test]
100+
fn test_parse_priority_payload_exclusive() {
101+
let payload = vec![
102+
0x80, 0x00, 0x00, 0x03, // depends_on: 3 with exclusive bit set
103+
200, // weight: 200
104+
];
105+
106+
let result = parse_priority_payload(5, &payload);
107+
assert!(result.is_some(), "Failed to parse valid PRIORITY payload with exclusive bit");
108+
if let Some(priority) = result {
109+
assert_eq!(priority.stream_id, 5);
110+
assert!(priority.exclusive);
111+
assert_eq!(priority.depends_on, 3);
112+
assert_eq!(priority.weight, 200);
113+
}
114+
}
115+
116+
#[test]
117+
fn test_parse_priority_payload_too_short() {
118+
let payload = vec![0x00, 0x00, 0x00];
119+
let result = parse_priority_payload(1, &payload);
120+
assert!(result.is_none());
121+
}
122+
123+
#[test]
124+
fn test_parse_settings_empty_payload() {
125+
let payload = vec![];
126+
let settings = parse_settings_payload(&payload);
127+
assert!(settings.is_empty());
128+
}
129+
130+
#[test]
131+
fn test_parse_settings_incomplete_setting() {
132+
let payload = vec![
133+
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // Complete setting
134+
0x00, 0x02, 0x00, // Incomplete setting (missing 3 bytes)
135+
];
136+
137+
let settings = parse_settings_payload(&payload);
138+
assert_eq!(settings.len(), 1);
139+
assert_eq!(settings[0].id, SettingId::HeaderTableSize);
140+
}

0 commit comments

Comments
 (0)