tiny_http/
common.rs

1use ascii::{AsciiStr, AsciiString, FromAsciiError};
2use std::cmp::Ordering;
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5
6/// Status code of a request or response.
7#[derive(Eq, PartialEq, Copy, Clone, Debug, Ord, PartialOrd)]
8pub struct StatusCode(pub u16);
9
10impl StatusCode {
11    /// Returns the default reason phrase for this status code.
12    /// For example the status code 404 corresponds to "Not Found".
13    pub fn default_reason_phrase(&self) -> &'static str {
14        match self.0 {
15            100 => "Continue",
16            101 => "Switching Protocols",
17            102 => "Processing",
18            103 => "Early Hints",
19
20            200 => "OK",
21            201 => "Created",
22            202 => "Accepted",
23            203 => "Non-Authoritative Information",
24            204 => "No Content",
25            205 => "Reset Content",
26            206 => "Partial Content",
27            207 => "Multi-Status",
28            208 => "Already Reported",
29            226 => "IM Used",
30
31            300 => "Multiple Choices",
32            301 => "Moved Permanently",
33            302 => "Found",
34            303 => "See Other",
35            304 => "Not Modified",
36            305 => "Use Proxy",
37            307 => "Temporary Redirect",
38            308 => "Permanent Redirect",
39
40            400 => "Bad Request",
41            401 => "Unauthorized",
42            402 => "Payment Required",
43            403 => "Forbidden",
44            404 => "Not Found",
45            405 => "Method Not Allowed",
46            406 => "Not Acceptable",
47            407 => "Proxy Authentication Required",
48            408 => "Request Timeout",
49            409 => "Conflict",
50            410 => "Gone",
51            411 => "Length Required",
52            412 => "Precondition Failed",
53            413 => "Payload Too Large",
54            414 => "URI Too Long",
55            415 => "Unsupported Media Type",
56            416 => "Range Not Satisfiable",
57            417 => "Expectation Failed",
58            421 => "Misdirected Request",
59            422 => "Unprocessable Entity",
60            423 => "Locked",
61            424 => "Failed Dependency",
62            426 => "Upgrade Required",
63            428 => "Precondition Required",
64            429 => "Too Many Requests",
65            431 => "Request Header Fields Too Large",
66            451 => "Unavailable For Legal Reasons",
67
68            500 => "Internal Server Error",
69            501 => "Not Implemented",
70            502 => "Bad Gateway",
71            503 => "Service Unavailable",
72            504 => "Gateway Timeout",
73            505 => "HTTP Version Not Supported",
74            506 => "Variant Also Negotiates",
75            507 => "Insufficient Storage",
76            508 => "Loop Detected",
77            510 => "Not Extended",
78            511 => "Network Authentication Required",
79            _ => "Unknown",
80        }
81    }
82}
83
84impl From<i8> for StatusCode {
85    fn from(in_code: i8) -> StatusCode {
86        StatusCode(in_code as u16)
87    }
88}
89
90impl From<u8> for StatusCode {
91    fn from(in_code: u8) -> StatusCode {
92        StatusCode(in_code as u16)
93    }
94}
95
96impl From<i16> for StatusCode {
97    fn from(in_code: i16) -> StatusCode {
98        StatusCode(in_code as u16)
99    }
100}
101
102impl From<u16> for StatusCode {
103    fn from(in_code: u16) -> StatusCode {
104        StatusCode(in_code)
105    }
106}
107
108impl From<i32> for StatusCode {
109    fn from(in_code: i32) -> StatusCode {
110        StatusCode(in_code as u16)
111    }
112}
113
114impl From<u32> for StatusCode {
115    fn from(in_code: u32) -> StatusCode {
116        StatusCode(in_code as u16)
117    }
118}
119
120impl AsRef<u16> for StatusCode {
121    fn as_ref(&self) -> &u16 {
122        &self.0
123    }
124}
125
126impl PartialEq<u16> for StatusCode {
127    fn eq(&self, other: &u16) -> bool {
128        &self.0 == other
129    }
130}
131
132impl PartialEq<StatusCode> for u16 {
133    fn eq(&self, other: &StatusCode) -> bool {
134        self == &other.0
135    }
136}
137
138impl PartialOrd<u16> for StatusCode {
139    fn partial_cmp(&self, other: &u16) -> Option<Ordering> {
140        self.0.partial_cmp(other)
141    }
142}
143
144impl PartialOrd<StatusCode> for u16 {
145    fn partial_cmp(&self, other: &StatusCode) -> Option<Ordering> {
146        self.partial_cmp(&other.0)
147    }
148}
149
150/// Represents a HTTP header.
151#[derive(Debug, Clone)]
152pub struct Header {
153    pub field: HeaderField,
154    pub value: AsciiString,
155}
156
157impl Header {
158    /// Builds a `Header` from two `Vec<u8>`s or two `&[u8]`s.
159    ///
160    /// Example:
161    ///
162    /// ```
163    /// let header = tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/plain"[..]).unwrap();
164    /// ```
165    #[allow(clippy::result_unit_err)]
166    pub fn from_bytes<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
167    where
168        B1: Into<Vec<u8>> + AsRef<[u8]>,
169        B2: Into<Vec<u8>> + AsRef<[u8]>,
170    {
171        let header = HeaderField::from_bytes(header).or(Err(()))?;
172        let value = AsciiString::from_ascii(value).or(Err(()))?;
173
174        Ok(Header {
175            field: header,
176            value,
177        })
178    }
179}
180
181impl FromStr for Header {
182    type Err = ();
183
184    fn from_str(input: &str) -> Result<Header, ()> {
185        let mut elems = input.splitn(2, ':');
186
187        let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188        let value = elems
189            .next()
190            .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191            .ok_or(())?;
192
193        Ok(Header { field, value })
194    }
195}
196
197impl Display for Header {
198    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), fmt::Error> {
199        write!(formatter, "{}: {}", self.field, self.value.as_str())
200    }
201}
202
203/// Field of a header (eg. `Content-Type`, `Content-Length`, etc.)
204///
205/// Comparison between two `HeaderField`s ignores case.
206#[derive(Debug, Clone, Eq)]
207pub struct HeaderField(AsciiString);
208
209impl HeaderField {
210    pub fn from_bytes<B>(bytes: B) -> Result<HeaderField, FromAsciiError<B>>
211    where
212        B: Into<Vec<u8>> + AsRef<[u8]>,
213    {
214        AsciiString::from_ascii(bytes).map(HeaderField)
215    }
216
217    pub fn as_str(&self) -> &AsciiStr {
218        &self.0
219    }
220
221    pub fn equiv(&self, other: &'static str) -> bool {
222        other.eq_ignore_ascii_case(self.as_str().as_str())
223    }
224}
225
226impl FromStr for HeaderField {
227    type Err = ();
228
229    fn from_str(s: &str) -> Result<HeaderField, ()> {
230        if s.contains(char::is_whitespace) {
231            Err(())
232        } else {
233            AsciiString::from_ascii(s).map(HeaderField).map_err(|_| ())
234        }
235    }
236}
237
238impl Display for HeaderField {
239    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), fmt::Error> {
240        write!(formatter, "{}", self.0.as_str())
241    }
242}
243
244impl PartialEq for HeaderField {
245    fn eq(&self, other: &HeaderField) -> bool {
246        let self_str: &str = self.as_str().as_ref();
247        let other_str = other.as_str().as_ref();
248        self_str.eq_ignore_ascii_case(other_str)
249    }
250}
251
252/// HTTP request methods
253///
254/// As per [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4.1) and
255/// [RFC 5789](https://tools.ietf.org/html/rfc5789)
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub enum Method {
258    /// `GET`
259    Get,
260
261    /// `HEAD`
262    Head,
263
264    /// `POST`
265    Post,
266
267    /// `PUT`
268    Put,
269
270    /// `DELETE`
271    Delete,
272
273    /// `CONNECT`
274    Connect,
275
276    /// `OPTIONS`
277    Options,
278
279    /// `TRACE`
280    Trace,
281
282    /// `PATCH`
283    Patch,
284
285    /// Request methods not standardized by the IETF
286    NonStandard(AsciiString),
287}
288
289impl Method {
290    pub fn as_str(&self) -> &str {
291        match *self {
292            Method::Get => "GET",
293            Method::Head => "HEAD",
294            Method::Post => "POST",
295            Method::Put => "PUT",
296            Method::Delete => "DELETE",
297            Method::Connect => "CONNECT",
298            Method::Options => "OPTIONS",
299            Method::Trace => "TRACE",
300            Method::Patch => "PATCH",
301            Method::NonStandard(ref s) => s.as_str(),
302        }
303    }
304}
305
306impl FromStr for Method {
307    type Err = ();
308
309    fn from_str(s: &str) -> Result<Method, ()> {
310        Ok(match s {
311            "GET" => Method::Get,
312            "HEAD" => Method::Head,
313            "POST" => Method::Post,
314            "PUT" => Method::Put,
315            "DELETE" => Method::Delete,
316            "CONNECT" => Method::Connect,
317            "OPTIONS" => Method::Options,
318            "TRACE" => Method::Trace,
319            "PATCH" => Method::Patch,
320            s => {
321                let ascii_string = AsciiString::from_ascii(s).map_err(|_| ())?;
322                Method::NonStandard(ascii_string)
323            }
324        })
325    }
326}
327
328impl Display for Method {
329    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), fmt::Error> {
330        write!(formatter, "{}", self.as_str())
331    }
332}
333
334/// HTTP version (usually 1.0 or 1.1).
335#[allow(clippy::upper_case_acronyms)]
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct HTTPVersion(pub u8, pub u8);
338
339impl Display for HTTPVersion {
340    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), fmt::Error> {
341        write!(formatter, "{}.{}", self.0, self.1)
342    }
343}
344
345impl Ord for HTTPVersion {
346    fn cmp(&self, other: &Self) -> Ordering {
347        let HTTPVersion(my_major, my_minor) = *self;
348        let HTTPVersion(other_major, other_minor) = *other;
349
350        if my_major != other_major {
351            return my_major.cmp(&other_major);
352        }
353
354        my_minor.cmp(&other_minor)
355    }
356}
357
358impl PartialOrd for HTTPVersion {
359    fn partial_cmp(&self, other: &HTTPVersion) -> Option<Ordering> {
360        Some(self.cmp(other))
361    }
362}
363
364impl PartialEq<(u8, u8)> for HTTPVersion {
365    fn eq(&self, &(major, minor): &(u8, u8)) -> bool {
366        self.eq(&HTTPVersion(major, minor))
367    }
368}
369
370impl PartialEq<HTTPVersion> for (u8, u8) {
371    fn eq(&self, other: &HTTPVersion) -> bool {
372        let &(major, minor) = self;
373        HTTPVersion(major, minor).eq(other)
374    }
375}
376
377impl PartialOrd<(u8, u8)> for HTTPVersion {
378    fn partial_cmp(&self, &(major, minor): &(u8, u8)) -> Option<Ordering> {
379        self.partial_cmp(&HTTPVersion(major, minor))
380    }
381}
382
383impl PartialOrd<HTTPVersion> for (u8, u8) {
384    fn partial_cmp(&self, other: &HTTPVersion) -> Option<Ordering> {
385        let &(major, minor) = self;
386        HTTPVersion(major, minor).partial_cmp(other)
387    }
388}
389
390impl From<(u8, u8)> for HTTPVersion {
391    fn from((major, minor): (u8, u8)) -> HTTPVersion {
392        HTTPVersion(major, minor)
393    }
394}
395
396#[cfg(test)]
397mod test {
398    use super::Header;
399    use httpdate::HttpDate;
400    use std::time::{Duration, SystemTime};
401
402    #[test]
403    fn test_parse_header() {
404        let header: Header = "Content-Type: text/html".parse().unwrap();
405
406        assert!(header.field.equiv(&"content-type"));
407        assert!(header.value.as_str() == "text/html");
408
409        assert!("hello world".parse::<Header>().is_err());
410    }
411
412    #[test]
413    fn formats_date_correctly() {
414        let http_date = HttpDate::from(SystemTime::UNIX_EPOCH + Duration::from_secs(420895020));
415
416        assert_eq!(http_date.to_string(), "Wed, 04 May 1983 11:17:00 GMT")
417    }
418
419    #[test]
420    fn test_parse_header_with_doublecolon() {
421        let header: Header = "Time: 20: 34".parse().unwrap();
422
423        assert!(header.field.equiv(&"time"));
424        assert!(header.value.as_str() == "20: 34");
425    }
426
427    // This tests reslstance to RUSTSEC-2020-0031: "HTTP Request smuggling
428    // through malformed Transfer Encoding headers"
429    // (https://rustsec.org/advisories/RUSTSEC-2020-0031.html).
430    #[test]
431    fn test_strict_headers() {
432        assert!("Transfer-Encoding : chunked".parse::<Header>().is_err());
433        assert!(" Transfer-Encoding: chunked".parse::<Header>().is_err());
434        assert!("Transfer Encoding: chunked".parse::<Header>().is_err());
435        assert!(" Transfer\tEncoding : chunked".parse::<Header>().is_err());
436        assert!("Transfer-Encoding: chunked".parse::<Header>().is_ok());
437        assert!("Transfer-Encoding: chunked ".parse::<Header>().is_ok());
438        assert!("Transfer-Encoding:   chunked ".parse::<Header>().is_ok());
439    }
440}