Skip to main content

packet_dissector_http/
lib.rs

1//! HTTP/1.1 dissector.
2//!
3//! Parses HTTP/1.1 request and response messages as defined in RFC 9112.
4//! Uses the [`httparse`] crate for robust, zero-copy start-line and header
5//! parsing, then handles Content-Length body framing on top.
6//!
7//! ## References
8//! - RFC 9112: HTTP/1.1 <https://www.rfc-editor.org/rfc/rfc9112>
9//! - RFC 9110: HTTP Semantics <https://www.rfc-editor.org/rfc/rfc9110>
10
11#![deny(missing_docs)]
12
13use packet_dissector_core::dissector::{DispatchHint, DissectResult, Dissector};
14use packet_dissector_core::error::PacketError;
15use packet_dissector_core::field::{FieldDescriptor, FieldType, FieldValue};
16use packet_dissector_core::packet::DissectBuffer;
17use packet_dissector_core::util::{intern_content_type, slice_offset, str_offset, trim_ows};
18
19/// Maximum number of HTTP headers to parse.
20const MAX_HEADERS: usize = 64;
21
22/// Minimum valid start-line length: "GET / HTTP/1.1\r\n" = 16 bytes
23const MIN_START_LINE_LEN: usize = 16;
24
25// ---------------------------------------------------------------------------
26// Field descriptors
27// ---------------------------------------------------------------------------
28
29/// Field descriptor indices for [`FIELD_DESCRIPTORS`].
30const FD_IS_RESPONSE: usize = 0;
31const FD_METHOD: usize = 1;
32const FD_URI: usize = 2;
33const FD_VERSION: usize = 3;
34const FD_STATUS_CODE: usize = 4;
35const FD_REASON_PHRASE: usize = 5;
36const FD_HEADERS: usize = 6;
37const FD_CONTENT_LENGTH: usize = 7;
38const FD_CONTENT_TYPE: usize = 8;
39
40/// Child descriptor indices for [`HEADER_CHILDREN`].
41const HC_NAME: usize = 0;
42const HC_VALUE: usize = 1;
43
44/// Child descriptors for each header entry object.
45static HEADER_CHILDREN: &[FieldDescriptor] = &[
46    FieldDescriptor::new("name", "Name", FieldType::Str),
47    FieldDescriptor::new("value", "Value", FieldType::Str),
48];
49
50/// All field descriptors for the HTTP dissector.
51static FIELD_DESCRIPTORS: &[FieldDescriptor] = &[
52    // RFC 9112, Section 2.1 — distinguishes request from response
53    FieldDescriptor::new("is_response", "Is Response", FieldType::U8),
54    // RFC 9112, Section 3 — request-line method token
55    FieldDescriptor::new("method", "Method", FieldType::Str).optional(),
56    // RFC 9112, Section 3 — request-target
57    FieldDescriptor::new("uri", "Request URI", FieldType::Str).optional(),
58    // RFC 9112, Section 2.3 — HTTP-version
59    FieldDescriptor::new("version", "Version", FieldType::Str),
60    // RFC 9112, Section 4 — status-code (3DIGIT)
61    FieldDescriptor::new("status_code", "Status Code", FieldType::U16).optional(),
62    // RFC 9112, Section 4 — reason-phrase
63    FieldDescriptor::new("reason_phrase", "Reason Phrase", FieldType::Str).optional(),
64    // RFC 9112, Section 5 — header fields
65    FieldDescriptor::new("headers", "Headers", FieldType::Array)
66        .optional()
67        .with_children(HEADER_CHILDREN),
68    // RFC 9112, Section 6.2 — Content-Length
69    FieldDescriptor::new("content_length", "Content Length", FieldType::U32).optional(),
70    // RFC 9110, Section 8.3 — Content-Type
71    // https://www.rfc-editor.org/rfc/rfc9110#section-8.3
72    FieldDescriptor::new("content_type", "Content Type", FieldType::Str).optional(),
73];
74
75/// HTTP/1.1 dissector.
76///
77/// Parses both request and response messages. The dissector detects whether the
78/// message is a request or response by checking if the start-line begins with
79/// `"HTTP/"` (response) or a method token (request).
80pub struct HttpDissector;
81
82impl Dissector for HttpDissector {
83    fn name(&self) -> &'static str {
84        "HyperText Transfer Protocol"
85    }
86
87    fn short_name(&self) -> &'static str {
88        "HTTP"
89    }
90
91    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
92        FIELD_DESCRIPTORS
93    }
94
95    fn dissect<'pkt>(
96        &self,
97        data: &'pkt [u8],
98        buf: &mut DissectBuffer<'pkt>,
99        offset: usize,
100    ) -> Result<DissectResult, PacketError> {
101        if data.len() < MIN_START_LINE_LEN {
102            return Err(PacketError::Truncated {
103                expected: MIN_START_LINE_LEN,
104                actual: data.len(),
105            });
106        }
107
108        // RFC 9112, Section 2.1 — detect request vs response
109        let is_response = data.starts_with(b"HTTP/");
110
111        buf.begin_layer("HTTP", None, FIELD_DESCRIPTORS, offset..offset);
112
113        buf.push_field(
114            &FIELD_DESCRIPTORS[FD_IS_RESPONSE],
115            FieldValue::U8(u8::from(is_response)),
116            offset..offset + 1,
117        );
118
119        let header_len = if is_response {
120            parse_response(data, offset, buf)?
121        } else {
122            parse_request(data, offset, buf)?
123        };
124
125        // Extract Content-Length and Content-Type from parsed headers
126        let content_length =
127            extract_header_value(buf, "Content-Length").and_then(|v| v.parse::<u32>().ok());
128        let content_type = extract_header_value(buf, "Content-Type");
129
130        if let Some(cl) = content_length {
131            buf.push_field(
132                &FIELD_DESCRIPTORS[FD_CONTENT_LENGTH],
133                FieldValue::U32(cl),
134                offset..offset + header_len,
135            );
136        }
137
138        if let Some(ct) = content_type {
139            buf.push_field(
140                &FIELD_DESCRIPTORS[FD_CONTENT_TYPE],
141                FieldValue::Str(ct),
142                offset..offset + header_len,
143            );
144        }
145
146        // Calculate total bytes consumed: headers + body
147        let body_len = content_length.unwrap_or(0) as usize;
148        let total = header_len + body_len;
149
150        if total > data.len() {
151            // End the layer before returning error
152            if let Some(layer) = buf.last_layer_mut() {
153                layer.range = offset..offset + header_len;
154            }
155            buf.end_layer();
156            return Err(PacketError::Truncated {
157                expected: total,
158                actual: data.len(),
159            });
160        }
161
162        // RFC 9110, Section 8.3 — dispatch body by Content-Type.
163        // https://www.rfc-editor.org/rfc/rfc9110#section-8.3
164        // When dispatching to a body dissector the registry advances offset by
165        // bytes_consumed before calling the next dissector, so we consume only
166        // the header section here and let the body dissector start at the body.
167        if body_len > 0 {
168            if let Some(ct) = extract_header_value(buf, "Content-Type") {
169                if let Some(interned) = intern_content_type(ct) {
170                    if let Some(layer) = buf.last_layer_mut() {
171                        layer.range = offset..offset + header_len;
172                    }
173                    buf.end_layer();
174                    return Ok(DissectResult::new(
175                        header_len,
176                        DispatchHint::ByContentType(interned),
177                    ));
178                }
179            }
180        }
181
182        if let Some(layer) = buf.last_layer_mut() {
183            layer.range = offset..offset + total;
184        }
185        buf.end_layer();
186
187        Ok(DissectResult::new(total, DispatchHint::End))
188    }
189}
190
191/// Convert httparse version number (0 = HTTP/1.0, 1 = HTTP/1.1) to string.
192fn version_str(v: u8) -> &'static str {
193    match v {
194        0 => "HTTP/1.0",
195        1 => "HTTP/1.1",
196        _ => "HTTP/1.x",
197    }
198}
199
200/// Parse an HTTP request using httparse, populating fields in the buffer.
201/// Returns the total header length (including final CRLF).
202fn parse_request<'pkt>(
203    data: &'pkt [u8],
204    offset: usize,
205    buf: &mut DissectBuffer<'pkt>,
206) -> Result<usize, PacketError> {
207    let mut headers_buf = [httparse::EMPTY_HEADER; MAX_HEADERS];
208    let mut req = httparse::Request::new(&mut headers_buf);
209
210    let header_len = match req.parse(data) {
211        Ok(httparse::Status::Complete(len)) => len,
212        Ok(httparse::Status::Partial) => {
213            return Err(PacketError::Truncated {
214                expected: data.len() + 1,
215                actual: data.len(),
216            });
217        }
218        Err(_) => {
219            return Err(PacketError::InvalidHeader("invalid HTTP request line"));
220        }
221    };
222
223    // RFC 9112, Section 3 — method
224    if let Some(method) = req.method {
225        let start = str_offset(data, method)?;
226        buf.push_field(
227            &FIELD_DESCRIPTORS[FD_METHOD],
228            FieldValue::Str(method),
229            offset + start..offset + start + method.len(),
230        );
231    }
232
233    // RFC 9112, Section 3 — request-target
234    if let Some(path) = req.path {
235        let start = str_offset(data, path)?;
236        buf.push_field(
237            &FIELD_DESCRIPTORS[FD_URI],
238            FieldValue::Str(path),
239            offset + start..offset + start + path.len(),
240        );
241    }
242
243    // RFC 9112, Section 2.3 — HTTP-version
244    if let Some(version) = req.version {
245        let vs = version_str(version);
246        buf.push_field(
247            &FIELD_DESCRIPTORS[FD_VERSION],
248            FieldValue::Str(vs),
249            offset..offset + header_len,
250        );
251    }
252
253    // RFC 9112, Section 5 — header fields
254    build_header_fields(data, offset, req.headers, buf)?;
255
256    Ok(header_len)
257}
258
259/// Parse an HTTP response using httparse, populating fields in the buffer.
260/// Returns the total header length (including final CRLF).
261fn parse_response<'pkt>(
262    data: &'pkt [u8],
263    offset: usize,
264    buf: &mut DissectBuffer<'pkt>,
265) -> Result<usize, PacketError> {
266    let mut headers_buf = [httparse::EMPTY_HEADER; MAX_HEADERS];
267    let mut resp = httparse::Response::new(&mut headers_buf);
268
269    let header_len = match resp.parse(data) {
270        Ok(httparse::Status::Complete(len)) => len,
271        Ok(httparse::Status::Partial) => {
272            return Err(PacketError::Truncated {
273                expected: data.len() + 1,
274                actual: data.len(),
275            });
276        }
277        Err(_) => {
278            return Err(PacketError::InvalidHeader("invalid HTTP status line"));
279        }
280    };
281
282    // RFC 9112, Section 2.3 — HTTP-version
283    if let Some(version) = resp.version {
284        let vs = version_str(version);
285        buf.push_field(
286            &FIELD_DESCRIPTORS[FD_VERSION],
287            FieldValue::Str(vs),
288            offset..offset + header_len,
289        );
290    }
291
292    // RFC 9112, Section 4 — status-code
293    if let Some(code) = resp.code {
294        buf.push_field(
295            &FIELD_DESCRIPTORS[FD_STATUS_CODE],
296            FieldValue::U16(code),
297            offset..offset + header_len,
298        );
299    }
300
301    // RFC 9112, Section 4 — reason-phrase
302    if let Some(reason) = resp.reason {
303        if !reason.is_empty() {
304            buf.push_field(
305                &FIELD_DESCRIPTORS[FD_REASON_PHRASE],
306                FieldValue::Str(reason),
307                offset..offset + header_len,
308            );
309        }
310    }
311
312    // RFC 9112, Section 5 — header fields
313    build_header_fields(data, offset, resp.headers, buf)?;
314
315    Ok(header_len)
316}
317
318/// Convert httparse headers into container fields in the buffer, with OWS trimming.
319fn build_header_fields<'pkt>(
320    data: &'pkt [u8],
321    offset: usize,
322    headers: &[httparse::Header<'pkt>],
323    buf: &mut DissectBuffer<'pkt>,
324) -> Result<(), PacketError> {
325    if headers.is_empty() {
326        return Ok(());
327    }
328
329    // Compute the overall headers range from first to last header
330    // SAFETY of indexing: `is_empty()` check above guarantees at least one header.
331    let first_header = &headers[0];
332    let last_header = &headers[headers.len() - 1];
333    let first_name_start = str_offset(data, first_header.name)? + offset;
334    let last_value_end = slice_offset(data, last_header.value)? + last_header.value.len() + offset;
335
336    let array_idx = buf.begin_container(
337        &FIELD_DESCRIPTORS[FD_HEADERS],
338        FieldValue::Array(0..0),
339        first_name_start..last_value_end,
340    );
341
342    for header in headers {
343        let name = header.name;
344        let trimmed_value = trim_ows(header.value);
345        let value_str = core::str::from_utf8(trimmed_value)
346            .map_err(|_| PacketError::InvalidHeader("header value is not valid UTF-8"))?;
347
348        // Compute byte range from the header name position in data
349        let name_start = str_offset(data, name)?;
350        let value_end = slice_offset(data, header.value)? + header.value.len();
351        let header_range = offset + name_start..offset + value_end;
352
353        let obj_idx = buf.begin_container(
354            &HEADER_CHILDREN[HC_NAME], // placeholder descriptor for the Object — we reuse HC_NAME
355            FieldValue::Object(0..0),
356            header_range.clone(),
357        );
358        buf.push_field(
359            &HEADER_CHILDREN[HC_NAME],
360            FieldValue::Str(name),
361            header_range.clone(),
362        );
363        buf.push_field(
364            &HEADER_CHILDREN[HC_VALUE],
365            FieldValue::Str(value_str),
366            header_range,
367        );
368        buf.end_container(obj_idx);
369    }
370
371    buf.end_container(array_idx);
372
373    Ok(())
374}
375
376/// Extract the value of a header by name from the buffer's fields during construction.
377///
378/// Performs case-insensitive matching on the header name.
379/// Returns a `&'pkt str` borrowed directly from the packet data.
380///
381/// This function scans the fields in the buffer from the current layer's start
382/// position, which works even before `end_layer()` has been called.
383fn extract_header_value<'pkt>(buf: &DissectBuffer<'pkt>, header_name: &str) -> Option<&'pkt str> {
384    // Find the "headers" array field in the current layer's fields
385    let layer = buf.layers().last()?;
386    let start = layer.field_range.start as usize;
387    let fields = &buf.fields()[start..];
388    let headers_field = fields.iter().find(|f| f.name() == "headers")?;
389    let array_range = match &headers_field.value {
390        FieldValue::Array(r) => r,
391        _ => return None,
392    };
393
394    // Each child in the array is an Object containing "name" and "value" fields
395    let children = buf.nested_fields(array_range);
396    for field in children {
397        if let FieldValue::Object(ref obj_range) = field.value {
398            let obj_fields = buf.nested_fields(obj_range);
399            let name_field = obj_fields.iter().find(|f| f.name() == "name")?;
400            let value_field = obj_fields.iter().find(|f| f.name() == "value")?;
401            if let FieldValue::Str(n) = &name_field.value {
402                if n.eq_ignore_ascii_case(header_name) {
403                    if let FieldValue::Str(v) = &value_field.value {
404                        return Some(v);
405                    }
406                }
407            }
408        }
409    }
410    None
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    // # RFC 9112 (HTTP/1.1) & RFC 9110 (HTTP Semantics) Coverage
418    //
419    // | RFC Section   | Description           | Test                                    |
420    // |---------------|-----------------------|-----------------------------------------|
421    // | 9112 2.1      | Message Format        | parse_http_request_basic                |
422    // | 9112 2.2      | Bare LF terminators   | parse_http_request_bare_lf              |
423    // | 9112 2.2      | Bare LF in response   | parse_http_response_bare_lf             |
424    // | 9112 3        | Request Line          | parse_http_request_basic                |
425    // | 9112 3        | Method token          | parse_http_post_request                 |
426    // | 9112 4        | Status Line           | parse_http_response_basic               |
427    // | 9112 4        | Reason Phrase          | parse_http_response_no_reason           |
428    // | 9112 4        | Invalid status-line   | parse_http_response_invalid_status      |
429    // | 9112 5        | Header Fields         | parse_http_request_with_headers         |
430    // | 9112 5        | Empty header name     | parse_http_empty_header_name            |
431    // | 9112 6.2      | Content-Length        | parse_http_request_with_body            |
432    // | 9110 8.3      | CT dispatch (request) | parse_http_post_content_type_dispatch   |
433    // | 9110 8.3      | CT dispatch (response)| parse_http_response_content_type_dispatch|
434    // | 9110 8.3      | CT param stripping    | parse_http_content_type_with_params     |
435    // | 9110 8.3      | CT case insensitive   | parse_http_content_type_case_insensitive|
436    // | -             | No CT body fallback   | parse_http_no_content_type_with_body    |
437    // | -             | No body w/ CT → End   | parse_http_no_body_with_content_type    |
438    // | -             | Truncated             | parse_http_truncated                    |
439    // | -             | Invalid header        | parse_http_invalid_request_line         |
440
441    fn dissect(data: &[u8]) -> Result<DissectBuffer<'_>, PacketError> {
442        let dissector = HttpDissector;
443        let mut buf = DissectBuffer::new();
444        dissector.dissect(data, &mut buf, 0)?;
445        Ok(buf)
446    }
447
448    fn dissect_err(data: &[u8]) -> PacketError {
449        let dissector = HttpDissector;
450        let mut buf = DissectBuffer::new();
451        dissector.dissect(data, &mut buf, 0).unwrap_err()
452    }
453
454    #[test]
455    fn parse_http_request_basic() {
456        let data = b"GET / HTTP/1.1\r\n\r\n";
457        let buf = dissect(data).unwrap();
458        let layer = buf.layer_by_name("HTTP").unwrap();
459
460        assert_eq!(
461            buf.field_by_name(layer, "is_response").unwrap().value,
462            FieldValue::U8(0)
463        );
464        assert_eq!(
465            buf.field_by_name(layer, "method").unwrap().value,
466            FieldValue::Str("GET")
467        );
468        assert_eq!(
469            buf.field_by_name(layer, "uri").unwrap().value,
470            FieldValue::Str("/")
471        );
472        assert_eq!(
473            buf.field_by_name(layer, "version").unwrap().value,
474            FieldValue::Str("HTTP/1.1")
475        );
476        assert!(buf.field_by_name(layer, "status_code").is_none());
477    }
478
479    #[test]
480    fn parse_http_post_request() {
481        let body = b"key=value";
482        let header = b"POST /submit HTTP/1.1\r\nContent-Length: 9\r\n\r\n";
483        let mut data = Vec::new();
484        data.extend_from_slice(header);
485        data.extend_from_slice(body);
486
487        let buf = dissect(&data).unwrap();
488        let layer = buf.layer_by_name("HTTP").unwrap();
489
490        assert_eq!(
491            buf.field_by_name(layer, "method").unwrap().value,
492            FieldValue::Str("POST")
493        );
494        assert_eq!(
495            buf.field_by_name(layer, "uri").unwrap().value,
496            FieldValue::Str("/submit")
497        );
498        assert_eq!(
499            buf.field_by_name(layer, "content_length").unwrap().value,
500            FieldValue::U32(9)
501        );
502        assert_eq!(layer.range, 0..data.len());
503    }
504
505    #[test]
506    fn parse_http_response_basic() {
507        let data = b"HTTP/1.1 200 OK\r\n\r\n";
508        let buf = dissect(data).unwrap();
509        let layer = buf.layer_by_name("HTTP").unwrap();
510
511        assert_eq!(
512            buf.field_by_name(layer, "is_response").unwrap().value,
513            FieldValue::U8(1)
514        );
515        assert_eq!(
516            buf.field_by_name(layer, "version").unwrap().value,
517            FieldValue::Str("HTTP/1.1")
518        );
519        assert_eq!(
520            buf.field_by_name(layer, "status_code").unwrap().value,
521            FieldValue::U16(200)
522        );
523        assert_eq!(
524            buf.field_by_name(layer, "reason_phrase").unwrap().value,
525            FieldValue::Str("OK")
526        );
527        assert!(buf.field_by_name(layer, "method").is_none());
528    }
529
530    #[test]
531    fn parse_http_response_no_reason() {
532        let data = b"HTTP/1.1 204\r\n\r\n";
533        let buf = dissect(data).unwrap();
534        let layer = buf.layer_by_name("HTTP").unwrap();
535
536        assert_eq!(
537            buf.field_by_name(layer, "status_code").unwrap().value,
538            FieldValue::U16(204)
539        );
540        assert!(buf.field_by_name(layer, "reason_phrase").is_none());
541    }
542
543    #[test]
544    fn parse_http_request_with_headers() {
545        let data = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\n\r\n";
546        let buf = dissect(data).unwrap();
547        let layer = buf.layer_by_name("HTTP").unwrap();
548
549        let headers_field = buf.field_by_name(layer, "headers").unwrap();
550        let array_range = match &headers_field.value {
551            FieldValue::Array(r) => r,
552            _ => panic!("expected Array"),
553        };
554
555        let children = buf.nested_fields(array_range);
556        // Find all Object entries
557        let objects: Vec<_> = children.iter().filter(|f| f.value.is_object()).collect();
558        assert_eq!(objects.len(), 2);
559
560        if let FieldValue::Object(ref r) = objects[0].value {
561            let obj_fields = buf.nested_fields(r);
562            assert_eq!(obj_fields[0].value, FieldValue::Str("Host"));
563            assert_eq!(obj_fields[1].value, FieldValue::Str("example.com"));
564        }
565
566        if let FieldValue::Object(ref r) = objects[1].value {
567            let obj_fields = buf.nested_fields(r);
568            assert_eq!(obj_fields[0].value, FieldValue::Str("Accept"));
569            assert_eq!(obj_fields[1].value, FieldValue::Str("text/html"));
570        }
571    }
572
573    #[test]
574    fn parse_http_request_with_body() {
575        let body = b"Hello, World!";
576        let header = b"POST /api HTTP/1.1\r\nContent-Length: 13\r\n\r\n";
577        let mut data = Vec::new();
578        data.extend_from_slice(header);
579        data.extend_from_slice(body);
580
581        let buf = dissect(&data).unwrap();
582        let layer = buf.layer_by_name("HTTP").unwrap();
583
584        assert_eq!(
585            buf.field_by_name(layer, "content_length").unwrap().value,
586            FieldValue::U32(13)
587        );
588        // Layer range should encompass headers + body
589        assert_eq!(layer.range, 0..data.len());
590    }
591
592    #[test]
593    fn parse_http_truncated() {
594        let data = b"GET /";
595        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
596    }
597
598    #[test]
599    fn parse_http_truncated_headers() {
600        // Start-line complete but headers not terminated
601        let data = b"GET / HTTP/1.1\r\nHost: example.com";
602        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
603    }
604
605    #[test]
606    fn parse_http_truncated_body() {
607        // Headers indicate 100 bytes body but only 5 present
608        let data = b"POST / HTTP/1.1\r\nContent-Length: 100\r\n\r\nHello";
609        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
610    }
611
612    #[test]
613    fn parse_http_invalid_request_line() {
614        // Missing SP between method and URI (must be >= MIN_START_LINE_LEN bytes)
615        let data = b"INVALIDREQUESTLINE\r\n\r\n";
616        assert!(matches!(dissect_err(data), PacketError::InvalidHeader(_)));
617    }
618
619    #[test]
620    fn parse_http_header_ows_trimming() {
621        // RFC 9112, Section 5.1 — OWS around field-value
622        let data = b"GET / HTTP/1.1\r\nHost:   example.com  \r\n\r\n";
623        let buf = dissect(data).unwrap();
624        let layer = buf.layer_by_name("HTTP").unwrap();
625
626        let headers_field = buf.field_by_name(layer, "headers").unwrap();
627        if let FieldValue::Array(ref r) = headers_field.value {
628            let children = buf.nested_fields(r);
629            let obj = children.iter().find(|f| f.value.is_object()).unwrap();
630            if let FieldValue::Object(ref obj_r) = obj.value {
631                let obj_fields = buf.nested_fields(obj_r);
632                assert_eq!(obj_fields[1].value, FieldValue::Str("example.com"));
633            }
634        }
635    }
636
637    #[test]
638    fn parse_http_response_with_body() {
639        let body = b"<html></html>";
640        let header = b"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\n";
641        let mut data = Vec::new();
642        data.extend_from_slice(header);
643        data.extend_from_slice(body);
644
645        let buf = dissect(&data).unwrap();
646        let layer = buf.layer_by_name("HTTP").unwrap();
647
648        assert_eq!(
649            buf.field_by_name(layer, "status_code").unwrap().value,
650            FieldValue::U16(200)
651        );
652        assert_eq!(
653            buf.field_by_name(layer, "content_length").unwrap().value,
654            FieldValue::U32(13)
655        );
656        assert_eq!(layer.range, 0..data.len());
657    }
658
659    #[test]
660    fn parse_http_with_offset() {
661        let data = b"GET / HTTP/1.1\r\n\r\n";
662        let dissector = HttpDissector;
663        let mut buf = DissectBuffer::new();
664        let result = dissector.dissect(data, &mut buf, 42).unwrap();
665
666        let layer = buf.layer_by_name("HTTP").unwrap();
667        assert_eq!(layer.range.start, 42);
668        assert_eq!(layer.range.end, 42 + data.len());
669        assert_eq!(result.bytes_consumed, data.len());
670        assert_eq!(result.next, DispatchHint::End);
671    }
672
673    #[test]
674    fn parse_http_request_bare_lf() {
675        // RFC 9112, Section 2.2 — recipient MAY recognize bare LF as line terminator
676        let data = b"GET / HTTP/1.1\nHost: example.com\n\n";
677        let buf = dissect(data).unwrap();
678        let layer = buf.layer_by_name("HTTP").unwrap();
679
680        assert_eq!(
681            buf.field_by_name(layer, "method").unwrap().value,
682            FieldValue::Str("GET")
683        );
684        let headers_field = buf.field_by_name(layer, "headers").unwrap();
685        if let FieldValue::Array(ref r) = headers_field.value {
686            let children = buf.nested_fields(r);
687            let obj = children.iter().find(|f| f.value.is_object()).unwrap();
688            if let FieldValue::Object(ref obj_r) = obj.value {
689                let obj_fields = buf.nested_fields(obj_r);
690                assert_eq!(obj_fields[1].value, FieldValue::Str("example.com"));
691            }
692        }
693    }
694
695    #[test]
696    fn parse_http_response_bare_lf() {
697        // RFC 9112, Section 2.2 — bare LF in response
698        let data = b"HTTP/1.1 200 OK\nContent-Length: 2\n\nhi";
699        let buf = dissect(data).unwrap();
700        let layer = buf.layer_by_name("HTTP").unwrap();
701
702        assert_eq!(
703            buf.field_by_name(layer, "status_code").unwrap().value,
704            FieldValue::U16(200)
705        );
706        assert_eq!(
707            buf.field_by_name(layer, "content_length").unwrap().value,
708            FieldValue::U32(2)
709        );
710    }
711
712    #[test]
713    fn parse_http_response_invalid_status() {
714        // "200OK" without SP after status-code should be rejected
715        let data = b"HTTP/1.1 200OK\r\n\r\n";
716        assert!(matches!(dissect_err(data), PacketError::InvalidHeader(_)));
717    }
718
719    #[test]
720    fn parse_http_empty_header_name() {
721        // Empty header field name (colon at position 0) should be rejected per RFC 9112
722        let data = b"GET / HTTP/1.1\r\n: value\r\n\r\n";
723        assert!(matches!(dissect_err(data), PacketError::InvalidHeader(_)));
724    }
725
726    #[test]
727    fn parse_http_post_content_type_dispatch() {
728        let body = b"{\"key\":\"value\"}";
729        let header =
730            b"POST /api HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n";
731        let mut data = Vec::new();
732        data.extend_from_slice(header);
733        data.extend_from_slice(body);
734
735        let dissector = HttpDissector;
736        let mut buf = DissectBuffer::new();
737        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
738
739        assert_eq!(result.next, DispatchHint::ByContentType("application/json"));
740        assert_eq!(result.bytes_consumed, header.len());
741
742        let layer = buf.layer_by_name("HTTP").unwrap();
743        assert_eq!(layer.range, 0..header.len());
744        assert_eq!(
745            buf.field_by_name(layer, "content_type").unwrap().value,
746            FieldValue::Str("application/json")
747        );
748    }
749
750    #[test]
751    fn parse_http_response_content_type_dispatch() {
752        let body = b"<html></html>";
753        let header = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 13\r\n\r\n";
754        let mut data = Vec::new();
755        data.extend_from_slice(header);
756        data.extend_from_slice(body);
757
758        let dissector = HttpDissector;
759        let mut buf = DissectBuffer::new();
760        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
761
762        assert_eq!(result.next, DispatchHint::ByContentType("text/html"));
763        assert_eq!(result.bytes_consumed, header.len());
764
765        let layer = buf.layer_by_name("HTTP").unwrap();
766        assert_eq!(layer.range, 0..header.len());
767    }
768
769    #[test]
770    fn parse_http_content_type_with_params() {
771        let body = b"{\"a\":1}";
772        let header = b"POST /api HTTP/1.1\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 7\r\n\r\n";
773        let mut data = Vec::new();
774        data.extend_from_slice(header);
775        data.extend_from_slice(body);
776
777        let dissector = HttpDissector;
778        let mut buf = DissectBuffer::new();
779        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
780
781        // Dispatch MIME has parameters stripped
782        assert_eq!(result.next, DispatchHint::ByContentType("application/json"));
783
784        // Field stores the raw value including parameters
785        let layer = buf.layer_by_name("HTTP").unwrap();
786        assert_eq!(
787            buf.field_by_name(layer, "content_type").unwrap().value,
788            FieldValue::Str("application/json; charset=utf-8")
789        );
790    }
791
792    #[test]
793    fn parse_http_content_type_case_insensitive() {
794        let body = b"{\"a\":1}";
795        let header =
796            b"POST /api HTTP/1.1\r\nContent-Type: Application/JSON\r\nContent-Length: 7\r\n\r\n";
797        let mut data = Vec::new();
798        data.extend_from_slice(header);
799        data.extend_from_slice(body);
800
801        let dissector = HttpDissector;
802        let mut buf = DissectBuffer::new();
803        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
804
805        // Dispatch MIME is interned to lowercase
806        assert_eq!(result.next, DispatchHint::ByContentType("application/json"));
807    }
808
809    #[test]
810    fn parse_http_no_content_type_with_body() {
811        let body = b"key=value";
812        let header = b"POST /submit HTTP/1.1\r\nContent-Length: 9\r\n\r\n";
813        let mut data = Vec::new();
814        data.extend_from_slice(header);
815        data.extend_from_slice(body);
816
817        let dissector = HttpDissector;
818        let mut buf = DissectBuffer::new();
819        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
820
821        assert_eq!(result.next, DispatchHint::End);
822        assert_eq!(result.bytes_consumed, data.len());
823
824        let layer = buf.layer_by_name("HTTP").unwrap();
825        assert_eq!(layer.range, 0..data.len());
826        assert!(buf.field_by_name(layer, "content_type").is_none());
827    }
828
829    #[test]
830    fn parse_http_no_body_with_content_type() {
831        let data = b"GET / HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n";
832
833        let dissector = HttpDissector;
834        let mut buf = DissectBuffer::new();
835        let result = dissector.dissect(data, &mut buf, 0).unwrap();
836
837        assert_eq!(result.next, DispatchHint::End);
838
839        let layer = buf.layer_by_name("HTTP").unwrap();
840        assert_eq!(
841            buf.field_by_name(layer, "content_type").unwrap().value,
842            FieldValue::Str("text/plain")
843        );
844    }
845
846    #[test]
847    fn dissector_metadata() {
848        let d = HttpDissector;
849        assert_eq!(d.name(), "HyperText Transfer Protocol");
850        assert_eq!(d.short_name(), "HTTP");
851        assert!(!d.field_descriptors().is_empty());
852    }
853}