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