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`] or
173    /// [`PURGE`](Request::PURGE), without building that list — it's checked
174    /// on every request.
175    pub fn is_supported_method(method: &str) -> bool {
176        [METHOD.get, METHOD.head, METHOD.post, METHOD.put, METHOD.delete, METHOD.connect, METHOD.options, METHOD.trace, METHOD.patch, Request::PURGE]
177            .iter()
178            .any(|m| m.eq_ignore_ascii_case(method))
179    }
180
181    /// The cache purge method (nginx/Varnish), accepted since 18.11.0 so a
182    /// [`CacheLayer`](crate::cache::CacheLayer) with a purge token can answer
183    /// it; anything else treats it like any unrouted method.
184    pub const PURGE: &'static str = "PURGE";
185
186    pub fn method_list() -> Vec<String> {
187        let method_get = METHOD.get.to_string();
188        let method_head = METHOD.head.to_string();
189        let method_post = METHOD.post.to_string();
190        let method_put = METHOD.put.to_string();
191        let method_delete = METHOD.delete.to_string();
192        let method_connect = METHOD.connect.to_string();
193        let method_options = METHOD.options.to_string();
194        let method_trace = METHOD.trace.to_string();
195        let method_patch = METHOD.patch.to_string();
196
197        let method_list = vec![
198            method_get,
199            method_head,
200            method_post,
201            method_put,
202            method_delete,
203            method_connect,
204            method_options,
205            method_trace,
206            method_patch,
207        ];
208
209        method_list
210    }
211
212    // instance method way to generate a request
213    pub fn generate(&self) -> Vec<u8> {
214        let clone = self.clone();
215        let mut request = Request::_generate_request(clone).as_bytes().to_vec();
216
217        request.extend_from_slice(&self.body);
218
219        request
220    }
221
222    // same as _generate_request, not renamed original for backward compatability
223    pub fn generate_request(request: Request) -> String {
224        Request::_generate_request(request)
225    }
226
227    pub fn _generate_request(request: Request) -> String {
228        let status = [
229            request.method,
230            request.request_uri,
231            request.http_version,
232            SYMBOL.new_line_carriage_return.to_string()
233        ].join(SYMBOL.whitespace);
234
235        let mut headers = SYMBOL.empty_string.to_string();
236        for header in request.headers {
237            let mut header_string = SYMBOL.empty_string.to_string();
238            header_string.push_str(&header.name);
239            header_string.push_str(Header::NAME_VALUE_SEPARATOR);
240            header_string.push_str(&header.value);
241            header_string.push_str(SYMBOL.new_line_carriage_return);
242            headers.push_str(&header_string);
243        }
244
245        let request = format!(
246            "{}{}{}",
247            status,
248            headers,
249            SYMBOL.new_line_carriage_return
250        );
251
252
253        request
254    }
255
256    pub fn parse(request_vec_u8: &[u8]) ->  Result<Request, String> {
257        Request::parse_request(request_vec_u8)
258    }
259
260    pub fn parse_request(request_vec_u8: &[u8]) ->  Result<Request, String> {
261        let mut cursor = io::Cursor::new(request_vec_u8);
262
263        let mut request = Request {
264            method: "".to_string(),
265            request_uri: "".to_string(),
266            http_version: "".to_string(),
267            headers: vec![],
268            body: Default::default(),
269        };
270
271        let content_length: usize = 0;
272        let iteration_number : usize = 0;
273        return match Request::cursor_read(&mut cursor, iteration_number, &mut request, content_length) {
274            Ok(_) => {
275                Ok(request)
276            }
277            Err(error_message) => {
278                Err(error_message)
279            }
280        }
281
282    }
283
284
285    pub fn parse_method_and_request_uri_and_http_version_string(http_version_status_code_reason_phrase: &str) -> Result<(String, String, String), String> {
286        let lowercase_unparsed_method_and_request_uri_and_http_version = http_version_status_code_reason_phrase.trim();
287
288        let boxed_split_without_method = lowercase_unparsed_method_and_request_uri_and_http_version.split_once(SYMBOL.whitespace);
289        if boxed_split_without_method.is_none() {
290            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
291        }
292
293        let (method, without_method) = boxed_split_without_method.unwrap();
294        if !Request::is_supported_method(method) {
295            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
296        }
297
298        let boxed_without_method = without_method.split_once(SYMBOL.whitespace);
299        if boxed_without_method.is_none() {
300            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
301        }
302
303        let (request_uri, http_version) = boxed_without_method.unwrap();
304        let request_uri = Request::normalize_request_target(request_uri)
305            .ok_or_else(|| Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())?;
306
307
308        if !HTTP::is_supported_version(http_version) {
309            return Err(Request::_ERROR_UNABLE_TO_PARSE_METHOD_AND_REQUEST_URI_AND_HTTP_VERSION.to_string())
310        }
311
312        Ok((method.to_string(), request_uri, http_version.to_string()))
313
314    }
315
316    /// RFC 9112 §3.2 request targets: origin-form (`/path?query`) is kept,
317    /// absolute-form (`http://host/path?query`) is reduced to its
318    /// origin-form path, asterisk-form (`*`) is kept. Anything else —
319    /// authority-form (`host:443`, CONNECT is not served) or junk like `:x` —
320    /// is `None`, so the request is rejected before reaching a controller.
321    pub fn normalize_request_target(target: &str) -> Option<String> {
322        if target.starts_with('/') || target == "*" {
323            return Some(target.to_string());
324        }
325        let lower = target.to_ascii_lowercase();
326        let after_scheme = if lower.starts_with("http://") {
327            &target["http://".len()..]
328        } else if lower.starts_with("https://") {
329            &target["https://".len()..]
330        } else {
331            return None;
332        };
333        let path_start = after_scheme.find(|c| c == '/' || c == '?').unwrap_or(after_scheme.len());
334        if path_start == 0 {
335            return None; // no authority
336        }
337        let rest = &after_scheme[path_start..];
338        Some(if rest.starts_with('/') { rest.to_string() } else { format!("/{}", rest) })
339    }
340
341    /// `Name: value` → `Header`. The name ends at the first `:`; the value
342    /// is everything after it, without surrounding spaces/tabs (RFC 9112
343    /// §5.1). Splitting on every ": " used to cut a value that itself
344    /// contained ": " short, and `Name:value` (no space) had an empty value.
345    pub fn parse_http_request_header_string(header_string: &str) -> Header {
346        let line = header_string.trim_end_matches(['\r', '\n']);
347        match line.split_once(':') {
348            Some((name, value)) => Header {
349                name: Header::name_from(name),
350                value: value.trim_matches([' ', '\t']).to_string().into(),
351            },
352            None => Header { name: line.to_string().into(), value: String::new().into() },
353        }
354    }
355
356    /// Parse the request line, headers, and body from `cursor` into `request`.
357    ///
358    /// Iterative: this used to recurse once per header line, so a single
359    /// request with a few thousand short header lines overflowed the worker
360    /// thread's stack and aborted the whole process. Errors anywhere in the
361    /// head (bad UTF-8, an invalid Content-Length) are returned instead of
362    /// being logged and dropped.
363    pub fn cursor_read(cursor: &mut Cursor<&[u8]>, mut iteration_number: usize, request: &mut Request, content_length: usize) -> Result<bool, String> {
364        let _ = content_length;
365        // Each line is a slice of the input (this used to read every line
366        // into a new Vec and String); only the values kept are allocated.
367        let input: &[u8] = cursor.get_ref();
368        loop {
369            let start = (cursor.position() as usize).min(input.len());
370            let rest = &input[start..];
371            let line_len = rest.iter().position(|&b| b == b'\n').map_or(rest.len(), |i| i + 1);
372            cursor.set_position((start + line_len) as u64);
373            let string = std::str::from_utf8(&rest[..line_len]).map_err(|e| e.to_string())?;
374
375            let is_first_iteration = iteration_number == 0;
376            if is_first_iteration {
377                let (method, request_uri, http_version) =
378                    Request::parse_method_and_request_uri_and_http_version_string(string)?;
379                request.method = method;
380                request.request_uri = request_uri;
381                request.http_version = http_version;
382            }
383
384            // Blank line (end of the head) or end of input.
385            if line_len == 0 || string.trim().is_empty() {
386                break;
387            }
388
389            // The request line is not a header. (This used to push an empty
390            // `Header { name: "", value: "" }` for it, which the reverse proxy
391            // forwarded as a malformed ": " line — nginx answers 400.)
392            if !is_first_iteration {
393                let header = Request::parse_http_request_header_string(string);
394                if header.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH) {
395                    // A client-supplied value: reject garbage (RFC 9112 §6.3) instead of panicking.
396                    let _: usize = header.value.trim().parse()
397                        .map_err(|_| format!("invalid Content-Length: {:?}", header.value))?;
398                }
399                request.headers.push(header);
400            }
401            iteration_number += 1;
402        }
403
404        // remaining part is request body
405        let start = (cursor.position() as usize).min(input.len());
406        request.body = input[start..].to_vec().into();
407        cursor.set_position(input.len() as u64);
408
409        Ok(true)
410    }
411
412}
413
414impl std::fmt::Display for Request {
415    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
416        write!(fmt, "Request method {} and request uri {} and http_version {}", self.method, self.request_uri, self.http_version)
417    }
418}