blob: 5445249f71fd5a5b1d64a1aabf6516f8ba0e2e3e [file] [log] [blame]
// Copyright (c) 2016 The Rouille developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::error;
use std::fmt;
use std::io::Error as IoError;
use std::io::Read;
use Request;
/// Error that can happen when parsing the request body as plain text.
#[derive(Debug)]
pub enum PlainTextError {
/// Can't parse the body of the request because it was already extracted.
BodyAlreadyExtracted,
/// Wrong content type.
WrongContentType,
/// Could not read the body from the request.
IoError(IoError),
/// The limit to the number of bytes has been exceeded.
LimitExceeded,
/// The content-type encoding is not ASCII or UTF-8, or the body is not valid UTF-8.
NotUtf8,
}
impl From<IoError> for PlainTextError {
fn from(err: IoError) -> PlainTextError {
PlainTextError::IoError(err)
}
}
impl error::Error for PlainTextError {
#[inline]
fn description(&self) -> &str {
match *self {
PlainTextError::BodyAlreadyExtracted => {
"the body of the request was already extracted"
},
PlainTextError::WrongContentType => {
"the request didn't have a plain text content type"
},
PlainTextError::IoError(_) => {
"could not read the body from the request, or could not execute the CGI program"
},
PlainTextError::LimitExceeded => {
"the limit to the number of bytes has been exceeded"
},
PlainTextError::NotUtf8 => {
"the content-type encoding is not ASCII or UTF-8, or the body is not valid UTF-8"
},
}
}
#[inline]
fn cause(&self) -> Option<&error::Error> {
match *self {
PlainTextError::IoError(ref e) => Some(e),
_ => None
}
}
}
impl fmt::Display for PlainTextError {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(fmt, "{}", error::Error::description(self))
}
}
/// Read plain text data from the body of a request.
///
/// Returns an error if the content-type of the request is not text/plain. Only the UTF-8 encoding
/// is supported. You will get an error if the client passed non-UTF8 data.
///
/// If the body of the request exceeds 1MB of data, an error is returned to prevent a malicious
/// client from crashing the server. Use the `plain_text_body_with_limit` function to customize
/// the limit.
///
/// # Example
///
/// ```
/// # #[macro_use] extern crate rouille;
/// # use rouille::{Request, Response};
/// # fn main() {}
/// fn route_handler(request: &Request) -> Response {
/// let text = try_or_400!(rouille::input::plain_text_body(request));
/// Response::text(format!("you sent: {}", text))
/// }
/// ```
///
#[inline]
pub fn plain_text_body(request: &Request) -> Result<String, PlainTextError> {
plain_text_body_with_limit(request, 1024 * 1024)
}
/// Reads plain text data from the body of a request.
///
/// This does the same as `plain_text_body`, but with a customizable limit in bytes to how much
/// data will be read from the request. If the limit is exceeded, a `LimitExceeded` error is
/// returned.
pub fn plain_text_body_with_limit(request: &Request, limit: usize)
-> Result<String, PlainTextError>
{
// TODO: handle encoding ; return NotUtf8 if a non-utf8 charset is sent
// if no encoding is specified by the client, the default is `US-ASCII` which is compatible with UTF8
if let Some(header) = request.header("Content-Type") {
if !header.starts_with("text/plain") {
return Err(PlainTextError::WrongContentType);
}
} else {
return Err(PlainTextError::WrongContentType);
}
let body = match request.data() {
Some(b) => b,
None => return Err(PlainTextError::BodyAlreadyExtracted),
};
let mut out = Vec::new();
try!(body.take(limit.saturating_add(1) as u64).read_to_end(&mut out));
if out.len() > limit {
return Err(PlainTextError::LimitExceeded);
}
let out = match String::from_utf8(out) {
Ok(o) => o,
Err(_) => return Err(PlainTextError::NotUtf8),
};
Ok(out)
}
#[cfg(test)]
mod test {
use Request;
use super::plain_text_body;
use super::plain_text_body_with_limit;
use super::PlainTextError;
#[test]
fn ok() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain".to_owned())
], b"test".to_vec());
match plain_text_body(&request) {
Ok(ref d) if d == "test" => (),
_ => panic!()
}
}
#[test]
fn charset() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain; charset=utf8".to_owned())
], b"test".to_vec());
match plain_text_body(&request) {
Ok(ref d) if d == "test" => (),
_ => panic!()
}
}
#[test]
fn missing_content_type() {
let request = Request::fake_http("GET", "/", vec![], Vec::new());
match plain_text_body(&request) {
Err(PlainTextError::WrongContentType) => (),
_ => panic!()
}
}
#[test]
fn wrong_content_type() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/html".to_owned())
], b"test".to_vec());
match plain_text_body(&request) {
Err(PlainTextError::WrongContentType) => (),
_ => panic!()
}
}
#[test]
fn body_twice() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain; charset=utf8".to_owned())
], b"test".to_vec());
match plain_text_body(&request) {
Ok(ref d) if d == "test" => (),
_ => panic!()
}
match plain_text_body(&request) {
Err(PlainTextError::BodyAlreadyExtracted) => (),
_ => panic!()
}
}
#[test]
fn bytes_limit() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain".to_owned())
], b"test".to_vec());
match plain_text_body_with_limit(&request, 2) {
Err(PlainTextError::LimitExceeded) => (),
_ => panic!()
}
}
#[test]
fn exact_limit() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain".to_owned())
], b"test".to_vec());
match plain_text_body_with_limit(&request, 4) {
Ok(ref d) if d == "test" => (),
_ => panic!()
}
}
#[test]
fn non_utf8_body() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain; charset=utf8".to_owned())
], b"\xc3\x28".to_vec());
match plain_text_body(&request) {
Err(PlainTextError::NotUtf8) => (),
_ => panic!()
}
}
#[test]
#[ignore] // TODO: not implemented
fn non_utf8_encoding() {
let request = Request::fake_http("GET", "/", vec![
("Content-Type".to_owned(), "text/plain; charset=iso-8859-1".to_owned())
], b"test".to_vec());
match plain_text_body(&request) {
Err(PlainTextError::NotUtf8) => (),
_ => panic!()
}
}
}