Skip to main content

stackforge_core/layer/http/
mod.rs

1//! HTTP/1.x layer implementation.
2//!
3//! This module provides:
4//!
5//! - **Detection** ([`detection`]) — fast byte-slice heuristics.
6//! - **Request parsing** ([`request`]) — zero-copy borrowed [`HttpRequest`].
7//! - **Response parsing** ([`response`]) — zero-copy borrowed [`HttpResponse`].
8//! - **Builders** ([`builder`]) — fluent owned builders for crafting raw bytes.
9//! - **[`HttpLayer`]** — a [`Layer`] implementation that presents a lazy,
10//!   index-based view into a packet buffer following the Stackforge
11//!   "Lazy Zero-Copy View" architecture.
12//!
13//! # Architecture
14//!
15//! [`HttpLayer`] holds only a [`LayerIndex`] (start / end byte offsets).  All
16//! field values are read on demand, directly from the caller-supplied buffer.
17//! No intermediate allocation is made when the layer is constructed.
18//!
19//! # Example
20//!
21//! ```rust
22//! use stackforge_core::layer::http::{HttpLayer, HttpRequestBuilder};
23//! use stackforge_core::layer::{LayerIndex, LayerKind};
24//!
25//! let raw = HttpRequestBuilder::new()
26//!     .method("GET")
27//!     .uri("/api/v1/status")
28//!     .header("Host", "example.com")
29//!     .build();
30//!
31//! let index = LayerIndex::new(LayerKind::Http, 0, raw.len());
32//! let layer = HttpLayer { index };
33//!
34//! assert_eq!(layer.method(&raw), Some("GET"));
35//! assert_eq!(layer.uri(&raw), Some("/api/v1/status"));
36//! ```
37
38pub mod builder;
39pub mod detection;
40pub mod request;
41pub mod response;
42
43pub use builder::{HttpRequestBuilder, HttpResponseBuilder};
44pub use request::HttpRequest;
45pub use response::HttpResponse;
46
47use crate::layer::field::{FieldError, FieldValue};
48use crate::layer::{Layer, LayerIndex, LayerKind};
49
50// ---------------------------------------------------------------------------
51// Field name registry
52// ---------------------------------------------------------------------------
53
54/// Canonical field names exposed by [`HttpLayer`].
55pub const HTTP_FIELD_NAMES: &[&str] = &[
56    "method",
57    "uri",
58    "version",
59    "status_code",
60    "reason",
61    "headers",
62    "body",
63];
64
65// ---------------------------------------------------------------------------
66// HttpLayer
67// ---------------------------------------------------------------------------
68
69/// Lazy, index-based view of an HTTP/1.x message inside a packet buffer.
70///
71/// The same type represents **either** a request **or** a response; use
72/// [`HttpLayer::is_request`] / [`HttpLayer::is_response`] to disambiguate.
73#[derive(Debug, Clone)]
74pub struct HttpLayer {
75    /// Start / end byte offsets of this HTTP message within the enclosing
76    /// packet buffer.
77    pub index: LayerIndex,
78}
79
80impl HttpLayer {
81    /// Create a new `HttpLayer` from a [`LayerIndex`].
82    #[must_use]
83    pub fn new(index: LayerIndex) -> Self {
84        Self { index }
85    }
86
87    // -----------------------------------------------------------------------
88    // Internal helpers
89    // -----------------------------------------------------------------------
90
91    /// Return the slice of `buf` that belongs to this layer.
92    #[inline]
93    fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
94        self.index.slice(buf)
95    }
96
97    // -----------------------------------------------------------------------
98    // Detection
99    // -----------------------------------------------------------------------
100
101    /// Returns `true` when the layer data looks like an HTTP request.
102    #[must_use]
103    pub fn is_request(&self, buf: &[u8]) -> bool {
104        detection::is_http_request(self.slice(buf))
105    }
106
107    /// Returns `true` when the layer data looks like an HTTP response.
108    #[must_use]
109    pub fn is_response(&self, buf: &[u8]) -> bool {
110        detection::is_http_response(self.slice(buf))
111    }
112
113    // -----------------------------------------------------------------------
114    // Raw line / header helpers
115    // -----------------------------------------------------------------------
116
117    /// Return the first CRLF-terminated line of the HTTP message.
118    #[must_use]
119    pub fn first_line<'a>(&self, buf: &'a [u8]) -> Option<&'a [u8]> {
120        let slice = self.slice(buf);
121        let crlf_pos = slice.windows(2).position(|w| w == b"\r\n")?;
122        Some(&slice[..crlf_pos])
123    }
124
125    /// Find the offset (relative to `start`) of the first byte **after** the
126    /// `\r\n\r\n` header terminator.  Returns `None` when the terminator is
127    /// not found in `buf[start..]`.
128    #[must_use]
129    pub fn headers_end(buf: &[u8], start: usize) -> Option<usize> {
130        let search_area = buf.get(start..)?;
131        search_area
132            .windows(4)
133            .position(|w| w == b"\r\n\r\n")
134            .map(|rel| start + rel + 4)
135    }
136
137    // -----------------------------------------------------------------------
138    // Request field accessors
139    // -----------------------------------------------------------------------
140
141    /// Return the HTTP method for a request (e.g. `"GET"`, `"POST"`).
142    ///
143    /// Returns `None` for responses or malformed data.
144    #[must_use]
145    pub fn method<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
146        if !self.is_request(buf) {
147            return None;
148        }
149        let line = std::str::from_utf8(self.first_line(buf)?).ok()?;
150        line.split(' ').next()
151    }
152
153    /// Return the request-URI for a request.
154    ///
155    /// Returns `None` for responses or malformed data.
156    #[must_use]
157    pub fn uri<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
158        if !self.is_request(buf) {
159            return None;
160        }
161        let slice = self.slice(buf);
162        let text = std::str::from_utf8(slice).ok()?;
163        let line_end = text.find("\r\n")?;
164        let request_line = &text[..line_end];
165        let mut parts = request_line.splitn(3, ' ');
166        parts.next(); // method
167        parts.next() // uri
168    }
169
170    /// Return the HTTP version string (e.g. `"HTTP/1.1"`) for either a
171    /// request or a response.
172    #[must_use]
173    pub fn http_version<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
174        let slice = self.slice(buf);
175        let text = std::str::from_utf8(slice).ok()?;
176        let line_end = text.find("\r\n")?;
177        let first_line = &text[..line_end];
178
179        if self.is_response(buf) {
180            // "HTTP/x.y STATUS REASON" — version is the first token.
181            first_line.split(' ').next()
182        } else {
183            // "METHOD URI HTTP/x.y" — version is the last token.
184            first_line.rsplit(' ').next()
185        }
186    }
187
188    // -----------------------------------------------------------------------
189    // Response field accessors
190    // -----------------------------------------------------------------------
191
192    /// Return the numeric status code for a response (e.g. `200`, `404`).
193    ///
194    /// Returns `None` for requests or malformed data.
195    #[must_use]
196    pub fn status_code(&self, buf: &[u8]) -> Option<u16> {
197        if !self.is_response(buf) {
198            return None;
199        }
200        let slice = self.slice(buf);
201        let text = std::str::from_utf8(slice).ok()?;
202        let line_end = text.find("\r\n")?;
203        let status_line = &text[..line_end];
204        let mut parts = status_line.splitn(3, ' ');
205        parts.next(); // version
206        parts.next()?.parse().ok()
207    }
208
209    /// Return the reason phrase for a response (e.g. `"OK"`, `"Not Found"`).
210    ///
211    /// Returns `None` for requests or malformed data.
212    #[must_use]
213    pub fn reason<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
214        if !self.is_response(buf) {
215            return None;
216        }
217        let slice = self.slice(buf);
218        let text = std::str::from_utf8(slice).ok()?;
219        let line_end = text.find("\r\n")?;
220        let status_line = &text[..line_end];
221        let mut parts = status_line.splitn(3, ' ');
222        parts.next(); // version
223        parts.next(); // status code
224        parts.next()
225    }
226
227    // -----------------------------------------------------------------------
228    // Generic header / body helpers
229    // -----------------------------------------------------------------------
230
231    /// Perform a **case-insensitive** lookup for a header by name.
232    ///
233    /// Returns the trimmed header value if found.
234    #[must_use]
235    pub fn header_value<'a>(&self, buf: &'a [u8], name: &str) -> Option<&'a str> {
236        let slice = self.slice(buf);
237        let text = std::str::from_utf8(slice).ok()?;
238
239        // Skip the first line (request/status line).
240        let first_crlf = text.find("\r\n")?;
241        let after_first = first_crlf + 2;
242        // Find \r\n\r\n starting from the end of the first line so we don't
243        // accidentally match the first line's own \r\n as part of the sequence.
244        let search_start = first_crlf; // back-up 2 to catch zero-header case
245        let headers_end = text[search_start..]
246            .find("\r\n\r\n")
247            .map(|rel| search_start + rel)?;
248
249        if after_first > headers_end {
250            return None; // no headers, and no match for the requested name
251        }
252        let header_section = &text[after_first..headers_end];
253        for line in header_section.split("\r\n") {
254            if let Some(colon) = line.find(':') {
255                let hdr_name = &line[..colon];
256                if hdr_name.eq_ignore_ascii_case(name) {
257                    return Some(line[colon + 1..].trim());
258                }
259            }
260        }
261        None
262    }
263
264    /// Return the absolute byte offset (in `buf`) where the body starts, i.e.
265    /// the byte immediately after `\r\n\r\n`.
266    ///
267    /// Returns `None` when the header terminator is not present.
268    #[must_use]
269    pub fn body_offset(&self, buf: &[u8]) -> Option<usize> {
270        Self::headers_end(buf, self.index.start)
271    }
272
273    /// Parse the `Content-Length` header and return it as a `usize`.
274    ///
275    /// Returns `None` when the header is absent or cannot be parsed.
276    #[must_use]
277    pub fn content_length(&self, buf: &[u8]) -> Option<usize> {
278        self.header_value(buf, "content-length")
279            .and_then(|v| v.parse().ok())
280    }
281
282    /// Return `true` when `Transfer-Encoding: chunked` is present.
283    #[must_use]
284    pub fn is_chunked(&self, buf: &[u8]) -> bool {
285        self.header_value(buf, "transfer-encoding")
286            .is_some_and(|v| v.eq_ignore_ascii_case("chunked"))
287    }
288
289    // -----------------------------------------------------------------------
290    // Summary / header length
291    // -----------------------------------------------------------------------
292
293    /// Generate a human-readable one-line summary:
294    /// - Request : `"HTTP GET /path HTTP/1.1"`
295    /// - Response: `"HTTP 200 OK"`
296    #[must_use]
297    pub fn summary_str(&self, buf: &[u8]) -> String {
298        if self.is_request(buf) {
299            let method = self.method(buf).unwrap_or("?");
300            let uri = self.uri(buf).unwrap_or("?");
301            let version = self.http_version(buf).unwrap_or("HTTP/?");
302            format!("HTTP {method} {uri} {version}")
303        } else if self.is_response(buf) {
304            let code = self.status_code(buf).unwrap_or(0);
305            let reason = self.reason(buf).unwrap_or("?");
306            format!("HTTP {code} {reason}")
307        } else {
308            "HTTP".to_owned()
309        }
310    }
311
312    /// Return the byte length of the HTTP headers section, including the
313    /// terminal `\r\n\r\n`, but **excluding** the body.
314    ///
315    /// Returns `0` when the header block cannot be located.
316    #[must_use]
317    pub fn http_header_len(&self, buf: &[u8]) -> usize {
318        match self.body_offset(buf) {
319            Some(body_start) => body_start.saturating_sub(self.index.start),
320            None => 0,
321        }
322    }
323
324    // -----------------------------------------------------------------------
325    // Field name / value API
326    // -----------------------------------------------------------------------
327
328    /// Return the static list of field names for this layer.
329    #[must_use]
330    pub fn field_names(&self) -> &'static [&'static str] {
331        HTTP_FIELD_NAMES
332    }
333
334    /// Return a field value by name.
335    ///
336    /// | Field name    | Type                       | Notes              |
337    /// |---------------|----------------------------|--------------------|
338    /// | `method`      | `FieldValue::Bytes`        | requests only      |
339    /// | `uri`         | `FieldValue::Bytes`        | requests only      |
340    /// | `version`     | `FieldValue::Bytes`        | requests + responses |
341    /// | `status_code` | `FieldValue::U16`          | responses only     |
342    /// | `reason`      | `FieldValue::Bytes`        | responses only     |
343    ///
344    /// Returns `None` for unrecognised field names or when the field does not
345    /// apply to the current message type.
346    #[must_use]
347    pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
348        match name {
349            "method" => {
350                let v = self.method(buf)?;
351                Some(Ok(FieldValue::Bytes(v.as_bytes().to_vec())))
352            },
353            "uri" => {
354                let v = self.uri(buf)?;
355                Some(Ok(FieldValue::Bytes(v.as_bytes().to_vec())))
356            },
357            "version" => {
358                let v = self.http_version(buf)?;
359                Some(Ok(FieldValue::Bytes(v.as_bytes().to_vec())))
360            },
361            "status_code" => {
362                let v = self.status_code(buf)?;
363                Some(Ok(FieldValue::U16(v)))
364            },
365            "reason" => {
366                let v = self.reason(buf)?;
367                Some(Ok(FieldValue::Bytes(v.as_bytes().to_vec())))
368            },
369            _ => None,
370        }
371    }
372}
373
374// ---------------------------------------------------------------------------
375// Layer trait implementation
376// ---------------------------------------------------------------------------
377
378impl Layer for HttpLayer {
379    fn kind(&self) -> LayerKind {
380        LayerKind::Http
381    }
382
383    fn summary(&self, data: &[u8]) -> String {
384        self.summary_str(data)
385    }
386
387    /// The header length for an HTTP layer is the number of bytes from the
388    /// beginning of the message up to (and including) the `\r\n\r\n`
389    /// header terminator.  The body bytes are NOT included.
390    fn header_len(&self, data: &[u8]) -> usize {
391        self.http_header_len(data)
392    }
393
394    fn hashret(&self, _data: &[u8]) -> Vec<u8> {
395        // HTTP is stateless at this level; no meaningful hash.
396        vec![]
397    }
398
399    fn answers(&self, _data: &[u8], _other: &Self, _other_data: &[u8]) -> bool {
400        false
401    }
402
403    fn extract_padding<'a>(&self, data: &'a [u8]) -> (&'a [u8], &'a [u8]) {
404        // HTTP determines its own length via Content-Length or chunked
405        // encoding.  We expose the full slice as "data" with no separate
406        // padding region.
407        let slice = self.index.slice(data);
408        (slice, &[])
409    }
410
411    fn field_names(&self) -> &'static [&'static str] {
412        HTTP_FIELD_NAMES
413    }
414}
415
416// ---------------------------------------------------------------------------
417// Unit tests
418// ---------------------------------------------------------------------------
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    fn make_layer(buf: &[u8]) -> HttpLayer {
425        HttpLayer {
426            index: LayerIndex::new(LayerKind::Http, 0, buf.len()),
427        }
428    }
429
430    // --- Request tests ------------------------------------------------------
431
432    #[test]
433    fn test_request_is_request() {
434        let raw = b"GET / HTTP/1.1\r\nHost: h\r\n\r\n";
435        let layer = make_layer(raw);
436        assert!(layer.is_request(raw));
437        assert!(!layer.is_response(raw));
438    }
439
440    #[test]
441    fn test_request_method() {
442        let raw = b"POST /upload HTTP/1.1\r\n\r\n";
443        let layer = make_layer(raw);
444        assert_eq!(layer.method(raw), Some("POST"));
445    }
446
447    #[test]
448    fn test_request_uri() {
449        let raw = b"GET /path/to/resource HTTP/1.1\r\n\r\n";
450        let layer = make_layer(raw);
451        assert_eq!(layer.uri(raw), Some("/path/to/resource"));
452    }
453
454    #[test]
455    fn test_request_version() {
456        let raw = b"GET / HTTP/1.0\r\n\r\n";
457        let layer = make_layer(raw);
458        assert_eq!(layer.http_version(raw), Some("HTTP/1.0"));
459    }
460
461    #[test]
462    fn test_request_header_value() {
463        let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n";
464        let layer = make_layer(raw);
465        assert_eq!(layer.header_value(raw, "host"), Some("example.com"));
466        assert_eq!(layer.header_value(raw, "Host"), Some("example.com"));
467        assert_eq!(layer.header_value(raw, "Accept"), Some("*/*"));
468        assert_eq!(layer.header_value(raw, "X-Missing"), None);
469    }
470
471    #[test]
472    fn test_request_body_offset() {
473        let raw = b"GET / HTTP/1.1\r\nHost: h\r\n\r\nbody data";
474        let layer = make_layer(raw);
475        let offset = layer.body_offset(raw).unwrap();
476        assert_eq!(&raw[offset..], b"body data");
477    }
478
479    #[test]
480    fn test_request_content_length() {
481        let raw = b"POST / HTTP/1.1\r\nContent-Length: 42\r\n\r\n";
482        let layer = make_layer(raw);
483        assert_eq!(layer.content_length(raw), Some(42));
484    }
485
486    #[test]
487    fn test_request_is_chunked() {
488        let raw = b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n";
489        let layer = make_layer(raw);
490        assert!(layer.is_chunked(raw));
491    }
492
493    #[test]
494    fn test_request_not_chunked() {
495        let raw = b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n";
496        let layer = make_layer(raw);
497        assert!(!layer.is_chunked(raw));
498    }
499
500    #[test]
501    fn test_request_summary() {
502        let raw = b"DELETE /res HTTP/1.1\r\n\r\n";
503        let layer = make_layer(raw);
504        assert_eq!(layer.summary_str(raw), "HTTP DELETE /res HTTP/1.1");
505    }
506
507    #[test]
508    fn test_request_header_len() {
509        let headers = b"GET / HTTP/1.1\r\nHost: h\r\n\r\n";
510        let body = b"BODY";
511        let mut raw = headers.to_vec();
512        raw.extend_from_slice(body);
513        let layer = make_layer(&raw);
514        // header_len should NOT include the body.
515        assert_eq!(layer.http_header_len(&raw), headers.len());
516    }
517
518    #[test]
519    fn test_request_get_field_method() {
520        let raw = b"PUT /x HTTP/1.1\r\n\r\n";
521        let layer = make_layer(raw);
522        if let Some(Ok(FieldValue::Bytes(v))) = layer.get_field(raw, "method") {
523            assert_eq!(v, b"PUT");
524        } else {
525            panic!("expected Bytes value for method");
526        }
527    }
528
529    #[test]
530    fn test_request_get_field_uri() {
531        let raw = b"GET /hello HTTP/1.1\r\n\r\n";
532        let layer = make_layer(raw);
533        if let Some(Ok(FieldValue::Bytes(v))) = layer.get_field(raw, "uri") {
534            assert_eq!(v, b"/hello");
535        } else {
536            panic!("expected Bytes value for uri");
537        }
538    }
539
540    #[test]
541    fn test_request_get_field_version() {
542        let raw = b"GET / HTTP/1.0\r\n\r\n";
543        let layer = make_layer(raw);
544        if let Some(Ok(FieldValue::Bytes(v))) = layer.get_field(raw, "version") {
545            assert_eq!(v, b"HTTP/1.0");
546        } else {
547            panic!("expected Bytes value for version");
548        }
549    }
550
551    // --- Response tests -----------------------------------------------------
552
553    #[test]
554    fn test_response_is_response() {
555        let raw = b"HTTP/1.1 200 OK\r\n\r\n";
556        let layer = make_layer(raw);
557        assert!(layer.is_response(raw));
558        assert!(!layer.is_request(raw));
559    }
560
561    #[test]
562    fn test_response_status_code() {
563        let raw = b"HTTP/1.1 404 Not Found\r\n\r\n";
564        let layer = make_layer(raw);
565        assert_eq!(layer.status_code(raw), Some(404));
566    }
567
568    #[test]
569    fn test_response_reason() {
570        let raw = b"HTTP/1.1 301 Moved Permanently\r\n\r\n";
571        let layer = make_layer(raw);
572        assert_eq!(layer.reason(raw), Some("Moved Permanently"));
573    }
574
575    #[test]
576    fn test_response_version() {
577        let raw = b"HTTP/1.0 200 OK\r\n\r\n";
578        let layer = make_layer(raw);
579        assert_eq!(layer.http_version(raw), Some("HTTP/1.0"));
580    }
581
582    #[test]
583    fn test_response_header_value() {
584        let raw = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
585        let layer = make_layer(raw);
586        assert_eq!(
587            layer.header_value(raw, "content-type"),
588            Some("application/json")
589        );
590    }
591
592    #[test]
593    fn test_response_body_offset() {
594        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest";
595        let layer = make_layer(raw);
596        let offset = layer.body_offset(raw).unwrap();
597        assert_eq!(&raw[offset..], b"test");
598    }
599
600    #[test]
601    fn test_response_summary() {
602        let raw = b"HTTP/1.1 500 Internal Server Error\r\n\r\n";
603        let layer = make_layer(raw);
604        assert_eq!(layer.summary_str(raw), "HTTP 500 Internal Server Error");
605    }
606
607    #[test]
608    fn test_response_get_field_status_code() {
609        let raw = b"HTTP/1.1 200 OK\r\n\r\n";
610        let layer = make_layer(raw);
611        if let Some(Ok(FieldValue::U16(code))) = layer.get_field(raw, "status_code") {
612            assert_eq!(code, 200);
613        } else {
614            panic!("expected U16 for status_code");
615        }
616    }
617
618    #[test]
619    fn test_response_get_field_reason() {
620        let raw = b"HTTP/1.1 200 OK\r\n\r\n";
621        let layer = make_layer(raw);
622        if let Some(Ok(FieldValue::Bytes(v))) = layer.get_field(raw, "reason") {
623            assert_eq!(v, b"OK");
624        } else {
625            panic!("expected Bytes for reason");
626        }
627    }
628
629    // --- Layer trait tests --------------------------------------------------
630
631    #[test]
632    fn test_layer_kind() {
633        let raw = b"GET / HTTP/1.1\r\n\r\n";
634        let layer = make_layer(raw);
635        assert_eq!(layer.kind(), LayerKind::Http);
636    }
637
638    #[test]
639    fn test_layer_field_names() {
640        let raw = b"GET / HTTP/1.1\r\n\r\n";
641        let layer = make_layer(raw);
642        assert_eq!(layer.field_names(), HTTP_FIELD_NAMES);
643    }
644
645    // --- Cross-module builder + layer round-trip test ----------------------
646
647    #[test]
648    fn test_builder_and_layer_roundtrip_request() {
649        let raw = HttpRequestBuilder::new()
650            .method("PATCH")
651            .uri("/resource/1")
652            .header("Authorization", "Bearer token123")
653            .header("Content-Type", "application/json")
654            .body(b"{\"key\":\"val\"}".to_vec())
655            .build();
656
657        let layer = make_layer(&raw);
658        assert_eq!(layer.method(&raw), Some("PATCH"));
659        assert_eq!(layer.uri(&raw), Some("/resource/1"));
660        assert_eq!(layer.http_version(&raw), Some("HTTP/1.1"));
661        assert_eq!(
662            layer.header_value(&raw, "authorization"),
663            Some("Bearer token123")
664        );
665    }
666
667    #[test]
668    fn test_builder_and_layer_roundtrip_response() {
669        let raw = HttpResponseBuilder::new()
670            .status(201, "Created")
671            .header("Location", "/resource/1")
672            .body(b"{}".to_vec())
673            .build();
674
675        let layer = make_layer(&raw);
676        assert_eq!(layer.status_code(&raw), Some(201));
677        assert_eq!(layer.reason(&raw), Some("Created"));
678        assert_eq!(layer.header_value(&raw, "Location"), Some("/resource/1"));
679        let body = &raw[layer.body_offset(&raw).unwrap()..];
680        assert_eq!(body, b"{}");
681    }
682}