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