blob: 325f4ad735592993a655bba4657ed37a2b0e39af [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::str;
use Request;
use Response;
// The AsciiExt import is needed for Rust older than 1.23.0. These two lines can
// be removed when supporting older Rust is no longer needed.
#[allow(unused_imports)]
use std::ascii::AsciiExt;
/// Applies content encoding to the response.
///
/// Analyzes the `Accept-Encoding` header of the request. If one of the encodings is recognized and
/// supported by rouille, it adds a `Content-Encoding` header to the `Response` and encodes its
/// body.
///
/// If the response already has a `Content-Encoding` header, this function is a no-op.
/// If the response has a `Content-Type` header that isn't textual content, this function is a
/// no-op.
///
/// The gzip encoding is supported only if you enable the `gzip` feature of rouille (which is
/// enabled by default).
///
/// # Example
///
/// ```rust
/// use rouille::content_encoding;
/// use rouille::Request;
/// use rouille::Response;
///
/// fn handle(request: &Request) -> Response {
/// content_encoding::apply(request, Response::text("hello world"))
/// }
/// ```
pub fn apply(request: &Request, response: Response) -> Response {
// Only text should be encoded. Otherwise just return.
if !response_is_text(&response) {
return response;
}
// If any of the response's headers is equal to `Content-Encoding`, ignore the function
// call and return immediately.
if response.headers.iter().any(|&(ref key, _)| key.eq_ignore_ascii_case("Content-Encoding")) {
return response;
}
// Put the response in an Option for later.
let mut response = Some(response);
// Now let's get the list of content encodings accepted by the request.
// The list should be ordered from the most desired to the list desired.
// TODO: use input::priority_header_preferred instead
for encoding in accepted_content_encodings(request) {
// Try the brotli encoding.
if brotli(encoding, &mut response) {
return response.take().unwrap();
}
// Try the gzip encoding.
if gzip(encoding, &mut response) {
return response.take().unwrap();
}
// The identity encoding is always supported.
if encoding.eq_ignore_ascii_case("identity") {
return response.take().unwrap();
}
}
// No encoding accepted, don't do anything.
response.take().unwrap()
}
// Returns true if the Content-Type of the response is a type that should be encoded.
// Since encoding is purely an optimisation, it's not a problem if the function sometimes has
// false positives or false negatives.
fn response_is_text(response: &Response) -> bool {
response.headers.iter().any(|&(ref key, ref value)| {
if !key.eq_ignore_ascii_case("Content-Type") {
return false;
}
// TODO: perform case-insensitive comparison
value.starts_with("text/") || value.contains("javascript") || value.contains("json") ||
value.contains("xml") || value.contains("font")
})
}
/// Returns an iterator of the list of content encodings accepted by the request.
///
/// # Example
///
/// ```
/// use rouille::{Request, Response};
/// use rouille::content_encoding;
///
/// fn handle(request: &Request) -> Response {
/// for encoding in content_encoding::accepted_content_encodings(request) {
/// // ...
/// }
///
/// // ...
/// # panic!()
/// }
/// ```
pub fn accepted_content_encodings(request: &Request) -> AcceptedContentEncodingsIter {
let elems = request.header("Accept-Encoding").unwrap_or("").split(',');
AcceptedContentEncodingsIter { elements: elems }
}
/// Iterator to the list of content encodings accepted by a request.
pub struct AcceptedContentEncodingsIter<'a> {
elements: str::Split<'a, char>
}
impl<'a> Iterator for AcceptedContentEncodingsIter<'a> {
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<&'a str> {
loop {
match self.elements.next() {
None => return None,
Some(e) => {
let e = e.trim();
if !e.is_empty() {
return Some(e);
}
}
}
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let (_, max) = self.elements.size_hint();
(0, max)
}
}
#[cfg(feature = "gzip")]
fn gzip(e: &str, response: &mut Option<Response>) -> bool {
use ResponseBody;
use std::mem;
use std::io;
use deflate::deflate_bytes_gzip;
if !e.eq_ignore_ascii_case("gzip") {
return false;
}
let response = response.as_mut().unwrap();
response.headers.push(("Content-Encoding".into(), "gzip".into()));
let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
let (mut raw_data, size) = previous_body.into_reader_and_size();
let mut src = match size {
Some(size) => Vec::with_capacity(size),
None => Vec::new(),
};
io::copy(&mut raw_data, &mut src).expect("Failed reading response body while gzipping");
let zipped = deflate_bytes_gzip(&src);
response.data = ResponseBody::from_data(zipped);
true
}
#[cfg(not(feature = "gzip"))]
#[inline]
fn gzip(e: &str, response: &mut Option<Response>) -> bool {
false
}
#[cfg(feature = "brotli")]
fn brotli(e: &str, response: &mut Option<Response>) -> bool {
use ResponseBody;
use std::mem;
use brotli2::read::BrotliEncoder;
if !e.eq_ignore_ascii_case("br") {
return false;
}
let response = response.as_mut().unwrap();
response.headers.push(("Content-Encoding".into(), "br".into()));
let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
let (raw_data, _) = previous_body.into_reader_and_size();
response.data = ResponseBody::from_reader(BrotliEncoder::new(raw_data, 6));
true
}
#[cfg(not(feature = "brotli"))]
#[inline]
fn brotli(e: &str, response: &mut Option<Response>) -> bool {
false
}
#[cfg(test)]
mod tests {
use Request;
use content_encoding;
#[test]
fn no_req_encodings() {
let request = Request::fake_http("GET", "/", vec![], vec![]);
assert_eq!(content_encoding::accepted_content_encodings(&request).count(), 0);
}
#[test]
fn empty_req_encodings() {
let request = {
let h = vec![("Accept-Encoding".to_owned(), "".to_owned())];
Request::fake_http("GET", "/", h, vec![])
};
assert_eq!(content_encoding::accepted_content_encodings(&request).count(), 0);
}
#[test]
fn one_req_encoding() {
let request = {
let h = vec![("Accept-Encoding".to_owned(), "foo".to_owned())];
Request::fake_http("GET", "/", h, vec![])
};
let mut list = content_encoding::accepted_content_encodings(&request);
assert_eq!(list.next().unwrap(), "foo");
assert_eq!(list.next(), None);
}
#[test]
fn multi_req_encoding() {
let request = {
let h = vec![("Accept-Encoding".to_owned(), "foo, bar".to_owned())];
Request::fake_http("GET", "/", h, vec![])
};
let mut list = content_encoding::accepted_content_encodings(&request);
assert_eq!(list.next().unwrap(), "foo");
assert_eq!(list.next().unwrap(), "bar");
assert_eq!(list.next(), None);
}
// TODO: more tests for encoding stuff
}