|
| 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 | +} |
0 commit comments