rouille_ng/
content_encoding.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::str;
11use Request;
12use Response;
13
14/// Applies content encoding to the response.
15///
16/// Analyzes the `Accept-Encoding` header of the request. If one of the encodings is recognized and
17/// supported by rouille_ng, it adds a `Content-Encoding` header to the `Response` and encodes its
18/// body.
19///
20/// If the response already has a `Content-Encoding` header, this function is a no-op.
21/// If the response has a `Content-Type` header that isn't textual content, this function is a
22/// no-op.
23///
24/// The gzip encoding is supported only if you enable the `gzip` feature of rouille_ng (which is
25/// enabled by default).
26///
27/// # Example
28///
29/// ```rust
30/// use rouille_ng::content_encoding;
31/// use rouille_ng::Request;
32/// use rouille_ng::Response;
33///
34/// fn handle(request: &Request) -> Response {
35///     content_encoding::apply(request, Response::text("hello world"))
36/// }
37/// ```
38pub fn apply(request: &Request, response: Response) -> Response {
39    // Only text should be encoded. Otherwise just return.
40    if !response_is_text(&response) {
41        return response;
42    }
43
44    // If any of the response's headers is equal to `Content-Encoding`, ignore the function
45    // call and return immediately.
46    if response
47        .headers
48        .iter()
49        .any(|&(ref key, _)| key.eq_ignore_ascii_case("Content-Encoding"))
50    {
51        return response;
52    }
53
54    // Put the response in an Option for later.
55    let mut response = Some(response);
56
57    // Now let's get the list of content encodings accepted by the request.
58    // The list should be ordered from the most desired to the list desired.
59    // TODO: use input::priority_header_preferred instead
60    for encoding in accepted_content_encodings(request) {
61        // Try the brotli encoding.
62        if brotli(encoding, &mut response) {
63            return response.take().unwrap();
64        }
65
66        // Try the gzip encoding.
67        if gzip(encoding, &mut response) {
68            return response.take().unwrap();
69        }
70
71        // The identity encoding is always supported.
72        if encoding.eq_ignore_ascii_case("identity") {
73            return response.take().unwrap();
74        }
75    }
76
77    // No encoding accepted, don't do anything.
78    response.take().unwrap()
79}
80
81// Returns true if the Content-Type of the response is a type that should be encoded.
82// Since encoding is purely an optimisation, it's not a problem if the function sometimes has
83// false positives or false negatives.
84fn response_is_text(response: &Response) -> bool {
85    response.headers.iter().any(|&(ref key, ref value)| {
86        if !key.eq_ignore_ascii_case("Content-Type") {
87            return false;
88        }
89
90        // TODO: perform case-insensitive comparison
91        value.starts_with("text/")
92            || value.contains("javascript")
93            || value.contains("json")
94            || value.contains("xml")
95            || value.contains("font")
96    })
97}
98
99/// Returns an iterator of the list of content encodings accepted by the request.
100///
101/// # Example
102///
103/// ```
104/// use rouille_ng::{Request, Response};
105/// use rouille_ng::content_encoding;
106///
107/// fn handle(request: &Request) -> Response {
108///     for encoding in content_encoding::accepted_content_encodings(request) {
109///         // ...
110///     }
111///
112///     // ...
113/// # panic!()
114/// }
115/// ```
116pub fn accepted_content_encodings(request: &Request) -> AcceptedContentEncodingsIter {
117    let elems = request.header("Accept-Encoding").unwrap_or("").split(',');
118    AcceptedContentEncodingsIter { elements: elems }
119}
120
121/// Iterator to the list of content encodings accepted by a request.
122pub struct AcceptedContentEncodingsIter<'a> {
123    elements: str::Split<'a, char>,
124}
125
126impl<'a> Iterator for AcceptedContentEncodingsIter<'a> {
127    type Item = &'a str;
128
129    #[inline]
130    fn next(&mut self) -> Option<&'a str> {
131        loop {
132            match self.elements.next() {
133                None => return None,
134                Some(e) => {
135                    let e = e.trim();
136                    if !e.is_empty() {
137                        return Some(e);
138                    }
139                }
140            }
141        }
142    }
143
144    #[inline]
145    fn size_hint(&self) -> (usize, Option<usize>) {
146        let (_, max) = self.elements.size_hint();
147        (0, max)
148    }
149}
150
151#[cfg(feature = "gzip")]
152fn gzip(e: &str, response: &mut Option<Response>) -> bool {
153    use deflate::deflate_bytes_gzip;
154    use std::io;
155    use std::mem;
156    use ResponseBody;
157
158    if !e.eq_ignore_ascii_case("gzip") {
159        return false;
160    }
161
162    let response = response.as_mut().unwrap();
163    response
164        .headers
165        .push(("Content-Encoding".into(), "gzip".into()));
166    let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
167    let (mut raw_data, size) = previous_body.into_reader_and_size();
168    let mut src = match size {
169        Some(size) => Vec::with_capacity(size),
170        None => Vec::new(),
171    };
172    io::copy(&mut raw_data, &mut src).expect("Failed reading response body while gzipping");
173    let zipped = deflate_bytes_gzip(&src);
174    response.data = ResponseBody::from_data(zipped);
175    true
176}
177
178#[cfg(not(feature = "gzip"))]
179#[inline]
180fn gzip(e: &str, response: &mut Option<Response>) -> bool {
181    false
182}
183
184#[cfg(feature = "brotli")]
185fn brotli(e: &str, response: &mut Option<Response>) -> bool {
186    use brotli2::read::BrotliEncoder;
187    use std::mem;
188    use ResponseBody;
189
190    if !e.eq_ignore_ascii_case("br") {
191        return false;
192    }
193
194    let response = response.as_mut().unwrap();
195    response
196        .headers
197        .push(("Content-Encoding".into(), "br".into()));
198    let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
199    let (raw_data, _) = previous_body.into_reader_and_size();
200    response.data = ResponseBody::from_reader(BrotliEncoder::new(raw_data, 6));
201    true
202}
203
204#[cfg(not(feature = "brotli"))]
205#[inline]
206fn brotli(e: &str, response: &mut Option<Response>) -> bool {
207    false
208}
209
210#[cfg(test)]
211mod tests {
212    use content_encoding;
213    use Request;
214
215    #[test]
216    fn no_req_encodings() {
217        let request = Request::fake_http("GET", "/", vec![], vec![]);
218        assert_eq!(
219            content_encoding::accepted_content_encodings(&request).count(),
220            0
221        );
222    }
223
224    #[test]
225    fn empty_req_encodings() {
226        let request = {
227            let h = vec![("Accept-Encoding".to_owned(), "".to_owned())];
228            Request::fake_http("GET", "/", h, vec![])
229        };
230
231        assert_eq!(
232            content_encoding::accepted_content_encodings(&request).count(),
233            0
234        );
235    }
236
237    #[test]
238    fn one_req_encoding() {
239        let request = {
240            let h = vec![("Accept-Encoding".to_owned(), "foo".to_owned())];
241            Request::fake_http("GET", "/", h, vec![])
242        };
243
244        let mut list = content_encoding::accepted_content_encodings(&request);
245        assert_eq!(list.next().unwrap(), "foo");
246        assert_eq!(list.next(), None);
247    }
248
249    #[test]
250    fn multi_req_encoding() {
251        let request = {
252            let h = vec![("Accept-Encoding".to_owned(), "foo, bar".to_owned())];
253            Request::fake_http("GET", "/", h, vec![])
254        };
255
256        let mut list = content_encoding::accepted_content_encodings(&request);
257        assert_eq!(list.next().unwrap(), "foo");
258        assert_eq!(list.next().unwrap(), "bar");
259        assert_eq!(list.next(), None);
260    }
261
262    // TODO: more tests for encoding stuff
263}