stackforge_core/layer/http/
request.rs1use super::detection::is_http_request;
8
9#[derive(Debug, Clone)]
15pub struct HttpRequest<'a> {
16 pub method: &'a str,
18 pub uri: &'a str,
20 pub version: &'a str,
22 pub headers: Vec<(&'a str, &'a str)>,
25 pub body_offset: usize,
30}
31
32impl<'a> HttpRequest<'a> {
33 pub fn parse(buf: &'a [u8]) -> Option<Self> {
40 if !is_http_request(buf) {
42 return None;
43 }
44
45 let text = std::str::from_utf8(buf).ok()?;
46
47 let first_crlf = text.find("\r\n")?;
49 let request_line = &text[..first_crlf];
50
51 let mut parts = request_line.splitn(3, ' ');
53 let method = parts.next()?;
54 let uri = parts.next()?;
55 let version = parts.next()?;
56
57 if !version.starts_with("HTTP/") {
59 return None;
60 }
61
62 let header_block_start = first_crlf + 2; let end_marker = "\r\n\r\n";
68 let search_start = if header_block_start >= 2 {
69 header_block_start - 2
70 } else {
71 0
72 };
73 let headers_end_offset = text[search_start..]
74 .find(end_marker)
75 .map(|rel| search_start + rel)?;
76 let body_offset = headers_end_offset + end_marker.len();
77
78 let header_text = if header_block_start <= headers_end_offset {
80 &text[header_block_start..headers_end_offset]
81 } else {
82 ""
83 };
84 let headers = parse_headers(header_text);
85
86 Some(Self {
87 method,
88 uri,
89 version,
90 headers,
91 body_offset,
92 })
93 }
94}
95
96fn parse_headers<'a>(header_text: &'a str) -> Vec<(&'a str, &'a str)> {
101 header_text
102 .split("\r\n")
103 .filter_map(|line| {
104 let colon_pos = line.find(':')?;
105 let name = &line[..colon_pos];
106 let value = line[colon_pos + 1..].trim();
107 Some((name, value))
108 })
109 .collect()
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_parse_simple_get() {
118 let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n";
119 let req = HttpRequest::parse(raw).unwrap();
120 assert_eq!(req.method, "GET");
121 assert_eq!(req.uri, "/");
122 assert_eq!(req.version, "HTTP/1.1");
123 assert_eq!(req.headers.len(), 2);
124 assert_eq!(req.headers[0], ("Host", "example.com"));
125 assert_eq!(req.headers[1], ("Accept", "*/*"));
126 assert_eq!(req.body_offset, raw.len());
127 }
128
129 #[test]
130 fn test_parse_post_with_body() {
131 let raw = b"POST /submit HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello";
132 let req = HttpRequest::parse(raw).unwrap();
133 assert_eq!(req.method, "POST");
134 assert_eq!(req.uri, "/submit");
135 assert_eq!(req.version, "HTTP/1.1");
136 let body = &raw[req.body_offset..];
138 assert_eq!(body, b"hello");
139 }
140
141 #[test]
142 fn test_parse_rejects_response() {
143 let raw = b"HTTP/1.1 200 OK\r\n\r\n";
144 assert!(HttpRequest::parse(raw).is_none());
145 }
146
147 #[test]
148 fn test_parse_rejects_incomplete() {
149 let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\n";
151 assert!(HttpRequest::parse(raw).is_none());
152 }
153
154 #[test]
155 fn test_header_case_preserved() {
156 let raw = b"GET / HTTP/1.0\r\ncontent-type: text/html\r\n\r\n";
157 let req = HttpRequest::parse(raw).unwrap();
158 assert_eq!(req.headers[0].0, "content-type");
159 assert_eq!(req.headers[0].1, "text/html");
160 }
161}