huginn_net_http/
http_common.rs

1use crate::http;
2use std::collections::HashMap;
3use std::time::Instant;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum HeaderSource {
7    Http1Line,
8    Http2PseudoHeader,
9    Http2Header,
10    Http3Header,
11}
12
13/// Represents an HTTP header with metadata
14#[derive(Debug, Clone, PartialEq)]
15pub struct HttpHeader {
16    pub name: String,
17    pub value: Option<String>,
18    /// Position in the original header sequence (0-based)
19    pub position: usize,
20    /// Source protocol/type of this header
21    pub source: HeaderSource,
22}
23
24impl HttpHeader {
25    pub fn new(name: &str, value: Option<&str>, position: usize, source: HeaderSource) -> Self {
26        Self { name: name.to_string(), value: value.map(String::from), position, source }
27    }
28}
29
30/// Represents an HTTP cookie
31#[derive(Debug, Clone, PartialEq)]
32pub struct HttpCookie {
33    pub name: String,
34    pub value: Option<String>,
35    /// Position in the cookie header (0-based)
36    pub position: usize,
37}
38
39/// Advanced parsing metadata for fingerprinting
40#[derive(Debug, Clone)]
41pub struct ParsingMetadata {
42    pub header_count: usize,
43    pub duplicate_headers: Vec<String>,
44    pub case_variations: HashMap<String, Vec<String>>,
45    pub parsing_time_ns: u64,
46    pub has_malformed_headers: bool,
47    pub request_line_length: usize,
48    pub total_headers_length: usize,
49}
50
51impl ParsingMetadata {
52    pub fn new() -> Self {
53        Self {
54            header_count: 0,
55            duplicate_headers: Vec::new(),
56            case_variations: HashMap::new(),
57            parsing_time_ns: 0,
58            has_malformed_headers: false,
59            request_line_length: 0,
60            total_headers_length: 0,
61        }
62    }
63
64    pub fn with_timing<F, R>(mut self, f: F) -> (R, Self)
65    where
66        F: FnOnce() -> R,
67    {
68        let start = Instant::now();
69        let result = f();
70        self.parsing_time_ns = start.elapsed().as_nanos() as u64;
71        (result, self)
72    }
73}
74
75impl Default for ParsingMetadata {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81use crate::observable::{ObservableHttpRequest, ObservableHttpResponse};
82
83/// Common trait for all HTTP parsers across different versions
84pub trait HttpParser {
85    /// Get the HTTP version this parser supports
86    fn supported_version(&self) -> http::Version;
87
88    /// Check if this parser can handle the given data
89    fn can_parse(&self, data: &[u8]) -> bool;
90
91    /// Get a human-readable name for this parser
92    fn name(&self) -> &'static str;
93
94    /// Parse HTTP request data into observable signals
95    /// Returns None if data cannot be parsed by this parser
96    fn parse_request(&self, data: &[u8]) -> Option<ObservableHttpRequest>;
97
98    /// Parse HTTP response data into observable signals  
99    /// Returns None if data cannot be parsed by this parser
100    fn parse_response(&self, data: &[u8]) -> Option<ObservableHttpResponse>;
101}
102
103/// Common trait for HTTP protocol processors
104pub trait HttpProcessor {
105    /// Check if this processor can handle the given request data
106    fn can_process_request(&self, data: &[u8]) -> bool;
107
108    /// Check if this processor can handle the given response data
109    fn can_process_response(&self, data: &[u8]) -> bool;
110
111    /// Check if the data appears to be complete for this protocol
112    fn has_complete_data(&self, data: &[u8]) -> bool;
113
114    /// Process HTTP request data and return observable request
115    fn process_request(
116        &self,
117        data: &[u8],
118    ) -> Result<Option<ObservableHttpRequest>, crate::error::HuginnNetHttpError>;
119
120    /// Process HTTP response data and return observable response  
121    fn process_response(
122        &self,
123        data: &[u8],
124    ) -> Result<Option<ObservableHttpResponse>, crate::error::HuginnNetHttpError>;
125
126    /// Get the HTTP version this processor handles
127    fn supported_version(&self) -> http::Version;
128
129    /// Get a human-readable name for this processor
130    fn name(&self) -> &'static str;
131}
132
133/// HTTP diagnostic function - determines the relationship between User-Agent and OS signature
134///
135/// This function analyzes the consistency between the reported User-Agent string and
136/// the detected OS signature from TCP fingerprinting to identify potential spoofing.
137///
138/// # Arguments
139/// * `user_agent` - Optional User-Agent string from HTTP headers
140/// * `ua_matcher` - Optional tuple of (OS name, browser flavor) extracted from User-Agent
141/// * `signature_os_matcher` - Optional OS label from TCP signature matching
142///
143/// # Returns
144/// * `HttpDiagnosis::Anonymous` - No User-Agent provided
145/// * `HttpDiagnosis::Generic` - User-Agent OS matches TCP signature OS
146/// * `HttpDiagnosis::Dishonest` - User-Agent OS differs from TCP signature OS (potential spoofing)
147/// * `HttpDiagnosis::None` - Insufficient data for comparison
148pub fn get_diagnostic(
149    user_agent: Option<String>,
150    ua_matcher: Option<(&String, &Option<String>)>,
151    signature_os_matcher: Option<&huginn_net_db::Label>,
152) -> http::HttpDiagnosis {
153    match user_agent {
154        None => http::HttpDiagnosis::Anonymous,
155        Some(_ua) => match (ua_matcher, signature_os_matcher) {
156            (Some((ua_name_db, _ua_flavor_db)), Some(signature_label_db)) => {
157                if ua_name_db.eq_ignore_ascii_case(&signature_label_db.name) {
158                    http::HttpDiagnosis::Generic
159                } else {
160                    http::HttpDiagnosis::Dishonest
161                }
162            }
163            _ => http::HttpDiagnosis::None,
164        },
165    }
166}