[go: up one dir, main page]

rouille 3.3.0

High-level idiomatic web framework.
Documentation
// 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.

//! Apply content encodings (such as gzip compression) to the response.
//!
//! This module provides access to the content encodings supported by a request as well as
//! a function to automatically apply common content encodings to a response.
//! # Basic example
//!
//! Here is a basic example showing how to use content encodings:
//!
//! ```
//! use rouille::Request;
//! use rouille::Response;
//! use rouille::content_encoding;
//!
//! fn handle_request(request: &Request) -> Response {
//!     let mut text = String::new();
//!     for encoding in content_encoding::accepted_content_encodings(request) {
//!         text.push_str(encoding);
//!         text.push('\n');
//!     }
//!     let response = Response::text(&text);
//!     content_encoding::apply(&request, response)
//! }
//! ```
use std::str;
use Request;
use Response;

/// 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 optimization, 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 deflate::deflate_bytes_gzip;
    use std::io;
    use std::mem;
    use ResponseBody;

    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 brotli2::read::BrotliEncoder;
    use std::mem;
    use ResponseBody;

    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 content_encoding;
    use Request;

    #[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
}