use sha2::{Digest, Sha256};
use std::fmt;
use std::fmt::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum SettingId {
HeaderTableSize = 1,
EnablePush = 2,
MaxConcurrentStreams = 3,
InitialWindowSize = 4,
MaxFrameSize = 5,
MaxHeaderListSize = 6,
NoRfc7540Priorities = 9,
Unknown(u16),
}
impl From<u16> for SettingId {
fn from(id: u16) -> Self {
match id {
1 => Self::HeaderTableSize,
2 => Self::EnablePush,
3 => Self::MaxConcurrentStreams,
4 => Self::InitialWindowSize,
5 => Self::MaxFrameSize,
6 => Self::MaxHeaderListSize,
9 => Self::NoRfc7540Priorities,
other => Self::Unknown(other),
}
}
}
impl fmt::Display for SettingId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HeaderTableSize => write!(f, "HEADER_TABLE_SIZE"),
Self::EnablePush => write!(f, "ENABLE_PUSH"),
Self::MaxConcurrentStreams => write!(f, "MAX_CONCURRENT_STREAMS"),
Self::InitialWindowSize => write!(f, "INITIAL_WINDOW_SIZE"),
Self::MaxFrameSize => write!(f, "MAX_FRAME_SIZE"),
Self::MaxHeaderListSize => write!(f, "MAX_HEADER_LIST_SIZE"),
Self::NoRfc7540Priorities => write!(f, "NO_RFC7540_PRIORITIES"),
Self::Unknown(id) => write!(f, "UNKNOWN_{id}"),
}
}
}
impl SettingId {
#[must_use]
pub const fn as_u16(self) -> u16 {
match self {
Self::HeaderTableSize => 1,
Self::EnablePush => 2,
Self::MaxConcurrentStreams => 3,
Self::InitialWindowSize => 4,
Self::MaxFrameSize => 5,
Self::MaxHeaderListSize => 6,
Self::NoRfc7540Priorities => 9,
Self::Unknown(id) => id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettingParameter {
pub id: SettingId,
pub value: u32,
}
impl fmt::Display for SettingParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.id, self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Http2Priority {
pub stream_id: u32,
pub exclusive: bool,
pub depends_on: u32,
pub weight: u8, }
impl fmt::Display for Http2Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"stream={}, exclusive={}, depends_on={}, weight={}",
self.stream_id,
self.exclusive,
self.depends_on,
self.weight.saturating_add(1) )
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PseudoHeader {
Method,
Path,
Authority,
Scheme,
Status,
Unknown(String),
}
impl From<&str> for PseudoHeader {
fn from(s: &str) -> Self {
match s {
":method" => Self::Method,
":path" => Self::Path,
":authority" => Self::Authority,
":scheme" => Self::Scheme,
":status" => Self::Status,
other => Self::Unknown(other.to_string()),
}
}
}
impl fmt::Display for PseudoHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Method => write!(f, "m"),
Self::Path => write!(f, "p"),
Self::Authority => write!(f, "a"),
Self::Scheme => write!(f, "s"),
Self::Status => write!(f, "st"),
Self::Unknown(name) => write!(f, "?{name}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AkamaiFingerprint {
pub settings: Vec<SettingParameter>,
pub window_update: u32,
pub priority_frames: Vec<Http2Priority>,
pub pseudo_header_order: Vec<PseudoHeader>,
pub fingerprint: String,
pub hash: String,
}
impl AkamaiFingerprint {
#[must_use]
pub fn generate_fingerprint_string(
settings: &[SettingParameter],
window_update: u32,
priority_frames: &[Http2Priority],
pseudo_header_order: &[PseudoHeader],
) -> String {
let settings_str = if settings.is_empty() {
String::new()
} else {
settings
.iter()
.map(|s| format!("{}:{}", s.id.as_u16(), s.value))
.collect::<Vec<_>>()
.join(";")
};
let window_str = if window_update == 0 {
"00".to_string()
} else {
window_update.to_string()
};
let priority_str = if priority_frames.is_empty() {
"0".to_string()
} else {
priority_frames
.iter()
.map(|p| {
format!(
"{}:{}:{}:{}",
p.stream_id,
u8::from(p.exclusive),
p.depends_on,
u16::from(p.weight).saturating_add(1) )
})
.collect::<Vec<_>>()
.join(",")
};
let pseudo_str = pseudo_header_order
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
format!("{settings_str}|{window_str}|{priority_str}|{pseudo_str}")
}
#[must_use]
pub fn new(
settings: Vec<SettingParameter>,
window_update: u32,
priority_frames: Vec<Http2Priority>,
pseudo_header_order: Vec<PseudoHeader>,
) -> Self {
let fingerprint = Self::generate_fingerprint_string(
&settings,
window_update,
&priority_frames,
&pseudo_header_order,
);
let hash = Self::hash_fingerprint(&fingerprint);
Self { settings, window_update, priority_frames, pseudo_header_order, fingerprint, hash }
}
#[must_use]
pub fn hash_fingerprint(fingerprint: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(fingerprint.as_bytes());
let result = hasher.finalize();
result[..16]
.iter()
.fold(String::with_capacity(32), |mut acc, b| {
let _ = write!(acc, "{b:02x}");
acc
})
}
}
impl fmt::Display for AkamaiFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Akamai HTTP/2 Fingerprint:")?;
writeln!(f, " Fingerprint: {}", self.fingerprint)?;
writeln!(f, " Hash: {}", self.hash)?;
writeln!(f)?;
writeln!(f, " SETTINGS:")?;
for setting in &self.settings {
writeln!(f, " {setting}")?;
}
writeln!(f)?;
writeln!(f, " WINDOW_UPDATE: {}", self.window_update)?;
writeln!(f)?;
if self.priority_frames.is_empty() {
writeln!(f, " PRIORITY: none")?;
} else {
writeln!(f, " PRIORITY:")?;
for priority in &self.priority_frames {
writeln!(f, " {priority}")?;
}
}
writeln!(f)?;
writeln!(
f,
" Pseudo-headers: {}",
self.pseudo_header_order
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
}