Skip to main content

huginn_net_http/http2/
process.rs

1use super::parser as http2_parser;
2use crate::error::HuginnNetHttpError;
3use crate::http;
4use crate::http::common as http_common;
5use crate::http::common::HttpProcessor;
6use crate::http::languages as http_languages;
7use crate::http::observable::{ObservableHttpRequest, ObservableHttpResponse};
8use crate::http::Header;
9use tracing::debug;
10
11/// HTTP/2 Protocol Processor
12///
13/// Implements the HttpProcessor trait for HTTP/2 protocol.
14/// Handles both request and response processing with proper protocol detection.
15/// Contains a parser instance that is created once and reused.
16pub struct Http2Processor {
17    parser: http2_parser::Http2Parser<'static>,
18}
19
20impl Http2Processor {
21    pub fn new() -> Self {
22        Self { parser: http2_parser::Http2Parser::new() }
23    }
24}
25
26impl Default for Http2Processor {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl HttpProcessor for Http2Processor {
33    fn can_process_request(&self, data: &[u8]) -> bool {
34        // VERY SPECIFIC: HTTP/2 requests MUST start with exact connection preface
35        if data.len() < 24 {
36            // Minimum for preface
37            return false;
38        }
39
40        // SPECIFIC: Must start with exact HTTP/2 connection preface
41        http2_parser::is_http2_traffic(data)
42    }
43
44    fn can_process_response(&self, data: &[u8]) -> bool {
45        // VERY SPECIFIC: HTTP/2 responses are frame-based, not text-based
46        if data.len() < 9 {
47            // Minimum frame header size
48            return false;
49        }
50
51        // SPECIFIC: Must NOT look like HTTP/1.x first
52        let data_str = String::from_utf8_lossy(&data[..data.len().min(20)]);
53        if data_str.starts_with("HTTP/1.") {
54            return false;
55        }
56
57        // SPECIFIC: Must look like valid HTTP/2 frame
58        looks_like_http2_response(data)
59    }
60
61    fn has_complete_data(&self, data: &[u8]) -> bool {
62        has_complete_data(data)
63    }
64
65    fn process_request(
66        &self,
67        data: &[u8],
68    ) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
69        parse_http2_request(data, &self.parser)
70    }
71
72    fn process_response(
73        &self,
74        data: &[u8],
75    ) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
76        parse_http2_response(data, &self.parser)
77    }
78
79    fn supported_version(&self) -> http::Version {
80        http::Version::V20
81    }
82
83    fn name(&self) -> &'static str {
84        "HTTP/2"
85    }
86}
87
88pub fn convert_http2_request_to_observable(
89    req: http2_parser::Http2Request,
90) -> ObservableHttpRequest {
91    // Create map once for all lookups (only headers with values)
92    let mut headers_map = std::collections::HashMap::new();
93    for header in &req.headers {
94        if let Some(ref value) = header.value {
95            headers_map.insert(header.name.to_lowercase(), value.as_str());
96        }
97    }
98
99    let lang = headers_map
100        .get("accept-language")
101        .and_then(|accept_language| {
102            http_languages::get_highest_quality_language(accept_language.to_string())
103        });
104
105    let headers_in_order = convert_http2_headers_to_http_format(&req.headers, true);
106    let headers_absent = build_absent_headers_from_http2(&req.headers, true);
107
108    let user_agent = headers_map.get("user-agent").map(|s| s.to_string());
109
110    ObservableHttpRequest {
111        matching: crate::observable::HttpRequestObservation {
112            version: req.version,
113            horder: headers_in_order,
114            habsent: headers_absent,
115            expsw: extract_traffic_classification(user_agent.as_deref()),
116        },
117        lang,
118        user_agent,
119        headers: req.headers,
120        cookies: req.cookies.clone(),
121        referer: req.referer.clone(),
122        method: Some(req.method),
123        uri: Some(req.path),
124    }
125}
126
127pub fn convert_http2_response_to_observable(
128    res: http2_parser::Http2Response,
129) -> ObservableHttpResponse {
130    let headers_in_order = convert_http2_headers_to_http_format(&res.headers, false);
131    let headers_absent = build_absent_headers_from_http2(&res.headers, false);
132
133    ObservableHttpResponse {
134        matching: crate::observable::HttpResponseObservation {
135            version: res.version,
136            horder: headers_in_order,
137            habsent: headers_absent,
138            expsw: extract_traffic_classification(res.server.as_deref()),
139        },
140        headers: res.headers,
141        status_code: Some(res.status),
142    }
143}
144
145fn convert_http2_headers_to_http_format(
146    headers: &[http_common::HttpHeader],
147    is_request: bool,
148) -> Vec<Header> {
149    let mut headers_in_order: Vec<Header> = Vec::with_capacity(headers.len());
150    let optional_list = if is_request {
151        http::request_optional_headers()
152    } else {
153        http::response_optional_headers()
154    };
155    let skip_value_list = if is_request {
156        http::request_skip_value_headers()
157    } else {
158        http::response_skip_value_headers()
159    };
160
161    for header in headers {
162        let header_name_lower = header.name.to_lowercase();
163        if optional_list.contains(&header_name_lower.as_str()) {
164            headers_in_order.push(http::Header::new(&header.name).optional());
165        } else if skip_value_list.contains(&header_name_lower.as_str()) {
166            headers_in_order.push(http::Header::new(&header.name));
167        } else {
168            headers_in_order
169                .push(http::Header::new(&header.name).with_optional_value(header.value.clone()));
170        }
171    }
172
173    headers_in_order
174}
175
176fn build_absent_headers_from_http2(
177    headers: &[http_common::HttpHeader],
178    is_request: bool,
179) -> Vec<Header> {
180    let common_list: Vec<&str> = if is_request {
181        http::request_common_headers()
182    } else {
183        http::response_common_headers()
184    };
185
186    let current_headers: Vec<String> = headers.iter().map(|h| h.name.to_lowercase()).collect();
187
188    let mut headers_absent: Vec<Header> = Vec::with_capacity(common_list.len());
189    for header in &common_list {
190        if !current_headers.contains(&header.to_lowercase()) {
191            headers_absent.push(http::Header::new(header));
192        }
193    }
194    headers_absent
195}
196
197/// Parse HTTP/2 request and convert to ObservableHttpRequest
198///
199/// This function parses HTTP/2 request data and converts it to an ObservableHttpRequest
200/// that can be used for fingerprinting and analysis.
201pub fn parse_http2_request(
202    data: &[u8],
203    parser: &http2_parser::Http2Parser,
204) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
205    match parser.parse_request(data) {
206        Ok(Some(req)) => {
207            let observable = convert_http2_request_to_observable(req);
208            Ok(Some(observable))
209        }
210        Ok(None) => {
211            debug!("Incomplete HTTP/2 request data");
212            Ok(None)
213        }
214        Err(e) => {
215            debug!("Failed to parse HTTP/2 request: {}", e);
216            Err(HuginnNetHttpError::Parse(format!("Failed to parse HTTP/2 request: {e}")))
217        }
218    }
219}
220
221fn parse_http2_response(
222    data: &[u8],
223    parser: &http2_parser::Http2Parser,
224) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
225    match parser.parse_response(data) {
226        Ok(Some(res)) => {
227            let observable = convert_http2_response_to_observable(res);
228            Ok(Some(observable))
229        }
230        Ok(None) => {
231            debug!("Incomplete HTTP/2 response data");
232            Ok(None)
233        }
234        Err(e) => {
235            debug!("Failed to parse HTTP/2 response: {}", e);
236            Err(HuginnNetHttpError::Parse(format!("Failed to parse HTTP/2 response: {e}")))
237        }
238    }
239}
240
241pub fn extract_traffic_classification(value: Option<&str>) -> String {
242    value.unwrap_or("???").to_string()
243}
244
245/// Check if data looks like HTTP/2 response (frames without preface)
246pub fn looks_like_http2_response(data: &[u8]) -> bool {
247    if data.len() < 9 {
248        return false;
249    }
250
251    // HTTP/2 frame format: 3 bytes length + 1 byte type + 1 byte flags + 4 bytes stream_id
252    let frame_length = u32::from_be_bytes([0, data[0], data[1], data[2]]);
253    let frame_type = data[3];
254
255    // Check if frame length is more than the default max frame size
256    if frame_length > 16384 {
257        return false;
258    }
259
260    // Check if frame type is valid HTTP/2 frame type
261    // Common response frame types: HEADERS(1), DATA(0), SETTINGS(4), WINDOW_UPDATE(8)
262    matches!(frame_type, 0..=10)
263}
264
265/// Check if HTTP/2 data has complete frames for parsing
266pub fn has_complete_data(data: &[u8]) -> bool {
267    // For requests: Must have at least the connection preface
268    if data.starts_with(crate::http2_parser::HTTP2_CONNECTION_PREFACE) {
269        let frame_data = &data[crate::http2_parser::HTTP2_CONNECTION_PREFACE.len()..];
270        return has_complete_frames(frame_data);
271    }
272
273    // For responses: No preface, check frames directly
274    has_complete_frames(data)
275}
276
277/// Check if we have complete HTTP/2 frames (at least HEADERS frame)
278fn has_complete_frames(data: &[u8]) -> bool {
279    let mut remaining = data;
280
281    while remaining.len() >= 9 {
282        // Parse frame header (9 bytes)
283        let length = u32::from_be_bytes([0, remaining[0], remaining[1], remaining[2]]);
284        let frame_type_byte = remaining[3];
285        let _flags = remaining[4];
286        let stream_id =
287            u32::from_be_bytes([remaining[5], remaining[6], remaining[7], remaining[8]])
288                & 0x7FFF_FFFF;
289
290        // Check if frame is complete
291        let frame_total_size = match usize::try_from(9_u32.saturating_add(length)) {
292            Ok(size) => size,
293            Err(_) => return false, // Frame too large
294        };
295
296        if remaining.len() < frame_total_size {
297            return false; // Incomplete frame
298        }
299
300        // Check if this is a HEADERS frame (type 0x1) with a valid stream ID
301        if frame_type_byte == 0x1 && stream_id > 0 {
302            // We need at least one complete HEADERS frame
303            return true;
304        }
305
306        // Move to next frame
307        remaining = &remaining[frame_total_size..];
308    }
309
310    false
311}