1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum HttpMethod {
10 Get,
11 Post,
12 Put,
13 Delete,
14 Head,
15 Options,
16 Patch,
17 Other(String),
18}
19
20impl HttpMethod {
21 #[allow(clippy::should_implement_trait)]
23 pub fn from_str(s: &str) -> Self {
24 match s.to_ascii_uppercase().as_str() {
25 "GET" => Self::Get,
26 "POST" => Self::Post,
27 "PUT" => Self::Put,
28 "DELETE" => Self::Delete,
29 "HEAD" => Self::Head,
30 "OPTIONS" => Self::Options,
31 "PATCH" => Self::Patch,
32 other => Self::Other(other.to_string()),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct HttpHeader {
40 pub name: String,
41 pub value: String,
42}
43
44#[derive(Debug, Clone)]
46pub struct HttpRequest {
47 pub method: HttpMethod,
48 pub path: String,
49 pub version: String,
50 pub headers: Vec<HttpHeader>,
51 pub body: Vec<u8>,
52}
53
54#[derive(Debug, Clone)]
56pub struct HttpResponse {
57 pub version: String,
58 pub status_code: u16,
59 pub reason: String,
60 pub headers: Vec<HttpHeader>,
61 pub body: Vec<u8>,
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub enum HttpError {
67 MalformedRequestLine,
68 MalformedStatusLine,
69 MalformedHeader(String),
70 InvalidStatusCode(String),
71 UnexpectedEnd,
72}
73
74impl std::fmt::Display for HttpError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::MalformedRequestLine => write!(f, "malformed HTTP request line"),
78 Self::MalformedStatusLine => write!(f, "malformed HTTP status line"),
79 Self::MalformedHeader(s) => write!(f, "malformed HTTP header: {s}"),
80 Self::InvalidStatusCode(s) => write!(f, "invalid HTTP status code: {s}"),
81 Self::UnexpectedEnd => write!(f, "unexpected end of HTTP message"),
82 }
83 }
84}
85
86pub fn parse_request(raw: &[u8]) -> Result<HttpRequest, HttpError> {
88 let text = std::str::from_utf8(raw).map_err(|_| HttpError::MalformedRequestLine)?;
89 let mut lines = text.split("\r\n");
90 let request_line = lines.next().ok_or(HttpError::UnexpectedEnd)?;
91 let mut parts = request_line.splitn(3, ' ');
92 let method = parts
93 .next()
94 .ok_or(HttpError::MalformedRequestLine)
95 .map(HttpMethod::from_str)?;
96 let path = parts
97 .next()
98 .ok_or(HttpError::MalformedRequestLine)?
99 .to_string();
100 let version = parts
101 .next()
102 .ok_or(HttpError::MalformedRequestLine)?
103 .to_string();
104 let mut headers = vec![];
105 for line in lines.by_ref() {
106 if line.is_empty() {
107 break;
108 }
109 let mut h = line.splitn(2, ':');
110 let name = h.next().unwrap_or("").trim().to_string();
111 let value = h.next().unwrap_or("").trim().to_string();
112 headers.push(HttpHeader { name, value });
113 }
114 Ok(HttpRequest {
115 method,
116 path,
117 version,
118 headers,
119 body: vec![],
120 })
121}
122
123pub fn parse_response(raw: &[u8]) -> Result<HttpResponse, HttpError> {
125 let text = std::str::from_utf8(raw).map_err(|_| HttpError::MalformedStatusLine)?;
126 let mut lines = text.split("\r\n");
127 let status_line = lines.next().ok_or(HttpError::UnexpectedEnd)?;
128 let mut parts = status_line.splitn(3, ' ');
129 let version = parts
130 .next()
131 .ok_or(HttpError::MalformedStatusLine)?
132 .to_string();
133 let code_str = parts.next().ok_or(HttpError::MalformedStatusLine)?;
134 let status_code = code_str
135 .parse::<u16>()
136 .map_err(|_| HttpError::InvalidStatusCode(code_str.to_string()))?;
137 let reason = parts.next().unwrap_or("").to_string();
138 let mut headers = vec![];
139 for line in lines.by_ref() {
140 if line.is_empty() {
141 break;
142 }
143 let mut h = line.splitn(2, ':');
144 let name = h.next().unwrap_or("").trim().to_string();
145 let value = h.next().unwrap_or("").trim().to_string();
146 headers.push(HttpHeader { name, value });
147 }
148 Ok(HttpResponse {
149 version,
150 status_code,
151 reason,
152 headers,
153 body: vec![],
154 })
155}
156
157pub fn find_header<'a>(headers: &'a [HttpHeader], name: &str) -> Option<&'a str> {
159 headers
160 .iter()
161 .find(|h| h.name.eq_ignore_ascii_case(name))
162 .map(|h| h.value.as_str())
163}
164
165pub fn is_http11(req: &HttpRequest) -> bool {
167 req.version == "HTTP/1.1"
168}
169
170pub fn content_length(headers: &[HttpHeader]) -> Option<usize> {
172 find_header(headers, "content-length").and_then(|v| v.trim().parse().ok())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_parse_get_request() {
181 let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
183 let req = parse_request(raw).expect("should succeed");
184 assert_eq!(req.method, HttpMethod::Get);
185 assert_eq!(req.path, "/");
186 }
187
188 #[test]
189 fn test_parse_response_200() {
190 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
192 let resp = parse_response(raw).expect("should succeed");
193 assert_eq!(resp.status_code, 200);
194 }
195
196 #[test]
197 fn test_find_header_case_insensitive() {
198 let headers = vec![HttpHeader {
200 name: "Content-Type".to_string(),
201 value: "text/html".to_string(),
202 }];
203 assert_eq!(find_header(&headers, "content-type"), Some("text/html"));
204 }
205
206 #[test]
207 fn test_is_http11_true() {
208 let raw = b"GET / HTTP/1.1\r\n\r\n";
210 let req = parse_request(raw).expect("should succeed");
211 assert!(is_http11(&req));
212 }
213
214 #[test]
215 fn test_content_length_header() {
216 let headers = vec![HttpHeader {
218 name: "Content-Length".to_string(),
219 value: "42".to_string(),
220 }];
221 assert_eq!(content_length(&headers), Some(42));
222 }
223
224 #[test]
225 fn test_method_post() {
226 let raw = b"POST /data HTTP/1.1\r\n\r\n";
228 let req = parse_request(raw).expect("should succeed");
229 assert_eq!(req.method, HttpMethod::Post);
230 }
231
232 #[test]
233 fn test_invalid_status_code() {
234 let raw = b"HTTP/1.1 OK notanumber\r\n\r\n";
236 assert!(parse_response(raw).is_err());
237 }
238
239 #[test]
240 fn test_multiple_headers() {
241 let raw = b"GET / HTTP/1.1\r\nHost: x\r\nAccept: */*\r\n\r\n";
243 let req = parse_request(raw).expect("should succeed");
244 assert_eq!(req.headers.len(), 2);
245 }
246
247 #[test]
248 fn test_find_header_missing() {
249 let headers: Vec<HttpHeader> = vec![];
251 assert!(find_header(&headers, "x-custom").is_none());
252 }
253
254 #[test]
255 fn test_parse_response_404() {
256 let raw = b"HTTP/1.1 404 Not Found\r\n\r\n";
258 let resp = parse_response(raw).expect("should succeed");
259 assert_eq!(resp.status_code, 404);
260 assert_eq!(resp.reason, "Not Found");
261 }
262}