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
25pub 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 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 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 pub fn with_matcher(mut self, matcher: SharedHttpMatcher) -> Self {
124 self.matcher = Some(matcher);
125 self
126 }
127
128 pub fn with_filter(mut self, config: FilterConfig) -> Self {
130 self.filter_config = Some(config);
131 self
132 }
133
134 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 pub fn worker_pool(&self) -> Option<Arc<WorkerPool>> {
164 self.worker_pool.as_ref().map(Arc::clone)
165 }
166
167 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 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 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 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}