Skip to main content

huginn_net_http/
analyzer.rs

1use crate::error::HuginnNetHttpError;
2use crate::filter::raw as raw_filter;
3use crate::filter::FilterConfig;
4use crate::matcher_api::HttpMatcher;
5use crate::output::HttpAnalysisResult;
6use crate::parser::packet::{parse_packet, IpPacket};
7use crate::process::{FlowKey, HttpProcessors, PoolStats, SharedHttpMatcher, TcpFlow, WorkerPool};
8use pcap_file::pcap::PcapReader;
9use pnet::datalink::{self, Channel, Config};
10use std::fs::File;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::mpsc::Sender;
13use std::sync::Arc;
14use tracing::{debug, error};
15use ttl_cache::TtlCache;
16
17#[derive(Debug, Clone)]
18struct ParallelConfig {
19    num_workers: usize,
20    queue_size: usize,
21    batch_size: usize,
22    timeout_ms: u64,
23}
24
25/// An HTTP-focused passive fingerprinting analyzer.
26///
27/// Extracts HTTP/1.x and HTTP/2 observable signals and optionally matches them
28/// against a signature database for browser and web-server identification.
29///
30/// # Examples
31///
32/// **Sequential: raw HTTP signals, no database:**
33///
34/// ```no_run
35/// use huginn_net_http::HuginnNetHttp;
36/// use std::sync::mpsc;
37///
38/// let (tx, rx) = mpsc::channel();
39/// HuginnNetHttp::new(1000).analyze_pcap("capture.pcap", tx, None).unwrap();
40/// for result in rx {
41///     if let Some(req) = result.http_request { println!("{req}"); }
42/// }
43/// ```
44///
45/// **Sequential with browser/server matching (`huginn-net-db`):**
46///
47/// ```ignore
48/// use huginn_net_http::HuginnNetHttp;
49/// use huginn_net_db::{Database, SharedHttpSignatureMatcher};
50/// use std::sync::{mpsc, Arc};
51///
52/// let db = Database::load_default().unwrap();
53/// let matcher = Arc::new(SharedHttpSignatureMatcher::from_database(&db));
54/// let (tx, rx) = mpsc::channel();
55/// HuginnNetHttp::new(1000)
56///     .with_matcher(matcher)
57///     .analyze_pcap("capture.pcap", tx, None)
58///     .unwrap();
59/// ```
60///
61/// **Parallel: flow-based routing so HTTP reassembly state stays per-worker:**
62///
63/// ```no_run
64/// use huginn_net_http::HuginnNetHttp;
65/// use std::sync::mpsc;
66///
67/// let (tx, rx) = mpsc::channel();
68/// let mut analyzer = HuginnNetHttp::new(1000).with_parallel(2, 100, 16, 10);
69/// // Results are delivered via the sender given to init_pool.
70/// analyzer.init_pool(tx.clone()).unwrap();
71/// analyzer.analyze_pcap("capture.pcap", tx, None).unwrap();
72/// ```
73pub struct HuginnNetHttp {
74    http_flows: TtlCache<FlowKey, TcpFlow>,
75    http_processors: HttpProcessors,
76    parallel_config: Option<ParallelConfig>,
77    worker_pool: Option<Arc<WorkerPool>>,
78    matcher: Option<SharedHttpMatcher>,
79    max_connections: usize,
80    filter_config: Option<FilterConfig>,
81}
82
83impl HuginnNetHttp {
84    /// Creates a new instance of `HuginnNetHttp` in sequential mode.
85    ///
86    /// # Parameters
87    /// - `max_connections`: Maximum number of HTTP flows to track
88    pub fn new(max_connections: usize) -> Self {
89        Self {
90            http_flows: TtlCache::new(max_connections),
91            http_processors: HttpProcessors::new(),
92            parallel_config: None,
93            worker_pool: None,
94            matcher: None,
95            max_connections,
96            filter_config: None,
97        }
98    }
99
100    /// Enable parallel processing (builder pattern).
101    ///
102    /// # Parameters
103    /// - `num_workers`: Number of worker threads (recommended: 2 for HTTP due to flow tracking)
104    /// - `queue_size`: Size of each worker's packet queue (typical: 100-200)
105    /// - `batch_size`: Maximum packets to process in one batch (typical: 8-32, recommended: 16)
106    /// - `timeout_ms`: Worker receive timeout in milliseconds (typical: 5-20, recommended: 10)
107    pub fn with_parallel(
108        mut self,
109        num_workers: usize,
110        queue_size: usize,
111        batch_size: usize,
112        timeout_ms: u64,
113    ) -> Self {
114        self.parallel_config =
115            Some(ParallelConfig { num_workers, queue_size, batch_size, timeout_ms });
116        self
117    }
118
119    /// Plug a signature matcher (e.g. `huginn_net_db::SharedHttpSignatureMatcher`).
120    ///
121    /// Without a matcher, `analyze_*` still returns observable HTTP signals
122    /// but every match quality is reported as `Disabled`.
123    pub fn with_matcher(mut self, matcher: SharedHttpMatcher) -> Self {
124        self.matcher = Some(matcher);
125        self
126    }
127
128    /// Configure packet filtering (builder pattern)
129    pub fn with_filter(mut self, config: FilterConfig) -> Self {
130        self.filter_config = Some(config);
131        self
132    }
133
134    /// Initializes the worker pool for parallel processing.
135    ///
136    /// Must be called after `with_parallel` and before calling `analyze_network` or `analyze_pcap`.
137    pub fn init_pool(
138        &mut self,
139        result_tx: Sender<HttpAnalysisResult>,
140    ) -> Result<(), HuginnNetHttpError> {
141        if let Some(config) = &self.parallel_config {
142            let pool = WorkerPool::new(
143                config.num_workers,
144                config.queue_size,
145                config.batch_size,
146                config.timeout_ms,
147                result_tx,
148                self.matcher.clone(),
149                self.max_connections,
150                self.filter_config.clone(),
151            )?;
152            self.worker_pool = Some(pool);
153            Ok(())
154        } else {
155            Err(HuginnNetHttpError::Misconfiguration(
156                "Parallel config not set. Use with_parallel() to enable parallel processing"
157                    .to_string(),
158            ))
159        }
160    }
161
162    /// Returns a reference to the worker pool if initialized.
163    pub fn worker_pool(&self) -> Option<Arc<WorkerPool>> {
164        self.worker_pool.as_ref().map(Arc::clone)
165    }
166
167    /// Returns current worker pool statistics if parallel mode is active.
168    pub fn stats(&self) -> Option<PoolStats> {
169        self.worker_pool.as_ref().map(|pool| pool.stats())
170    }
171
172    fn process_with<F>(
173        &mut self,
174        packet_fn: F,
175        sender: Sender<HttpAnalysisResult>,
176        cancel_signal: Option<Arc<AtomicBool>>,
177    ) -> Result<(), HuginnNetHttpError>
178    where
179        F: FnMut() -> Option<Result<Vec<u8>, HuginnNetHttpError>>,
180    {
181        if self.parallel_config.is_some() {
182            self.process_parallel(packet_fn, cancel_signal)
183        } else {
184            self.process_sequential(packet_fn, sender, cancel_signal)
185        }
186    }
187
188    fn process_sequential<F>(
189        &mut self,
190        mut packet_fn: F,
191        sender: Sender<HttpAnalysisResult>,
192        cancel_signal: Option<Arc<AtomicBool>>,
193    ) -> Result<(), HuginnNetHttpError>
194    where
195        F: FnMut() -> Option<Result<Vec<u8>, HuginnNetHttpError>>,
196    {
197        while let Some(packet_result) = packet_fn() {
198            if let Some(ref cancel) = cancel_signal {
199                if cancel.load(Ordering::Relaxed) {
200                    debug!("Cancellation signal received, stopping packet processing");
201                    break;
202                }
203            }
204
205            match packet_result {
206                Ok(packet) => match self.process_packet(&packet) {
207                    Ok(result) => {
208                        if sender.send(result).is_err() {
209                            error!("Receiver dropped, stopping packet processing");
210                            break;
211                        }
212                    }
213                    Err(http_error) => {
214                        debug!("Error processing packet: {}", http_error);
215                    }
216                },
217                Err(e) => {
218                    error!("Failed to read packet: {}", e);
219                }
220            }
221        }
222        Ok(())
223    }
224
225    fn process_parallel<F>(
226        &mut self,
227        mut packet_fn: F,
228        cancel_signal: Option<Arc<AtomicBool>>,
229    ) -> Result<(), HuginnNetHttpError>
230    where
231        F: FnMut() -> Option<Result<Vec<u8>, HuginnNetHttpError>>,
232    {
233        let worker_pool = self.worker_pool.as_ref().ok_or_else(|| {
234            HuginnNetHttpError::Misconfiguration("Worker pool not initialized".to_string())
235        })?;
236
237        while let Some(packet_result) = packet_fn() {
238            if let Some(ref cancel) = cancel_signal {
239                if cancel.load(Ordering::Relaxed) {
240                    debug!("Cancellation signal received, stopping packet processing");
241                    break;
242                }
243            }
244
245            match packet_result {
246                Ok(packet) => {
247                    let _ = worker_pool.dispatch(packet);
248                }
249                Err(e) => {
250                    error!("Failed to read packet: {}", e);
251                }
252            }
253        }
254
255        worker_pool.shutdown();
256        Ok(())
257    }
258
259    /// Analyzes network traffic from a live network interface for HTTP packets.
260    pub fn analyze_network(
261        &mut self,
262        interface_name: &str,
263        sender: Sender<HttpAnalysisResult>,
264        cancel_signal: Option<Arc<AtomicBool>>,
265    ) -> Result<(), HuginnNetHttpError> {
266        let interfaces = datalink::interfaces();
267        let interface = interfaces
268            .into_iter()
269            .find(|iface| iface.name == interface_name)
270            .ok_or_else(|| {
271                HuginnNetHttpError::Parse(format!(
272                    "Could not find network interface: {interface_name}"
273                ))
274            })?;
275
276        debug!("Using network interface: {}", interface.name);
277
278        let config = Config { promiscuous: true, ..Config::default() };
279
280        let (_tx, mut rx) = match datalink::channel(&interface, config) {
281            Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
282            Ok(_) => return Err(HuginnNetHttpError::Parse("Unhandled channel type".to_string())),
283            Err(e) => {
284                return Err(HuginnNetHttpError::Parse(format!("Unable to create channel: {e}")))
285            }
286        };
287
288        self.process_with(
289            move || match rx.next() {
290                Ok(packet) => Some(Ok(packet.to_vec())),
291                Err(e) => {
292                    Some(Err(HuginnNetHttpError::Parse(format!("Error receiving packet: {e}"))))
293                }
294            },
295            sender,
296            cancel_signal,
297        )
298    }
299
300    /// Analyzes HTTP packets from a PCAP file.
301    pub fn analyze_pcap(
302        &mut self,
303        pcap_path: &str,
304        sender: Sender<HttpAnalysisResult>,
305        cancel_signal: Option<Arc<AtomicBool>>,
306    ) -> Result<(), HuginnNetHttpError> {
307        let file = File::open(pcap_path)
308            .map_err(|e| HuginnNetHttpError::Parse(format!("Failed to open PCAP file: {e}")))?;
309        let mut pcap_reader = PcapReader::new(file)
310            .map_err(|e| HuginnNetHttpError::Parse(format!("Failed to create PCAP reader: {e}")))?;
311
312        self.process_with(
313            move || match pcap_reader.next_packet() {
314                Some(Ok(packet)) => Some(Ok(packet.data.to_vec())),
315                Some(Err(e)) => {
316                    Some(Err(HuginnNetHttpError::Parse(format!("Error reading PCAP packet: {e}"))))
317                }
318                None => None,
319            },
320            sender,
321            cancel_signal,
322        )
323    }
324
325    /// Processes a single packet and extracts HTTP information if present.
326    fn process_packet(&mut self, packet: &[u8]) -> Result<HttpAnalysisResult, HuginnNetHttpError> {
327        if let Some(ref filter) = self.filter_config {
328            if !raw_filter::apply(packet, filter) {
329                debug!("Filtered out packet before parsing");
330                return Ok(HttpAnalysisResult::empty());
331            }
332        }
333
334        let matcher: Option<&dyn HttpMatcher> =
335            self.matcher.as_deref().map(|m| m as &dyn HttpMatcher);
336
337        match parse_packet(packet) {
338            IpPacket::Ipv4(ipv4) => crate::process::process_ipv4_packet(
339                &ipv4,
340                &mut self.http_flows,
341                &self.http_processors,
342                matcher,
343            ),
344            IpPacket::Ipv6(ipv6) => crate::process::process_ipv6_packet(
345                &ipv6,
346                &mut self.http_flows,
347                &self.http_processors,
348                matcher,
349            ),
350            IpPacket::None => Ok(HttpAnalysisResult::empty()),
351        }
352    }
353}