Skip to main content

rust_web_server/request/
mod.rs

1//! HTTP requests: [`Request`] and its parser.
2//!
3//! [`Request::parse`] turns a request head (`GET /path HTTP/1.1`, header lines,
4//! blank line) into a `Request`; `generate` writes one back to bytes (used by the
5//! proxy and tests). The request target is normalized during parsing: origin-form
6//! is kept, absolute-form reduced to its path, `*` kept, anything else rejected.
7//! Look headers up with `request.header(name)` (case-insensitive, no
8//! allocation). `get_query`, `get_path`, `get_domain` and `get_port` read the URL
9//! parts; the domain and port come from the `Host` header, not the socket.
10//! Examples: `src/request/example/`.
11
12#[cfg(test)]
13mod tests;
14#[cfg(test)]
15mod example;
16#[cfg(test)]
17mod reference_parser;
18
19use std::collections::HashMap;
20use std::io;
21use std::io::Cursor;
22use crate::header::Header;
23use crate::http::HTTP;
24use crate::symbol::SYMBOL;
25use crate::url::URL;
26
27mod body;
28pub use body::Body;
29
30/// A parsed HTTP request.
31#[derive(PartialEq, Eq, Clone, Debug)]
32pub struct Request {
33    /// HTTP method: `"GET"`, `"POST"`, etc. Compare with [`METHOD`] constants.
34    pub method: String,
35    /// Request URI including path and query string, e.g. `"/search?q=rust"`.
36    pub request_uri: String,
37    /// HTTP version string, e.g. `"HTTP/1.1"`. Compare with [`crate::http::VERSION`] constants.
38    pub http_version: String,
39    /// Request headers.
40    pub headers: Vec<Header>,
41    /// The request body: its bytes (`&request.body[..]`, `request.body.len()`)
42    /// — or, for an application that opted in with
43    /// [`Application::streams_body`](crate::application::Application::streams_body),
44    /// a stream to read as it arrives. See [`Body`].
45    pub body: Body,
46}
47
48#[derive(PartialEq, Eq, Clone, Debug)]
49pub struct Method {
50    pub get: &'static str,
51    pub head: &'static str,
52    pub post: &'static str,
53    pub put: &'static str,
54    pub delete: &'static str,
55    pub connect: &'static str,
56    pub options: &'static str,
57    pub trace: &'static str,
58    pub patch: &'static str,
59}
60
61pub const METHOD: Method = Method {
62    get: "GET",
63    head: "HEAD",
64    post: "POST",
65    put: "PUT",
66    delete: "DELETE",
67    connect: "CONNECT",
68    options: "OPTIONS",
69    trace: "TRACE",
70    patch: "PATCH",
71};
72
73impl Request {
74    pub const _ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION: &'static str = "Unable to parse method, request uri and http version";
75
76    /// The first header named `name` (case-insensitive). Prefer
77    /// [`Request::header`], which needs no `String`.
78    pub fn get_header(&self, name: String) -> Option<&Header> {
79        self.header(&name)
80    }
81
82    /// The first header named `name`, compared case-insensitively without
83    /// allocating (this used to lowercase every header name on every lookup).
84    pub fn header(&self, name: &str) -> Option<&Header> {
85        self.headers.iter().find(|h| h.name.eq_ignore_ascii_case(name))
86    }
87
88    pub fn get_domain(&self) -> Result<Option<String>, String> {
89        let boxed_host = &self.header(Header::_HOST);
90        if boxed_host.is_none() {
91            return Ok(None);
92        }
93
94        let host_header = boxed_host.unwrap();
95        let host = host_header.value.to_string();
96        if host.is_empty() {
97            return Ok(None); // `Host:` with no value names no domain
98        }
99
100        let boxed_host_port = host.split_once(&SYMBOL.colon.to_string());
101        if boxed_host_port.is_none() {
102            return Ok(Some(host))
103        }
104
105        let (domain, _port_str) = boxed_host_port.unwrap();
106
107        Ok(Some(domain.to_string()))
108    }
109
110    pub fn get_port(&self) -> Result<Option<i128>, String> {
111        let boxed_host = &self.header(Header::_HOST);
112        if boxed_host.is_none() {
113            return Ok(None);
114        }
115
116        let host_header = boxed_host.unwrap();
117        let host = host_header.value.to_string();
118
119        let boxed_host_port = host.split_once(&SYMBOL.colon.to_string());
120        if boxed_host_port.is_none() {
121            return Ok(None)
122        }
123
124        let (_domain, port_str) = boxed_host_port.unwrap();
125
126        let boxed_port_i128 = port_str.parse::<i128>();
127        if boxed_port_i128.is_err() {
128            let message = boxed_port_i128.err().unwrap().to_string();
129            return Err(message);
130        }
131
132        let port = boxed_port_i128.unwrap();
133
134        Ok(Some(port))
135    }
136
137    pub fn get_query(&self) -> Result<Option<HashMap<String, String>>, String> {
138        self.get_uri_query()
139    }
140
141    pub fn get_uri_query(&self) -> Result<Option<HashMap<String, String>>, String> {
142        // it will return an error if unable to parse url
143        // it will return None if there are no query params
144        // scheme and host required for the parse_url function
145        let url_array = ["http://", "localhost/", &self.request_uri];
146        let url = url_array.join(SYMBOL.empty_string);
147
148        let boxed_url_components = URL::parse(&url);
149        if boxed_url_components.is_err() {
150            let message = boxed_url_components.err().unwrap().to_string();
151            return Err(message)
152        }
153        Ok(boxed_url_components.unwrap().query)
154    }
155
156    pub fn get_path(&self) -> Result<String, String> {
157        self.get_uri_path()
158    }
159
160    pub fn get_uri_path(&self) -> Result<String, String> {
161        // scheme and host required for the parse_url function
162        let url = self.request_uri.to_string();
163
164        let boxed_url_components = crate::url::URL::parse_request_target(&url);
165        if boxed_url_components.is_err() {
166            let message = boxed_url_components.err().unwrap().to_string();
167            return Err(message)
168        }
169        Ok(boxed_url_components.unwrap().path)
170    }
171
172    /// `true` if `method` (any case) is one of [`Request::method_list`],
173    /// without building that list — it's checked on every request.
174    pub fn is_supported_method(method: &str) -> bool {
175        [METHOD.get, METHOD.head, METHOD.post, METHOD.put, METHOD.delete, METHOD.connect, METHOD.options, METHOD.trace, METHOD.patch]
176            .iter()
177            .any(|m| m.eq_ignore_ascii_case(method))
178    }
179
180    pub fn method_list() -> Vec<String> {
181        let method_get = METHOD.get.to_string();
182        let method_head = METHOD.head.to_string();
183        let method_post = METHOD.post.to_string();
184        let method_put = METHOD.put.to_string();
185        let method_delete = METHOD.delete.to_string();
186        let method_connect = METHOD.connect.to_string();
187        let method_options = METHOD.options.to_string();
188        let method_trace = METHOD.trace.to_string();
189        let method_patch = METHOD.patch.to_string();
190
191        let method_list = vec![
192            method_get,
193            method_head,
194            method_post,
195            method_put,
196            method_delete,
197            method_connect,
198            method_options,
199            method_trace,
200            method_patch,
201        ];
202
203        method_list
204    }
205
206    // instance method way to generate a request
207    pub fn generate(&self) -> Vec<u8> {
208        let clone = self.clone();
209        let mut request = Request::_generate_request(clone).as_bytes().to_vec();
210
211        request.extend_from_slice(&self.body);
212
213        request
214    }
215
216    // same as _generate_request, not renamed original for backward compatability
217    pub fn generate_request(request: Request) -> String {
218        Request::_generate_request(request)
219    }
220
221    pub fn _generate_request(request: Request) -> String {
222        let status = [
223            request.method,
224            request.request_uri,
225            request.http_version,
226            SYMBOL.new_line_carriage_return.to_string()
227        ].join(SYMBOL.whitespace);
228
229        let mut headers = SYMBOL.empty_string.to_string();
230        for header in request.headers {
231            let mut header_string = SYMBOL.empty_string.to_string();
232            header_string.push_str(&header.name);
233            header_string.push_str(Header::NAME_VALUE_SEPARATOR);
234            header_string.push_str(&header.value);
235            header_string.push_str(SYMBOL.new_line_carriage_return);
236            headers.push_str(&header_string);
237        }
238
239        let request = format!(
240            "{}{}{}",
241            status,
242            headers,
243            SYMBOL.new_line_carriage_return
244        );
245
246
247        request
248    }
249
250    pub fn parse(request_vec_u8: &[u8]) ->  Result<Request, String> {
251        Request::parse_request(request_vec_u8)
252    }
253
254    pub fn parse_request(request_vec_u8: &[u8]) ->  Result<Request, String> {
255        let mut cursor = io::Cursor::new(request_vec_u8);
256
257        let mut request = Request {
258            method: "".to_string(),
259            request_uri: "".to_string(),
260            http_version: "".to_string(),
261            headers: vec![],
262            body: Default::default(),
263        };
264
265        let content_length: usize = 0;
266        let iteration_number : usize = 0;
267        return match Request::cursor_read(&mut cursor, iteration_number, &mut request, content_length) {
268            Ok(_) => {
269                Ok(request)
270            }
271            Err(error_message) => {
272                Err(error_message)
273            }
274        }
275
276    }
277
278
279    pub fn parse_method_and_request_uri_and_http_version_string(http_version_status_code_reason_phrase: &str) -> Result<(String, String, String), String> {
280        let lowercase_unparsed_method_and_request_uri_and_http_version = http_version_status_code_reason_phrase.trim();
281
282        let boxed_split_without_method = lowercase_unparsed_method_and_request_uri_and_http_version.split_once(SYMBOL.whitespace);
283        if boxed_split_without_method.is_none() {
284            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
285        }
286
287        let (method, without_method) = boxed_split_without_method.unwrap();
288        if !Request::is_supported_method(method) {
289            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
290        }
291
292        let boxed_without_method = without_method.split_once(SYMBOL.whitespace);
293        if boxed_without_method.is_none() {
294            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
295        }
296
297        let (request_uri, http_version) = boxed_without_method.unwrap();
298        let request_uri = Request::normalize_request_target(request_uri)
299            .ok_or_else(|| Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())?;
300
301
302        if !HTTP::is_supported_version(http_version) {
303            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
304        }
305
306        Ok((method.to_string(), request_uri, http_version.to_string()))
307
308    }
309
310    /// RFC 9112 §3.2 request targets: origin-form (`/path?query`) is kept,
311    /// absolute-form (`http://host/path?query`) is reduced to its
312    /// origin-form path, asterisk-form (`*`) is kept. Anything else —
313    /// authority-form (`host:443`, CONNECT is not served) or junk like `:x` —
314    /// is `None`, so the request is rejected before reaching a controller.
315    pub fn normalize_request_target(target: &str) -> Option<String> {
316        if target.starts_with('/') || target == "*" {
317            return Some(target.to_string());
318        }
319        let lower = target.to_ascii_lowercase();
320        let after_scheme = if lower.starts_with("http://") {
321            &target["http://".len()..]
322        } else if lower.starts_with("https://") {
323            &target["https://".len()..]
324        } else {
325            return None;
326        };
327        let path_start = after_scheme.find(|c| c == '/' || c == '?').unwrap_or(after_scheme.len());
328        if path_start == 0 {
329            return None; // no authority
330        }
331        let rest = &after_scheme[path_start..];
332        Some(if rest.starts_with('/') { rest.to_string() } else { format!("/{}", rest) })
333    }
334
335    /// `Name: value` → `Header`. The name ends at the first `:`; the value
336    /// is everything after it, without surrounding spaces/tabs (RFC 9112
337    /// §5.1). Splitting on every ": " used to cut a value that itself
338    /// contained ": " short, and `Name:value` (no space) had an empty value.
339    pub fn parse_http_request_header_string(header_string: &str) -> Header {
340        let line = header_string.trim_end_matches(['\r', '\n']);
341        match line.split_once(':') {
342            Some((name, value)) => Header {
343                name: Header::name_from(name),
344                value: value.trim_matches([' ', '\t']).to_string().into(),
345            },
346            None => Header { name: line.to_string().into(), value: String::new().into() },
347        }
348    }
349
350    /// Parse the request line, headers, and body from `cursor` into `request`.
351    ///
352    /// Iterative: this used to recurse once per header line, so a single
353    /// request with a few thousand short header lines overflowed the worker
354    /// thread's stack and aborted the whole process. Errors anywhere in the
355    /// head (bad UTF-8, an invalid Content-Length) are returned instead of
356    /// being logged and dropped.
357    pub fn cursor_read(cursor: &mut Cursor<&[u8]>, mut iteration_number: usize, request: &mut Request, content_length: usize) -> Result<bool, String> {
358        let _ = content_length;
359        // Each line is a slice of the input (this used to read every line
360        // into a new Vec and String); only the values kept are allocated.
361        let input: &[u8] = cursor.get_ref();
362        loop {
363            let start = (cursor.position() as usize).min(input.len());
364            let rest = &input[start..];
365            let line_len = rest.iter().position(|&b| b == b'\n').map_or(rest.len(), |i| i + 1);
366            cursor.set_position((start + line_len) as u64);
367            let string = std::str::from_utf8(&rest[..line_len]).map_err(|e| e.to_string())?;
368
369            let is_first_iteration = iteration_number == 0;
370            if is_first_iteration {
371                let (method, request_uri, http_version) =
372                    Request::parse_method_and_request_uri_and_http_version_string(string)?;
373                request.method = method;
374                request.request_uri = request_uri;
375                request.http_version = http_version;
376            }
377
378            // Blank line (end of the head) or end of input.
379            if line_len == 0 || string.trim().is_empty() {
380                break;
381            }
382
383            // The request line is not a header. (This used to push an empty
384            // `Header { name: "", value: "" }` for it, which the reverse proxy
385            // forwarded as a malformed ": " line — nginx answers 400.)
386            if !is_first_iteration {
387                let header = Request::parse_http_request_header_string(string);
388                if header.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH) {
389                    // A client-supplied value: reject garbage (RFC 9112 §6.3) instead of panicking.
390                    let _: usize = header.value.trim().parse()
391                        .map_err(|_| format!("invalid Content-Length: {:?}", header.value))?;
392                }
393                request.headers.push(header);
394            }
395            iteration_number += 1;
396        }
397
398        // remaining part is request body
399        let start = (cursor.position() as usize).min(input.len());
400        request.body = input[start..].to_vec().into();
401        cursor.set_position(input.len() as u64);
402
403        Ok(true)
404    }
405
406}
407
408impl std::fmt::Display for Request {
409    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
410        write!(fmt, "Request method {} and request uri {} and http_version {}", self.method, self.request_uri, self.http_version)
411    }
412}