use crate::akamai::{AkamaiFingerprint, Http2Priority, PseudoHeader, SettingId, SettingParameter};
use crate::error::HuginnNetHttpError;
use crate::http2_parser::Http2Parser;
use crate::http2_parser::{Http2Frame, Http2FrameType};
use crate::http_common::HttpHeader;
use hpack_patched::Decoder;
#[must_use]
pub fn calculate_frames_bytes_consumed(frames: &[Http2Frame]) -> usize {
frames.iter().map(|f| f.total_size()).sum()
}
pub fn extract_akamai_fingerprint_from_bytes(
data: &[u8],
) -> Result<AkamaiFingerprint, HuginnNetHttpError> {
let parser = Http2Parser::new();
let (frames, _) = parser
.parse_frames_skip_preface(data)
.map_err(|e| HuginnNetHttpError::Parse(e.to_string()))?;
extract_akamai_fingerprint(&frames)
}
pub fn extract_akamai_fingerprint(
frames: &[Http2Frame],
) -> Result<AkamaiFingerprint, HuginnNetHttpError> {
let settings = extract_settings_parameters(frames);
if settings.is_empty() {
return Err(HuginnNetHttpError::NoSettingsFrame);
}
let window_update = extract_window_update(frames);
let priority_frames = extract_priority_frames(frames);
let pseudo_header_order = extract_pseudo_header_order(frames)?;
Ok(AkamaiFingerprint::new(
settings,
window_update,
priority_frames,
pseudo_header_order,
))
}
fn extract_settings_parameters(frames: &[Http2Frame]) -> Vec<SettingParameter> {
frames
.iter()
.find(|f| f.frame_type == Http2FrameType::Settings && f.stream_id == 0)
.map(|frame| parse_settings_payload(&frame.payload))
.unwrap_or_default()
}
#[doc(hidden)]
pub fn parse_settings_payload(payload: &[u8]) -> Vec<SettingParameter> {
let mut settings = Vec::new();
let mut offset: usize = 0;
while offset.saturating_add(6) <= payload.len() {
if let (Some(&id_h), Some(&id_l), Some(&v0), Some(&v1), Some(&v2), Some(&v3)) = (
payload.get(offset),
payload.get(offset.saturating_add(1)),
payload.get(offset.saturating_add(2)),
payload.get(offset.saturating_add(3)),
payload.get(offset.saturating_add(4)),
payload.get(offset.saturating_add(5)),
) {
let id = u16::from_be_bytes([id_h, id_l]);
let value = u32::from_be_bytes([v0, v1, v2, v3]);
settings.push(SettingParameter { id: SettingId::from(id), value });
}
offset = offset.saturating_add(6);
}
settings
}
fn extract_window_update(frames: &[Http2Frame]) -> u32 {
frames
.iter()
.find(|f| f.frame_type == Http2FrameType::WindowUpdate && f.stream_id == 0)
.and_then(|frame| parse_window_update_payload(&frame.payload))
.unwrap_or(0)
}
#[doc(hidden)]
pub fn parse_window_update_payload(payload: &[u8]) -> Option<u32> {
if payload.len() < 4 {
return None;
}
let increment = u32::from_be_bytes([payload[0] & 0x7F, payload[1], payload[2], payload[3]]);
Some(increment)
}
fn extract_priority_frames(frames: &[Http2Frame]) -> Vec<Http2Priority> {
frames
.iter()
.filter(|f| f.frame_type == Http2FrameType::Priority)
.filter_map(|frame| parse_priority_payload(frame.stream_id, &frame.payload))
.collect()
}
#[doc(hidden)]
pub fn parse_priority_payload(stream_id: u32, payload: &[u8]) -> Option<Http2Priority> {
if payload.len() < 5 {
return None;
}
let exclusive = (payload[0] & 0x80) != 0;
let depends_on = u32::from_be_bytes([payload[0] & 0x7F, payload[1], payload[2], payload[3]]);
let weight = payload[4];
Some(Http2Priority { stream_id, exclusive, depends_on, weight })
}
const FLAG_PADDED: u8 = 0x08;
const FLAG_PRIORITY: u8 = 0x20;
fn extract_pseudo_header_order(
frames: &[Http2Frame],
) -> Result<Vec<PseudoHeader>, HuginnNetHttpError> {
let headers_frame = frames
.iter()
.find(|f| f.frame_type == Http2FrameType::Headers && f.stream_id > 0);
let Some(frame) = headers_frame else {
return Ok(Vec::new()); };
let mut hpack_payload = frame.payload.as_slice();
if frame.flags & FLAG_PADDED != 0 {
let Some((&pad_len, rest)) = hpack_payload.split_first() else {
return Err(HuginnNetHttpError::MalformedPseudoHeaders(
"PADDED flag set but payload is empty".into(),
));
};
let pad = pad_len as usize;
let end = rest.len().checked_sub(pad).ok_or_else(|| {
HuginnNetHttpError::MalformedPseudoHeaders(format!(
"pad length {pad} exceeds remaining payload of {} bytes",
rest.len()
))
})?;
hpack_payload = &rest[..end];
}
if frame.flags & FLAG_PRIORITY != 0 {
if hpack_payload.len() < 5 {
return Err(HuginnNetHttpError::MalformedPseudoHeaders(format!(
"PRIORITY flag set but only {} bytes remain (need 5)",
hpack_payload.len()
)));
}
hpack_payload = &hpack_payload[5..];
}
decode_headers(hpack_payload)
.map(|headers| {
headers
.iter()
.filter(|h| h.name.starts_with(':'))
.map(|h| PseudoHeader::from(h.name.as_str()))
.collect()
})
.map_err(|e| {
HuginnNetHttpError::MalformedPseudoHeaders(format!("HPACK decode error: {e:?}"))
})
}
fn decode_headers(payload: &[u8]) -> Result<Vec<HttpHeader>, hpack_patched::decoder::DecoderError> {
let mut decoder = Decoder::new();
let mut headers = Vec::new();
match decoder.decode(payload) {
Ok(header_list) => {
for (position, (name, value)) in header_list.into_iter().enumerate() {
if let (Ok(name_str), Ok(value_str)) =
(String::from_utf8(name), String::from_utf8(value))
{
let source = if name_str.starts_with(':') {
crate::http_common::HeaderSource::Http2PseudoHeader
} else {
crate::http_common::HeaderSource::Http2Header
};
headers.push(HttpHeader {
name: name_str,
value: Some(value_str),
position,
source,
});
}
}
Ok(headers)
}
Err(e) => Err(e),
}
}