1use crate::error::HuginnNetHttpError;
2use crate::http::Header;
3use crate::http_common::HttpProcessor;
4use crate::observable::{ObservableHttpRequest, ObservableHttpResponse};
5use crate::{http, http2_parser, http_common, http_languages};
6use tracing::debug;
7
8pub struct Http2Processor {
14 parser: http2_parser::Http2Parser<'static>,
15}
16
17impl Http2Processor {
18 pub fn new() -> Self {
19 Self {
20 parser: http2_parser::Http2Parser::new(),
21 }
22 }
23}
24
25impl Default for Http2Processor {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl HttpProcessor for Http2Processor {
32 fn can_process_request(&self, data: &[u8]) -> bool {
33 if data.len() < 24 {
35 return false;
37 }
38
39 http2_parser::is_http2_traffic(data)
41 }
42
43 fn can_process_response(&self, data: &[u8]) -> bool {
44 if data.len() < 9 {
46 return false;
48 }
49
50 let data_str = String::from_utf8_lossy(&data[..data.len().min(20)]);
52 if data_str.starts_with("HTTP/1.") {
53 return false;
54 }
55
56 looks_like_http2_response(data)
58 }
59
60 fn has_complete_data(&self, data: &[u8]) -> bool {
61 has_complete_data(data)
62 }
63
64 fn process_request(
65 &self,
66 data: &[u8],
67 ) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
68 parse_http2_request(data, &self.parser)
69 }
70
71 fn process_response(
72 &self,
73 data: &[u8],
74 ) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
75 parse_http2_response(data, &self.parser)
76 }
77
78 fn supported_version(&self) -> http::Version {
79 http::Version::V20
80 }
81
82 fn name(&self) -> &'static str {
83 "HTTP/2"
84 }
85}
86
87fn convert_http2_request_to_observable(req: http2_parser::Http2Request) -> ObservableHttpRequest {
88 let mut headers_map = std::collections::HashMap::new();
90 for header in &req.headers {
91 if let Some(ref value) = header.value {
92 headers_map.insert(header.name.to_lowercase(), value.as_str());
93 }
94 }
95
96 let lang = headers_map
97 .get("accept-language")
98 .and_then(|accept_language| {
99 http_languages::get_highest_quality_language(accept_language.to_string())
100 });
101
102 let headers_in_order = convert_http2_headers_to_http_format(&req.headers, true);
103 let headers_absent = build_absent_headers_from_http2(&req.headers, true);
104
105 let user_agent = headers_map.get("user-agent").map(|s| s.to_string());
106
107 ObservableHttpRequest {
108 matching: huginn_net_db::observable_signals::HttpRequestObservation {
109 version: req.version,
110 horder: headers_in_order,
111 habsent: headers_absent,
112 expsw: extract_traffic_classification(user_agent.as_deref()),
113 },
114 lang,
115 user_agent,
116 headers: req.headers,
117 cookies: req.cookies.clone(),
118 referer: req.referer.clone(),
119 method: Some(req.method),
120 uri: Some(req.path),
121 }
122}
123
124fn convert_http2_response_to_observable(
125 res: http2_parser::Http2Response,
126) -> ObservableHttpResponse {
127 let headers_in_order = convert_http2_headers_to_http_format(&res.headers, false);
128 let headers_absent = build_absent_headers_from_http2(&res.headers, false);
129
130 ObservableHttpResponse {
131 matching: huginn_net_db::observable_signals::HttpResponseObservation {
132 version: res.version,
133 horder: headers_in_order,
134 habsent: headers_absent,
135 expsw: extract_traffic_classification(res.server.as_deref()),
136 },
137 headers: res.headers,
138 status_code: Some(res.status),
139 }
140}
141
142fn convert_http2_headers_to_http_format(
143 headers: &[http_common::HttpHeader],
144 is_request: bool,
145) -> Vec<Header> {
146 let mut headers_in_order: Vec<Header> = Vec::new();
147 let optional_list = if is_request {
148 http::request_optional_headers()
149 } else {
150 http::response_optional_headers()
151 };
152 let skip_value_list = if is_request {
153 http::request_skip_value_headers()
154 } else {
155 http::response_skip_value_headers()
156 };
157
158 for header in headers {
159 let header_name_lower = header.name.to_lowercase();
160 if optional_list.contains(&header_name_lower.as_str()) {
161 headers_in_order.push(http::Header::new(&header.name).optional());
162 } else if skip_value_list.contains(&header_name_lower.as_str()) {
163 headers_in_order.push(http::Header::new(&header.name));
164 } else {
165 headers_in_order
166 .push(http::Header::new(&header.name).with_optional_value(header.value.clone()));
167 }
168 }
169
170 headers_in_order
171}
172
173fn build_absent_headers_from_http2(
174 headers: &[http_common::HttpHeader],
175 is_request: bool,
176) -> Vec<Header> {
177 let mut headers_absent: Vec<Header> = Vec::new();
178 let common_list: Vec<&str> = if is_request {
179 http::request_common_headers()
180 } else {
181 http::response_common_headers()
182 };
183 let current_headers: Vec<String> = headers.iter().map(|h| h.name.to_lowercase()).collect();
184
185 for header in &common_list {
186 if !current_headers.contains(&header.to_lowercase()) {
187 headers_absent.push(http::Header::new(header));
188 }
189 }
190 headers_absent
191}
192
193fn parse_http2_request(
194 data: &[u8],
195 parser: &http2_parser::Http2Parser,
196) -> Result<Option<ObservableHttpRequest>, HuginnNetHttpError> {
197 match parser.parse_request(data) {
198 Ok(Some(req)) => {
199 let observable = convert_http2_request_to_observable(req);
200 Ok(Some(observable))
201 }
202 Ok(None) => {
203 debug!("Incomplete HTTP/2 request data");
204 Ok(None)
205 }
206 Err(e) => {
207 debug!("Failed to parse HTTP/2 request: {}", e);
208 Err(HuginnNetHttpError::Parse(format!(
209 "Failed to parse HTTP/2 request: {e}"
210 )))
211 }
212 }
213}
214
215fn parse_http2_response(
216 data: &[u8],
217 parser: &http2_parser::Http2Parser,
218) -> Result<Option<ObservableHttpResponse>, HuginnNetHttpError> {
219 match parser.parse_response(data) {
220 Ok(Some(res)) => {
221 let observable = convert_http2_response_to_observable(res);
222 Ok(Some(observable))
223 }
224 Ok(None) => {
225 debug!("Incomplete HTTP/2 response data");
226 Ok(None)
227 }
228 Err(e) => {
229 debug!("Failed to parse HTTP/2 response: {}", e);
230 Err(HuginnNetHttpError::Parse(format!(
231 "Failed to parse HTTP/2 response: {e}"
232 )))
233 }
234 }
235}
236
237fn extract_traffic_classification(value: Option<&str>) -> String {
238 value.unwrap_or("???").to_string()
239}
240
241pub fn looks_like_http2_response(data: &[u8]) -> bool {
243 if data.len() < 9 {
244 return false;
245 }
246
247 let frame_length = u32::from_be_bytes([0, data[0], data[1], data[2]]);
249 let frame_type = data[3];
250
251 if frame_length > 16384 {
253 return false;
254 }
255
256 matches!(frame_type, 0..=10)
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use huginn_net_db;
265
266 #[test]
267 fn test_http2_request_conversion() {
268 let req = http2_parser::Http2Request {
270 method: "GET".to_string(),
271 path: "/test".to_string(),
272 authority: Some("example.com".to_string()),
273 scheme: Some("https".to_string()),
274 version: http::Version::V20,
275 headers: vec![],
276 cookies: vec![],
277 referer: None,
278 stream_id: 1,
279 parsing_metadata: http_common::ParsingMetadata {
280 header_count: 0,
281 duplicate_headers: vec![],
282 case_variations: std::collections::HashMap::new(),
283 parsing_time_ns: 0,
284 has_malformed_headers: false,
285 request_line_length: 0,
286 total_headers_length: 0,
287 },
288 frame_sequence: vec![],
289 settings: http2_parser::Http2Settings::default(),
290 };
291
292 let observable = convert_http2_request_to_observable(req);
293
294 assert_eq!(observable.matching.version, http::Version::V20);
295 assert_eq!(observable.method, Some("GET".to_string()));
296 assert_eq!(observable.uri, Some("/test".to_string()));
297 }
298
299 #[test]
300 fn test_http2_response_conversion() {
301 let res = http2_parser::Http2Response {
302 status: 200,
303 version: http::Version::V20,
304 headers: vec![],
305 stream_id: 1,
306 parsing_metadata: http_common::ParsingMetadata {
307 header_count: 0,
308 duplicate_headers: vec![],
309 case_variations: std::collections::HashMap::new(),
310 parsing_time_ns: 0,
311 has_malformed_headers: false,
312 request_line_length: 0,
313 total_headers_length: 0,
314 },
315 frame_sequence: vec![],
316 server: Some("nginx/1.20".to_string()),
317 content_type: Some("text/html".to_string()),
318 };
319
320 let observable = convert_http2_response_to_observable(res);
321
322 assert_eq!(observable.matching.version, http::Version::V20);
323 assert_eq!(observable.status_code, Some(200));
324 assert_eq!(observable.matching.expsw, "nginx/1.20");
325 }
326
327 #[test]
328 fn test_get_diagnostic_for_http2() {
329 let diagnosis = http_common::get_diagnostic(None, None, None);
330 assert_eq!(diagnosis, http::HttpDiagnosis::Anonymous);
331 }
332
333 #[test]
334 fn test_get_diagnostic_with_http2_user_agent() {
335 let user_agent = Some("Mozilla/5.0 HTTP/2.0".to_string());
336 let os = "Linux".to_string();
337 let browser = Some("Firefox".to_string());
338 let ua_matcher: Option<(&String, &Option<String>)> = Some((&os, &browser));
339 let label = huginn_net_db::Label {
340 ty: huginn_net_db::Type::Specified,
341 class: None,
342 name: "Linux".to_string(),
343 flavor: None,
344 };
345 let signature_os_matcher: Option<&huginn_net_db::Label> = Some(&label);
346
347 let diagnosis = http_common::get_diagnostic(user_agent, ua_matcher, signature_os_matcher);
348 assert_eq!(diagnosis, http::HttpDiagnosis::Generic);
349 }
350}
351
352fn has_complete_data(data: &[u8]) -> bool {
354 if data.starts_with(crate::http2_parser::HTTP2_CONNECTION_PREFACE) {
356 let frame_data = &data[crate::http2_parser::HTTP2_CONNECTION_PREFACE.len()..];
357 return has_complete_frames(frame_data);
358 }
359
360 has_complete_frames(data)
362}
363
364fn has_complete_frames(data: &[u8]) -> bool {
366 let mut remaining = data;
367
368 while remaining.len() >= 9 {
369 let length = u32::from_be_bytes([0, remaining[0], remaining[1], remaining[2]]);
371 let frame_type_byte = remaining[3];
372 let _flags = remaining[4];
373 let stream_id =
374 u32::from_be_bytes([remaining[5], remaining[6], remaining[7], remaining[8]])
375 & 0x7FFF_FFFF;
376
377 let frame_total_size = match usize::try_from(9_u32.saturating_add(length)) {
379 Ok(size) => size,
380 Err(_) => return false, };
382
383 if remaining.len() < frame_total_size {
384 return false; }
386
387 if frame_type_byte == 0x1 && stream_id > 0 {
389 return true;
391 }
392
393 remaining = &remaining[frame_total_size..];
395 }
396
397 false
398}
399
400#[cfg(test)]
401mod frame_detection_tests {
402 use super::*;
403 use crate::http2_parser::HTTP2_CONNECTION_PREFACE;
404
405 #[test]
406 fn test_no_preface() {
407 let data = b"GET /path HTTP/1.1\r\n";
408 assert!(!has_complete_data(data));
409 }
410
411 #[test]
412 fn test_preface_only() {
413 assert!(!has_complete_data(HTTP2_CONNECTION_PREFACE));
414 }
415
416 #[test]
417 fn test_incomplete_frame() {
418 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
419 data.extend_from_slice(&[0x00, 0x00, 0x04, 0x01, 0x00]);
421 assert!(!has_complete_data(&data));
422 }
423
424 #[test]
425 fn test_complete_settings_frame_no_headers() {
426 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
427 data.extend_from_slice(&[
429 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
434 assert!(!has_complete_data(&data)); }
436
437 #[test]
438 fn test_complete_headers_frame() {
439 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
440 data.extend_from_slice(&[
442 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ]);
448 assert!(has_complete_data(&data));
449 }
450
451 #[test]
452 fn test_incomplete_headers_frame() {
453 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
454 data.extend_from_slice(&[
456 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, ]);
462 assert!(!has_complete_data(&data));
463 }
464
465 #[test]
466 fn test_headers_frame_stream_id_zero() {
467 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
468 data.extend_from_slice(&[
470 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
475 assert!(!has_complete_data(&data)); }
477
478 #[test]
479 fn test_multiple_frames_with_headers() {
480 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
481
482 data.extend_from_slice(&[
484 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, ]);
490
491 data.extend_from_slice(&[
493 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ]);
499
500 assert!(has_complete_data(&data)); }
502
503 #[test]
504 fn test_response_frame_detection() {
505 let response_data = [
507 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ];
513
514 assert!(has_complete_data(&response_data));
515 }
516
517 #[test]
518 fn test_frame_too_large() {
519 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
520
521 data.extend_from_slice(&[
523 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, ]);
528
529 assert!(!has_complete_data(&data)); }
531 #[test]
532 fn test_can_process_request_detection() {
533 let processor = Http2Processor::new();
534
535 let mut valid_data = HTTP2_CONNECTION_PREFACE.to_vec();
537 valid_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01]);
538 assert!(processor.can_process_request(&valid_data));
539
540 let http1_data = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
542 assert!(!processor.can_process_request(http1_data));
543
544 let short_data = b"PRI * HTTP/2.0";
546 assert!(!processor.can_process_request(short_data));
547
548 let invalid_preface = b"GET * HTTP/2.0\r\n\r\nSM\r\n\r\n";
550 assert!(!processor.can_process_request(invalid_preface));
551 }
552
553 #[test]
554 fn test_can_process_response_detection() {
555 let processor = Http2Processor::new();
556
557 let valid_response = [
559 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ];
565 assert!(processor.can_process_response(&valid_response));
566
567 let http1_response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
569 assert!(!processor.can_process_response(http1_response));
570
571 let short_data = b"HTTP/2";
573 assert!(!processor.can_process_response(short_data));
574
575 let invalid_frame = [
577 0x00, 0x00, 0x04, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ];
583 assert!(!processor.can_process_response(&invalid_frame));
584 }
585
586 #[test]
587 fn test_looks_like_http2_response_edge_cases() {
588 for frame_type in 0..=10 {
590 let frame = [
591 0x00, 0x00, 0x04, frame_type, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ];
597 assert!(
598 looks_like_http2_response(&frame),
599 "Frame type {frame_type} should be valid"
600 );
601 }
602
603 let invalid_frame = [
605 0x00, 0x00, 0x04, 11, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, ];
611 assert!(!looks_like_http2_response(&invalid_frame));
612
613 let large_frame = [
615 0x00, 0x40, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, ];
620 assert!(!looks_like_http2_response(&large_frame));
621
622 let max_frame = [
624 0x00, 0x40, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, ];
629 assert!(looks_like_http2_response(&max_frame));
630 }
631
632 #[test]
633 fn test_processor_trait_methods() {
634 let processor = Http2Processor::new();
635
636 assert_eq!(processor.supported_version(), http::Version::V20);
638 assert_eq!(processor.name(), "HTTP/2");
639 }
640
641 #[test]
642 fn test_parse_http2_request_error_handling() {
643 let processor = Http2Processor::new();
644
645 let invalid_data = b"GET / HTTP/1.1\r\n\r\n";
647 let result = processor.process_request(invalid_data);
648 assert!(result.is_err());
649
650 let result = processor.process_request(HTTP2_CONNECTION_PREFACE);
652 match result {
653 Ok(None) => {} Ok(Some(_)) => panic!("Should return None for preface without frames"),
655 Err(e) => panic!("Should not error for valid preface: {e:?}"),
656 }
657 }
658
659 #[test]
660 fn test_parse_http2_response_error_handling() {
661 let processor = Http2Processor::new();
662
663 let result = processor.process_response(&[]);
665 match result {
666 Ok(None) => {} Ok(Some(_)) => panic!("Should return None for empty data"),
668 Err(e) => panic!("Should not error for empty data: {e:?}"),
669 }
670
671 let invalid_frame = [0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00];
673 let result = processor.process_response(&invalid_frame);
674 match result {
676 Ok(None) => {} Err(_) => {} Ok(Some(_)) => panic!("Should not return valid response for invalid frame"),
679 }
680 }
681
682 #[test]
683 fn test_extract_traffic_classification() {
684 assert_eq!(extract_traffic_classification(Some("test")), "test");
686
687 assert_eq!(extract_traffic_classification(None), "???");
689 }
690
691 #[test]
692 fn test_has_complete_data_edge_cases() {
693 let processor = Http2Processor::new();
694
695 assert!(!processor.has_complete_data(&[]));
697
698 assert!(!processor.has_complete_data(HTTP2_CONNECTION_PREFACE));
700
701 let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
703 data.extend_from_slice(&[0x00, 0x00, 0x04, 0x01]); assert!(!processor.has_complete_data(&data));
705
706 let response_frame = [
708 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, ];
713 assert!(processor.has_complete_data(&response_frame));
714 }
715
716 #[test]
717 fn test_conversion_functions_with_complex_data() {
718 let req = http2_parser::Http2Request {
720 method: "POST".to_string(),
721 path: "/api/test".to_string(),
722 authority: Some("api.example.com:443".to_string()),
723 scheme: Some("https".to_string()),
724 version: http::Version::V20,
725 headers: vec![http_common::HttpHeader {
726 name: "content-type".to_string(),
727 value: Some("application/json".to_string()),
728 position: 0,
729 source: http_common::HeaderSource::Http2Header,
730 }],
731 cookies: vec![http_common::HttpCookie {
732 name: "session".to_string(),
733 value: Some("abc123".to_string()),
734 position: 1,
735 }],
736 referer: Some("https://example.com".to_string()),
737 stream_id: 3,
738 parsing_metadata: http_common::ParsingMetadata {
739 header_count: 1,
740 duplicate_headers: vec![],
741 case_variations: std::collections::HashMap::new(),
742 parsing_time_ns: 12345,
743 has_malformed_headers: false,
744 request_line_length: 0,
745 total_headers_length: 25,
746 },
747 frame_sequence: vec![
748 http2_parser::Http2FrameType::Settings,
749 http2_parser::Http2FrameType::Headers,
750 http2_parser::Http2FrameType::Data,
751 ],
752 settings: http2_parser::Http2Settings {
753 header_table_size: Some(4096),
754 enable_push: Some(false),
755 max_concurrent_streams: Some(100),
756 initial_window_size: Some(65535),
757 max_frame_size: Some(16384),
758 max_header_list_size: Some(8192),
759 },
760 };
761
762 let observable = convert_http2_request_to_observable(req);
763
764 assert_eq!(observable.method, Some("POST".to_string()));
765 assert_eq!(observable.uri, Some("/api/test".to_string()));
766 assert_eq!(observable.matching.version, http::Version::V20);
767 assert!(!observable.headers.is_empty());
768
769 let res = http2_parser::Http2Response {
771 status: 201,
772 version: http::Version::V20,
773 headers: vec![
774 http_common::HttpHeader {
775 name: "server".to_string(),
776 value: Some("nginx/1.20".to_string()),
777 position: 0,
778 source: http_common::HeaderSource::Http2Header,
779 },
780 http_common::HttpHeader {
781 name: "content-type".to_string(),
782 value: Some("text/html".to_string()),
783 position: 1,
784 source: http_common::HeaderSource::Http2Header,
785 },
786 ],
787 stream_id: 5,
788 parsing_metadata: http_common::ParsingMetadata {
789 header_count: 2,
790 duplicate_headers: vec![],
791 case_variations: std::collections::HashMap::new(),
792 parsing_time_ns: 54321,
793 has_malformed_headers: false,
794 request_line_length: 0,
795 total_headers_length: 30,
796 },
797 frame_sequence: vec![
798 http2_parser::Http2FrameType::Headers,
799 http2_parser::Http2FrameType::Data,
800 ],
801 server: Some("nginx/1.20".to_string()),
802 content_type: Some("text/html".to_string()),
803 };
804
805 let observable = convert_http2_response_to_observable(res);
806
807 assert_eq!(observable.status_code, Some(201));
808 assert_eq!(observable.matching.version, http::Version::V20);
809 assert!(!observable.headers.is_empty());
810 }
811}