huginn_net_http/
http2_process.rs

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
8/// HTTP/2 Protocol Processor
9///
10/// Implements the HttpProcessor trait for HTTP/2 protocol.
11/// Handles both request and response processing with proper protocol detection.
12/// Contains a parser instance that is created once and reused.
13pub 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        // VERY SPECIFIC: HTTP/2 requests MUST start with exact connection preface
34        if data.len() < 24 {
35            // Minimum for preface
36            return false;
37        }
38
39        // SPECIFIC: Must start with exact HTTP/2 connection preface
40        http2_parser::is_http2_traffic(data)
41    }
42
43    fn can_process_response(&self, data: &[u8]) -> bool {
44        // VERY SPECIFIC: HTTP/2 responses are frame-based, not text-based
45        if data.len() < 9 {
46            // Minimum frame header size
47            return false;
48        }
49
50        // SPECIFIC: Must NOT look like HTTP/1.x first
51        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        // SPECIFIC: Must look like valid HTTP/2 frame
57        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    // Create map once for all lookups (only headers with values)
89    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
241/// Check if data looks like HTTP/2 response (frames without preface)
242pub fn looks_like_http2_response(data: &[u8]) -> bool {
243    if data.len() < 9 {
244        return false;
245    }
246
247    // HTTP/2 frame format: 3 bytes length + 1 byte type + 1 byte flags + 4 bytes stream_id
248    let frame_length = u32::from_be_bytes([0, data[0], data[1], data[2]]);
249    let frame_type = data[3];
250
251    // Check if frame length is more than the default max frame size
252    if frame_length > 16384 {
253        return false;
254    }
255
256    // Check if frame type is valid HTTP/2 frame type
257    // Common response frame types: HEADERS(1), DATA(0), SETTINGS(4), WINDOW_UPDATE(8)
258    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        // Create a mock HTTP/2 request
269        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
352/// Check if HTTP/2 data has complete frames for parsing
353fn has_complete_data(data: &[u8]) -> bool {
354    // For requests: Must have at least the connection preface
355    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    // For responses: No preface, check frames directly
361    has_complete_frames(data)
362}
363
364/// Check if we have complete HTTP/2 frames (at least HEADERS frame)
365fn has_complete_frames(data: &[u8]) -> bool {
366    let mut remaining = data;
367
368    while remaining.len() >= 9 {
369        // Parse frame header (9 bytes)
370        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        // Check if frame is complete
378        let frame_total_size = match usize::try_from(9_u32.saturating_add(length)) {
379            Ok(size) => size,
380            Err(_) => return false, // Frame too large
381        };
382
383        if remaining.len() < frame_total_size {
384            return false; // Incomplete frame
385        }
386
387        // Check if this is a HEADERS frame (type 0x1) with a valid stream ID
388        if frame_type_byte == 0x1 && stream_id > 0 {
389            // We need at least one complete HEADERS frame
390            return true;
391        }
392
393        // Move to next frame
394        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        // Add incomplete frame header (only 5 bytes instead of 9)
420        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        // Add complete SETTINGS frame (type 0x4)
428        data.extend_from_slice(&[
429            0x00, 0x00, 0x00, // Length: 0
430            0x04, // Type: SETTINGS
431            0x00, // Flags: 0
432            0x00, 0x00, 0x00, 0x00, // Stream ID: 0
433        ]);
434        assert!(!has_complete_data(&data)); // No HEADERS frame
435    }
436
437    #[test]
438    fn test_complete_headers_frame() {
439        let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
440        // Add complete HEADERS frame (type 0x1) with stream ID 1
441        data.extend_from_slice(&[
442            0x00, 0x00, 0x04, // Length: 4
443            0x01, // Type: HEADERS
444            0x00, // Flags: 0
445            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
446            0x00, 0x00, 0x00, 0x00, // Payload (4 bytes)
447        ]);
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        // Add incomplete HEADERS frame (missing payload)
455        data.extend_from_slice(&[
456            0x00, 0x00, 0x04, // Length: 4
457            0x01, // Type: HEADERS
458            0x00, // Flags: 0
459            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
460            0x00, 0x00, // Only 2 bytes of 4-byte payload
461        ]);
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        // Add HEADERS frame with stream ID 0 (invalid)
469        data.extend_from_slice(&[
470            0x00, 0x00, 0x00, // Length: 0
471            0x01, // Type: HEADERS
472            0x00, // Flags: 0
473            0x00, 0x00, 0x00, 0x00, // Stream ID: 0 (invalid)
474        ]);
475        assert!(!has_complete_data(&data)); // Stream ID 0 is invalid for HEADERS
476    }
477
478    #[test]
479    fn test_multiple_frames_with_headers() {
480        let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
481
482        // Add SETTINGS frame first
483        data.extend_from_slice(&[
484            0x00, 0x00, 0x06, // Length: 6
485            0x04, // Type: SETTINGS
486            0x00, // Flags: 0
487            0x00, 0x00, 0x00, 0x00, // Stream ID: 0
488            0x00, 0x02, 0x00, 0x00, 0x00, 0x01, // Setting: ENABLE_PUSH = 1
489        ]);
490
491        // Add HEADERS frame
492        data.extend_from_slice(&[
493            0x00, 0x00, 0x04, // Length: 4
494            0x01, // Type: HEADERS
495            0x00, // Flags: 0
496            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
497            0x00, 0x00, 0x00, 0x00, // Payload (4 bytes)
498        ]);
499
500        assert!(has_complete_data(&data)); // Should find the HEADERS frame
501    }
502
503    #[test]
504    fn test_response_frame_detection() {
505        // Test response without preface (just frames)
506        let response_data = [
507            0x00, 0x00, 0x04, // Length: 4
508            0x01, // Type: HEADERS
509            0x00, // Flags: 0
510            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
511            0x00, 0x00, 0x00, 0x00, // Payload (4 bytes)
512        ];
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        // Add frame with extremely large length (will cause overflow)
522        data.extend_from_slice(&[
523            0xFF, 0xFF, 0xFF, // Length: 16777215 (max 24-bit)
524            0x01, // Type: HEADERS
525            0x00, // Flags: 0
526            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
527        ]);
528
529        assert!(!has_complete_data(&data)); // Should reject oversized frame
530    }
531    #[test]
532    fn test_can_process_request_detection() {
533        let processor = Http2Processor::new();
534
535        // Valid HTTP/2 request with preface
536        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        // HTTP/1.1 request
541        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        // Too short data
545        let short_data = b"PRI * HTTP/2.0";
546        assert!(!processor.can_process_request(short_data));
547
548        // Invalid preface
549        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        // Valid HTTP/2 response frame
558        let valid_response = [
559            0x00, 0x00, 0x04, // Length: 4
560            0x01, // Type: HEADERS
561            0x00, // Flags: 0
562            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
563            0x00, 0x00, 0x00, 0x00, // Payload
564        ];
565        assert!(processor.can_process_response(&valid_response));
566
567        // HTTP/1.1 response
568        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        // Too short data
572        let short_data = b"HTTP/2";
573        assert!(!processor.can_process_response(short_data));
574
575        // Invalid frame type
576        let invalid_frame = [
577            0x00, 0x00, 0x04, // Length: 4
578            0xFF, // Type: Unknown (255)
579            0x00, // Flags: 0
580            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
581            0x00, 0x00, 0x00, 0x00, // Payload
582        ];
583        assert!(!processor.can_process_response(&invalid_frame));
584    }
585
586    #[test]
587    fn test_looks_like_http2_response_edge_cases() {
588        // Valid frame types (0-10)
589        for frame_type in 0..=10 {
590            let frame = [
591                0x00, 0x00, 0x04,       // Length: 4
592                frame_type, // Type: variable
593                0x00,       // Flags: 0
594                0x00, 0x00, 0x00, 0x01, // Stream ID: 1
595                0x00, 0x00, 0x00, 0x00, // Payload
596            ];
597            assert!(
598                looks_like_http2_response(&frame),
599                "Frame type {frame_type} should be valid"
600            );
601        }
602
603        // Invalid frame type (11+)
604        let invalid_frame = [
605            0x00, 0x00, 0x04, // Length: 4
606            11,   // Type: Invalid
607            0x00, // Flags: 0
608            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
609            0x00, 0x00, 0x00, 0x00, // Payload
610        ];
611        assert!(!looks_like_http2_response(&invalid_frame));
612
613        // Frame too large (exceeds default max frame size)
614        let large_frame = [
615            0x00, 0x40, 0x01, // Length: 16385 (> 16384)
616            0x01, // Type: HEADERS
617            0x00, // Flags: 0
618            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
619        ];
620        assert!(!looks_like_http2_response(&large_frame));
621
622        // Maximum valid frame size
623        let max_frame = [
624            0x00, 0x40, 0x00, // Length: 16384 (exactly max)
625            0x01, // Type: HEADERS
626            0x00, // Flags: 0
627            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
628        ];
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        // Test trait methods
637        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        // Invalid preface should return error
646        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        // Valid preface but no frames should return Ok(None)
651        let result = processor.process_request(HTTP2_CONNECTION_PREFACE);
652        match result {
653            Ok(None) => {} // Expected
654            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        // Empty data should return Ok(None)
664        let result = processor.process_response(&[]);
665        match result {
666            Ok(None) => {} // Expected
667            Ok(Some(_)) => panic!("Should return None for empty data"),
668            Err(e) => panic!("Should not error for empty data: {e:?}"),
669        }
670
671        // Invalid frame should return error or None
672        let invalid_frame = [0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00];
673        let result = processor.process_response(&invalid_frame);
674        // Should handle gracefully (either error or None)
675        match result {
676            Ok(None) => {} // Acceptable
677            Err(_) => {}   // Also acceptable
678            Ok(Some(_)) => panic!("Should not return valid response for invalid frame"),
679        }
680    }
681
682    #[test]
683    fn test_extract_traffic_classification() {
684        // Test with Some value
685        assert_eq!(extract_traffic_classification(Some("test")), "test");
686
687        // Test with None
688        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        // Empty data
696        assert!(!processor.has_complete_data(&[]));
697
698        // Only preface
699        assert!(!processor.has_complete_data(HTTP2_CONNECTION_PREFACE));
700
701        // Preface + incomplete frame header
702        let mut data = HTTP2_CONNECTION_PREFACE.to_vec();
703        data.extend_from_slice(&[0x00, 0x00, 0x04, 0x01]); // Only 4 bytes of 9-byte header
704        assert!(!processor.has_complete_data(&data));
705
706        // Response data (no preface) with valid frame
707        let response_frame = [
708            0x00, 0x00, 0x00, // Length: 0
709            0x01, // Type: HEADERS
710            0x00, // Flags: 0
711            0x00, 0x00, 0x00, 0x01, // Stream ID: 1
712        ];
713        assert!(processor.has_complete_data(&response_frame));
714    }
715
716    #[test]
717    fn test_conversion_functions_with_complex_data() {
718        // Test request conversion with all fields
719        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        // Test response conversion with all fields
770        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}