Skip to main content

ehttpd/http/
request.rs

1//! A HTTP request
2
3use crate::bytes::{Data, Parse, Source};
4use crate::err;
5use crate::error::Error;
6use std::io::Read;
7use std::path::Path;
8
9/// A HTTP request
10#[derive(Debug)]
11pub struct Request<'a, const HEADER_SIZE_MAX: usize = 4096> {
12    /// The raw header bytes
13    pub header: Data,
14    /// The method part within the request line
15    pub method: Data,
16    /// The target part within the request line
17    pub target: Data,
18    /// The version part within the request line
19    pub version: Data,
20    /// The key/value fields within the header
21    pub fields: Vec<(Data, Data)>,
22    /// The connection stream
23    pub stream: &'a mut Source,
24}
25// Core functionality
26impl<'a, const HEADER_SIZE_MAX: usize> Request<'a, HEADER_SIZE_MAX> {
27    /// Reads a HTTP request from a readable `stream`
28    pub fn from_stream(stream: &'a mut Source) -> Result<Option<Self>, Error> {
29        // Read the raw header or return `None` if the connection has been closed
30        let header = Self::read_header(stream)?;
31        if header.is_empty() {
32            return Ok(None);
33        }
34
35        // Parse the start line
36        let mut header_parsing = header.clone();
37        let (method, target, version) = {
38            let (method, target, version) = Self::parse_start_line(&mut header_parsing)?;
39            (method.trim(), target.trim(), version.trim())
40        };
41
42        // Parse the fields
43        let mut fields = Vec::new();
44        while !header_parsing.eq(b"\r\n") {
45            let (key, value) = Self::parse_field(&mut header_parsing)?;
46            fields.push((key, value));
47        }
48
49        // Init self
50        Ok(Some(Self { header, method, target, version, fields, stream }))
51    }
52
53    /// Reads the entire HTTP header from the stream
54    fn read_header(stream: &mut Source) -> Result<Data, Error> {
55        // Read the header
56        let mut header = Vec::with_capacity(HEADER_SIZE_MAX);
57        'read_loop: for byte in stream.bytes() {
58            // Read the next byte
59            let byte = byte?;
60            header.push(byte);
61
62            // Check if we have the header
63            if header.ends_with(b"\r\n\r\n") {
64                break 'read_loop;
65            }
66            if header.len() == HEADER_SIZE_MAX {
67                return Err(err!("HTTP header is too large"));
68            }
69        }
70
71        // Create the RcVec
72        header.shrink_to_fit();
73        let header = Data::from(header);
74        Ok(header)
75    }
76    /// Parses the start line
77    #[allow(clippy::type_complexity)]
78    fn parse_start_line(header: &mut Data) -> Result<(Data, Data, Data), Error> {
79        // Split the header line
80        let mut line = header.split_off(b"\r\n").ok_or_else(|| err!("Truncated HTTP start line: {header}"))?;
81        let method = line.split_off(b" ").ok_or_else(|| err!("Invalid HTTP start line: {line}"))?;
82        let target = line.split_off(b" ").ok_or_else(|| err!("Invalid HTTP start line: {line}"))?;
83        Ok((method, target, line))
84    }
85    /// Parses a header field
86    fn parse_field(header: &mut Data) -> Result<(Data, Data), Error> {
87        // Parse the field
88        let mut line = header.split_off(b"\r\n").ok_or_else(|| err!("Truncated HTTP header field: {header}"))?;
89        let key = line.split_off(b":").ok_or_else(|| err!("Invalid HTTP header field: {line}"))?;
90
91        // Trim the field values
92        let key = key.trim();
93        let value = line.trim();
94        Ok((key, value))
95    }
96}
97// Useful helpers
98impl<'a, const HEADER_SIZE_MAX: usize> Request<'a, HEADER_SIZE_MAX> {
99    /// Gets the request target as path
100    ///
101    /// # Important
102    /// On non-unix platforms, this function uses a `str` as intermediate representation, so the path must be valid
103    /// UTF-8. If this might be a problem, you should use the raw target field directly.
104    #[cfg(target_family = "unix")]
105    pub fn target_path(&self) -> Option<&Path> {
106        use std::ffi::OsStr;
107        use std::os::unix::ffi::OsStrExt;
108
109        // Create the path directly without going via `str`
110        let target = OsStr::from_bytes(&self.target);
111        Some(Path::new(target))
112    }
113    /// Gets the request target as path
114    ///
115    /// # Important
116    /// On non-unix platforms, this function uses a `str` as intermediate representation, so the path must be valid
117    /// UTF-8. If this might be a problem, you should use the raw target field directly.
118    #[cfg(not(any(target_family = "unix")))]
119    pub fn target_path(&self) -> Option<&Path> {
120        // Convert the target to UTF-8 and return it as string
121        let target = str::from_utf8(&self.target).ok()?;
122        Some(Path::new(target))
123    }
124
125    /// Gets the field with the given name (performs an ASCII-case-insensitve comparison)
126    pub fn field<N>(&self, name: N) -> Option<&Data>
127    where
128        N: AsRef<[u8]>,
129    {
130        for (key, value) in &self.fields {
131            // Perform a case-insensitive comparison since HTTP header field names are not case-sensitive
132            if key.eq_ignore_ascii_case(name.as_ref()) {
133                return Some(value);
134            }
135        }
136        None
137    }
138    /// The request content length field if any
139    pub fn content_length(&self) -> Result<Option<u64>, Error> {
140        // Get the content length field if set
141        let Some(content_length_raw) = self.field("Content-Length") else {
142            // Content-length field is unset
143            return Ok(None);
144        };
145
146        // Parse the field
147        let content_length_utf8 = str::from_utf8(content_length_raw)?;
148        let content_length: u64 = content_length_utf8.parse()?;
149        Ok(Some(content_length))
150    }
151
152    /// Reads the request body into memory *if a content-length header is set* and *transfer-encoding is not chunked*;
153    /// returns `None` otherwise
154    pub fn read_body_data(&mut self, content_length_max: u64) -> Result<Option<Data>, Error> {
155        // Check if the transfer encoding is chunked
156        let is_chunked = self.field("Transfer-Encoding").map(|encoding| encoding.eq_ignore_ascii_case(b"chunked"));
157        let (None | Some(false)) = is_chunked else {
158            // Chunked transfer-encoding is not supported
159            return Ok(None);
160        };
161
162        // Check if a content-length header is set
163        let Some(content_length) = self.content_length()? else {
164            // Indeterminate lengths are not supported
165            return Ok(None);
166        };
167
168        // Validate content length
169        let true = content_length <= content_length_max else {
170            // Body is too large
171            return Err(err!("HTTP body is too large"));
172        };
173
174        // Read body
175        let mut body = Vec::new();
176        let body_len = self.stream.take(content_length).read_to_end(&mut body)? as u64;
177        let true = body_len == content_length else {
178            // Truncated body
179            return Err(err!("Truncated HTTP body"))?;
180        };
181
182        // Return body
183        let body = Data::from(body);
184        Ok(Some(body))
185    }
186
187    /// Checks if the header has `Connection: Close` set
188    pub fn has_connection_close(&self) -> bool {
189        // Search for `Connection` header
190        for (key, value) in &self.fields {
191            if key.eq_ignore_ascii_case(b"Connection") {
192                return value.eq_ignore_ascii_case(b"close");
193            }
194        }
195        false
196    }
197}