rouille/input/
plain.rs

1// Copyright (c) 2016 The Rouille developers
2// Licensed under the Apache License, Version 2.0
3// <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6// at your option. All files in the project carrying such
7// notice may not be copied, modified, or distributed except
8// according to those terms.
9
10use std::error;
11use std::fmt;
12use std::io::Error as IoError;
13use std::io::Read;
14use Request;
15
16/// Error that can happen when parsing the request body as plain text.
17#[derive(Debug)]
18pub enum PlainTextError {
19    /// Can't parse the body of the request because it was already extracted.
20    BodyAlreadyExtracted,
21
22    /// Wrong content type.
23    WrongContentType,
24
25    /// Could not read the body from the request.
26    IoError(IoError),
27
28    /// The limit to the number of bytes has been exceeded.
29    LimitExceeded,
30
31    /// The content-type encoding is not ASCII or UTF-8, or the body is not valid UTF-8.
32    NotUtf8,
33}
34
35impl From<IoError> for PlainTextError {
36    fn from(err: IoError) -> PlainTextError {
37        PlainTextError::IoError(err)
38    }
39}
40
41impl error::Error for PlainTextError {
42    #[inline]
43    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
44        match *self {
45            PlainTextError::IoError(ref e) => Some(e),
46            _ => None,
47        }
48    }
49}
50
51impl fmt::Display for PlainTextError {
52    #[inline]
53    fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
54        let description = match *self {
55            PlainTextError::BodyAlreadyExtracted => "the body of the request was already extracted",
56            PlainTextError::WrongContentType => "the request didn't have a plain text content type",
57            PlainTextError::IoError(_) => {
58                "could not read the body from the request, or could not execute the CGI program"
59            }
60            PlainTextError::LimitExceeded => "the limit to the number of bytes has been exceeded",
61            PlainTextError::NotUtf8 => {
62                "the content-type encoding is not ASCII or UTF-8, or the body is not valid UTF-8"
63            }
64        };
65
66        write!(fmt, "{}", description)
67    }
68}
69
70/// Read plain text data from the body of a request.
71///
72/// Returns an error if the content-type of the request is not text/plain. Only the UTF-8 encoding
73/// is supported. You will get an error if the client passed non-UTF8 data.
74///
75/// If the body of the request exceeds 1MB of data, an error is returned to prevent a malicious
76/// client from crashing the server. Use the `plain_text_body_with_limit` function to customize
77/// the limit.
78///
79/// # Example
80///
81/// ```
82/// # #[macro_use] extern crate rouille;
83/// # use rouille::{Request, Response};
84/// # fn main() {}
85/// fn route_handler(request: &Request) -> Response {
86///     let text = try_or_400!(rouille::input::plain_text_body(request));
87///     Response::text(format!("you sent: {}", text))
88/// }
89/// ```
90///
91#[inline]
92pub fn plain_text_body(request: &Request) -> Result<String, PlainTextError> {
93    plain_text_body_with_limit(request, 1024 * 1024)
94}
95
96/// Reads plain text data from the body of a request.
97///
98/// This does the same as `plain_text_body`, but with a customizable limit in bytes to how much
99/// data will be read from the request. If the limit is exceeded, a `LimitExceeded` error is
100/// returned.
101pub fn plain_text_body_with_limit(
102    request: &Request,
103    limit: usize,
104) -> Result<String, PlainTextError> {
105    // TODO: handle encoding ; return NotUtf8 if a non-utf8 charset is sent
106    // if no encoding is specified by the client, the default is `US-ASCII` which is compatible with UTF8
107
108    if let Some(header) = request.header("Content-Type") {
109        if !header.starts_with("text/plain") {
110            return Err(PlainTextError::WrongContentType);
111        }
112    } else {
113        return Err(PlainTextError::WrongContentType);
114    }
115
116    let body = match request.data() {
117        Some(b) => b,
118        None => return Err(PlainTextError::BodyAlreadyExtracted),
119    };
120
121    let mut out = Vec::new();
122    body.take(limit.saturating_add(1) as u64)
123        .read_to_end(&mut out)?;
124    if out.len() > limit {
125        return Err(PlainTextError::LimitExceeded);
126    }
127
128    let out = match String::from_utf8(out) {
129        Ok(o) => o,
130        Err(_) => return Err(PlainTextError::NotUtf8),
131    };
132
133    Ok(out)
134}
135
136#[cfg(test)]
137mod test {
138    use super::plain_text_body;
139    use super::plain_text_body_with_limit;
140    use super::PlainTextError;
141    use Request;
142
143    #[test]
144    fn ok() {
145        let request = Request::fake_http(
146            "GET",
147            "/",
148            vec![("Content-Type".to_owned(), "text/plain".to_owned())],
149            b"test".to_vec(),
150        );
151
152        match plain_text_body(&request) {
153            Ok(ref d) if d == "test" => (),
154            _ => panic!(),
155        }
156    }
157
158    #[test]
159    fn charset() {
160        let request = Request::fake_http(
161            "GET",
162            "/",
163            vec![(
164                "Content-Type".to_owned(),
165                "text/plain; charset=utf8".to_owned(),
166            )],
167            b"test".to_vec(),
168        );
169
170        match plain_text_body(&request) {
171            Ok(ref d) if d == "test" => (),
172            _ => panic!(),
173        }
174    }
175
176    #[test]
177    fn missing_content_type() {
178        let request = Request::fake_http("GET", "/", vec![], Vec::new());
179
180        match plain_text_body(&request) {
181            Err(PlainTextError::WrongContentType) => (),
182            _ => panic!(),
183        }
184    }
185
186    #[test]
187    fn wrong_content_type() {
188        let request = Request::fake_http(
189            "GET",
190            "/",
191            vec![("Content-Type".to_owned(), "text/html".to_owned())],
192            b"test".to_vec(),
193        );
194
195        match plain_text_body(&request) {
196            Err(PlainTextError::WrongContentType) => (),
197            _ => panic!(),
198        }
199    }
200
201    #[test]
202    fn body_twice() {
203        let request = Request::fake_http(
204            "GET",
205            "/",
206            vec![(
207                "Content-Type".to_owned(),
208                "text/plain; charset=utf8".to_owned(),
209            )],
210            b"test".to_vec(),
211        );
212
213        match plain_text_body(&request) {
214            Ok(ref d) if d == "test" => (),
215            _ => panic!(),
216        }
217
218        match plain_text_body(&request) {
219            Err(PlainTextError::BodyAlreadyExtracted) => (),
220            _ => panic!(),
221        }
222    }
223
224    #[test]
225    fn bytes_limit() {
226        let request = Request::fake_http(
227            "GET",
228            "/",
229            vec![("Content-Type".to_owned(), "text/plain".to_owned())],
230            b"test".to_vec(),
231        );
232
233        match plain_text_body_with_limit(&request, 2) {
234            Err(PlainTextError::LimitExceeded) => (),
235            _ => panic!(),
236        }
237    }
238
239    #[test]
240    fn exact_limit() {
241        let request = Request::fake_http(
242            "GET",
243            "/",
244            vec![("Content-Type".to_owned(), "text/plain".to_owned())],
245            b"test".to_vec(),
246        );
247
248        match plain_text_body_with_limit(&request, 4) {
249            Ok(ref d) if d == "test" => (),
250            _ => panic!(),
251        }
252    }
253
254    #[test]
255    fn non_utf8_body() {
256        let request = Request::fake_http(
257            "GET",
258            "/",
259            vec![(
260                "Content-Type".to_owned(),
261                "text/plain; charset=utf8".to_owned(),
262            )],
263            b"\xc3\x28".to_vec(),
264        );
265
266        match plain_text_body(&request) {
267            Err(PlainTextError::NotUtf8) => (),
268            _ => panic!(),
269        }
270    }
271
272    #[test]
273    #[ignore] // TODO: not implemented
274    fn non_utf8_encoding() {
275        let request = Request::fake_http(
276            "GET",
277            "/",
278            vec![(
279                "Content-Type".to_owned(),
280                "text/plain; charset=iso-8859-1".to_owned(),
281            )],
282            b"test".to_vec(),
283        );
284
285        match plain_text_body(&request) {
286            Err(PlainTextError::NotUtf8) => (),
287            _ => panic!(),
288        }
289    }
290}