Skip to main content

huginn_net_http/http/
common.rs

1use super::{HttpDiagnosis, Version};
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 super::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) -> 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) -> 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 an
134/// externally-observed OS signal.
135///
136/// This function compares the OS family reported by the User-Agent string against an
137/// OS name observed from the network (typically obtained from TCP fingerprinting by the
138/// caller, but this crate is intentionally agnostic about the source). A mismatch
139/// between the two is a hint of potential spoofing.
140///
141/// # Arguments
142/// * `user_agent` - Optional User-Agent string from HTTP headers
143/// * `ua_os_family` - OS family resolved from the User-Agent (UA→OS mapping in the database)
144/// * `network_os_name` - OS name observed from another source (e.g. TCP fingerprinting).
145///   `huginn-net-http` does not know how this was produced; it just compares strings.
146///
147/// # Returns
148/// * `HttpDiagnosis::Anonymous` - No User-Agent provided
149/// * `HttpDiagnosis::Generic` - User-Agent OS matches the externally observed OS
150/// * `HttpDiagnosis::Dishonest` - User-Agent OS differs from the externally observed OS (potential spoofing)
151/// * `HttpDiagnosis::None` - Insufficient data for comparison
152pub fn get_diagnostic(
153    user_agent: Option<String>,
154    ua_os_family: Option<&str>,
155    network_os_name: Option<&str>,
156) -> HttpDiagnosis {
157    match user_agent {
158        None => HttpDiagnosis::Anonymous,
159        Some(_ua) => match (ua_os_family, network_os_name) {
160            (Some(ua_name), Some(net_name)) => {
161                if ua_name.eq_ignore_ascii_case(net_name) {
162                    HttpDiagnosis::Generic
163                } else {
164                    HttpDiagnosis::Dishonest
165                }
166            }
167            _ => HttpDiagnosis::None,
168        },
169    }
170}