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