Skip to main content

huginn_net_http/http1/
process.rs

1use super::parser as http1_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 crate::http2::parser as http2_parser;
10use crate::http2::process as http2_process;
11use tracing::debug;
12
13/// HTTP/1.x Protocol Processor
14///
15/// Implements the HttpProcessor trait for HTTP/1.0 and HTTP/1.1 protocols.
16/// Handles both request and response processing with proper protocol detection.
17/// Contains a parser instance that is created once and reused.
18pub struct Http1Processor {
19    parser: http1_parser::Http1Parser,
20}
21
22impl Http1Processor {
23    pub fn new() -> Self {
24        Self { parser: http1_parser::Http1Parser::new() }
25    }
26}
27
28impl Default for Http1Processor {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl HttpProcessor for Http1Processor {
35    fn can_process_request(&self, data: &[u8]) -> bool {
36        if data.len() < 16 {
37            // Minimum for "GET / HTTP/1.1\r\n"
38            return false;
39        }
40
41        // VERY SPECIFIC: Must NOT be HTTP/2 first
42        if http2_parser::is_http2_traffic(data) {
43            return false;
44        }
45
46        let data_str = String::from_utf8_lossy(data);
47        let first_line = data_str.lines().next().unwrap_or("");
48
49        // SPECIFIC: Must be exact HTTP/1.x request line format (exactly 3 tokens).
50        let mut tokens = first_line.split_whitespace();
51        let (Some(method), Some(uri), Some(version)) =
52            (tokens.next(), tokens.next(), tokens.next())
53        else {
54            return false;
55        };
56        if tokens.next().is_some() {
57            return false;
58        }
59
60        // SPECIFIC: Valid HTTP/1.x methods only
61        let methods = [
62            "GET",
63            "POST",
64            "PUT",
65            "DELETE",
66            "HEAD",
67            "OPTIONS",
68            "PATCH",
69            "TRACE",
70            "CONNECT",
71            "PROPFIND",
72            "PROPPATCH",
73            "MKCOL",
74            "COPY",
75            "MOVE",
76            "LOCK",
77            "UNLOCK",
78        ];
79
80        // SPECIFIC: Must be exact HTTP/1.0 or HTTP/1.1
81        methods.contains(&method)
82            && (version == "HTTP/1.0" || version == "HTTP/1.1")
83            && !uri.is_empty() // Must have URI
84    }
85
86    fn can_process_response(&self, data: &[u8]) -> bool {
87        if data.len() < 12 {
88            // Minimum for "HTTP/1.1 200"
89            return false;
90        }
91
92        // VERY SPECIFIC: Must NOT look like HTTP/2 frames
93        if data.len() >= 9 && http2_process::looks_like_http2_response(data) {
94            return false;
95        }
96
97        let data_str = String::from_utf8_lossy(data);
98        let first_line = data_str.lines().next().unwrap_or("");
99
100        // SPECIFIC: Must be exact HTTP/1.x response line format.
101        let mut tokens = first_line.splitn(3, ' ');
102        let (Some(version_str), Some(status_str)) = (tokens.next(), tokens.next()) else {
103            return false;
104        };
105
106        // SPECIFIC: Must be exact HTTP/1.0 or HTTP/1.1 with valid status code
107        (version_str == "HTTP/1.0" || version_str == "HTTP/1.1")
108            && status_str.len() == 3
109            && status_str.chars().all(|c| c.is_ascii_digit())
110    }
111
112    fn has_complete_data(&self, data: &[u8]) -> bool {
113        has_complete_headers(data)
114    }
115
116    fn process_request(
117        &self,
118        data: &[u8],
119    ) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
120        parse_http1_request(data, &self.parser)
121    }
122
123    fn process_response(
124        &self,
125        data: &[u8],
126    ) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
127        parse_http1_response(data, &self.parser)
128    }
129
130    fn supported_version(&self) -> http::Version {
131        http::Version::V11 // Primary version, but also supports V10
132    }
133
134    fn name(&self) -> &'static str {
135        "HTTP/1.x"
136    }
137}
138
139/// Check if HTTP/1.x headers are complete (lightweight verification)
140pub fn has_complete_headers(data: &[u8]) -> bool {
141    // Fast byte-level check for \r\n\r\n
142    if data.len() < 4 {
143        return false;
144    }
145
146    // Look for the header separator pattern
147    for i in 0..data.len().saturating_sub(3) {
148        if data[i] == b'\r'
149            && data.get(i.saturating_add(1)) == Some(&b'\n')
150            && data.get(i.saturating_add(2)) == Some(&b'\r')
151            && data.get(i.saturating_add(3)) == Some(&b'\n')
152        {
153            return true;
154        }
155    }
156    false
157}
158
159fn convert_http1_request_to_observable(req: http1_parser::Http1Request) -> ObservableHttpRequest {
160    let lang = req
161        .accept_language
162        .and_then(http_languages::get_highest_quality_language);
163
164    let headers_in_order = convert_headers_to_http_format(&req.headers, true);
165    let headers_absent = build_absent_headers_from_new_parser(&req.headers, true);
166
167    ObservableHttpRequest {
168        matching: crate::observable::HttpRequestObservation {
169            version: req.version,
170            horder: headers_in_order,
171            habsent: headers_absent,
172            expsw: extract_traffic_classification(req.user_agent.as_deref()),
173        },
174        lang,
175        user_agent: req.user_agent.clone(),
176        headers: req.headers,
177        cookies: req.cookies.clone(),
178        referer: req.referer.clone(),
179        method: Some(req.method),
180        uri: Some(req.uri),
181    }
182}
183
184fn convert_http1_response_to_observable(
185    res: http1_parser::Http1Response,
186) -> ObservableHttpResponse {
187    let headers_in_order = convert_headers_to_http_format(&res.headers, false);
188    let headers_absent = build_absent_headers_from_new_parser(&res.headers, false);
189
190    ObservableHttpResponse {
191        matching: crate::observable::HttpResponseObservation {
192            version: res.version,
193            horder: headers_in_order,
194            habsent: headers_absent,
195            expsw: extract_traffic_classification(res.server.as_deref()),
196        },
197        headers: res.headers,
198        status_code: Some(res.status_code),
199    }
200}
201
202/// Convert HTTP headers to fingerprint format
203/// Formats headers according to p0f-style fingerprinting rules.
204pub fn convert_headers_to_http_format(
205    headers: &[http_common::HttpHeader],
206    is_request: bool,
207) -> Vec<Header> {
208    let mut headers_in_order: Vec<Header> = Vec::with_capacity(headers.len());
209    let optional_list = if is_request {
210        http::request_optional_headers()
211    } else {
212        http::response_optional_headers()
213    };
214    let skip_value_list = if is_request {
215        http::request_skip_value_headers()
216    } else {
217        http::response_skip_value_headers()
218    };
219
220    for header in headers {
221        if optional_list.contains(&header.name.as_str()) {
222            headers_in_order.push(http::Header::new(&header.name).optional());
223        } else if skip_value_list.contains(&header.name.as_str()) {
224            headers_in_order.push(http::Header::new(&header.name));
225        } else {
226            headers_in_order
227                .push(Header::new(&header.name).with_optional_value(header.value.clone()));
228        }
229    }
230
231    headers_in_order
232}
233
234/// Build list of absent common headers for fingerprinting
235/// Returns a list of common headers that are expected but not present in the request/response.
236pub fn build_absent_headers_from_new_parser(
237    headers: &[http_common::HttpHeader],
238    is_request: bool,
239) -> Vec<Header> {
240    let common_list: Vec<&str> = if is_request {
241        http::request_common_headers()
242    } else {
243        http::response_common_headers()
244    };
245
246    let current_headers: Vec<String> = headers.iter().map(|h| h.name.to_lowercase()).collect();
247
248    let mut headers_absent: Vec<Header> = Vec::with_capacity(common_list.len());
249    for header in &common_list {
250        if !current_headers.contains(&header.to_lowercase()) {
251            headers_absent.push(Header::new(header));
252        }
253    }
254    headers_absent
255}
256
257pub fn parse_http1_request(
258    data: &[u8],
259    parser: &http1_parser::Http1Parser,
260) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
261    match parser.parse_request(data) {
262        Ok(Some(req)) => {
263            let observable = convert_http1_request_to_observable(req);
264            Ok(Some(observable))
265        }
266        Ok(None) => {
267            debug!("Incomplete HTTP/1.x request data");
268            Ok(None)
269        }
270        Err(e) => {
271            debug!("Failed to parse HTTP/1.x request: {}", e);
272            Err(HuginnNetHttpError::Parse(format!("Failed to parse HTTP/1.x request: {e}")))
273        }
274    }
275}
276
277pub fn parse_http1_response(
278    data: &[u8],
279    parser: &http1_parser::Http1Parser,
280) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
281    match parser.parse_response(data) {
282        Ok(Some(res)) => {
283            let observable = convert_http1_response_to_observable(res);
284            Ok(Some(observable))
285        }
286        Ok(None) => {
287            debug!("Incomplete HTTP/1.x response data");
288            Ok(None)
289        }
290        Err(e) => {
291            debug!("Failed to parse HTTP/1.x response: {}", e);
292            Err(HuginnNetHttpError::Parse(format!("Failed to parse HTTP/1.x response: {e}")))
293        }
294    }
295}
296
297fn extract_traffic_classification(value: Option<&str>) -> String {
298    value.unwrap_or("???").to_string()
299}
300
301/// Check if data looks like HTTP/1.x response
302pub fn looks_like_http1_response(data: &[u8]) -> bool {
303    if data.len() < 12 {
304        // Minimum for "HTTP/1.1 200"
305        return false;
306    }
307
308    // Must NOT look like HTTP/2 frames
309    if data.len() >= 9 && http2_process::looks_like_http2_response(data) {
310        return false;
311    }
312
313    let data_str = String::from_utf8_lossy(data);
314    let first_line = data_str.lines().next().unwrap_or("");
315
316    // Must be exact HTTP/1.x response line format.
317    let mut tokens = first_line.split_whitespace();
318    let (Some(version_str), Some(status_str)) = (tokens.next(), tokens.next()) else {
319        return false;
320    };
321
322    if version_str != "HTTP/1.0" && version_str != "HTTP/1.1" {
323        return false;
324    }
325
326    status_str.len() == 3 && status_str.chars().all(|c| c.is_ascii_digit())
327}