rouille/
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
10//! Apply content encodings (such as gzip compression) to the response.
11//!
12//! This module provides access to the content encodings supported by a request as well as
13//! a function to automatically apply common content encodings to a response.
14//! # Basic example
15//!
16//! Here is a basic example showing how to use content encodings:
17//!
18//! ```
19//! use rouille::Request;
20//! use rouille::Response;
21//! use rouille::content_encoding;
22//!
23//! fn handle_request(request: &Request) -> Response {
24//!     let response = Response::text("Hello world");
25//!     content_encoding::apply(&request, response)
26//! }
27//! ```
28use input;
29use Request;
30use Response;
31
32/// Applies content encoding to the response.
33///
34/// Analyzes the `Accept-Encoding` header of the request. If one of the encodings is recognized and
35/// supported by rouille, it adds a `Content-Encoding` header to the `Response` and encodes its
36/// body.
37///
38/// If the response already has a `Content-Encoding` header, this function is a no-op.
39/// If the response has a `Content-Type` header that isn't textual content, this function is a
40/// no-op.
41///
42/// The gzip encoding is supported only if you enable the `gzip` feature of rouille (which is
43/// enabled by default).
44///
45/// # Example
46///
47/// ```rust
48/// use rouille::content_encoding;
49/// use rouille::Request;
50/// use rouille::Response;
51///
52/// fn handle(request: &Request) -> Response {
53///     content_encoding::apply(request, Response::text("hello world"))
54/// }
55/// ```
56pub fn apply(request: &Request, mut response: Response) -> Response {
57    // Only text should be encoded. Otherwise just return.
58    if !response_is_text(&response) {
59        return response;
60    }
61
62    // If any of the response's headers is equal to `Content-Encoding`, ignore the function
63    // call and return immediately.
64    if response
65        .headers
66        .iter()
67        .any(|&(ref key, _)| key.eq_ignore_ascii_case("Content-Encoding"))
68    {
69        return response;
70    }
71
72    // Now let's get the list of content encodings accepted by the request.
73    // The list should be ordered from the most desired to the least desired.
74    let encoding_preference = ["br", "gzip", "x-gzip", "identity"];
75    let accept_encoding_header = request.header("Accept-Encoding").unwrap_or("");
76    if let Some(preferred_index) = input::priority_header_preferred(
77        accept_encoding_header,
78        encoding_preference.iter().cloned(),
79    ) {
80        match encoding_preference[preferred_index] {
81            "br" => brotli(&mut response),
82            "gzip" | "x-gzip" => gzip(&mut response),
83            _ => (),
84        }
85    }
86
87    response
88}
89
90// Returns true if the Content-Type of the response is a type that should be encoded.
91// Since encoding is purely an optimization, it's not a problem if the function sometimes has
92// false positives or false negatives.
93fn response_is_text(response: &Response) -> bool {
94    response.headers.iter().any(|&(ref key, ref value)| {
95        if !key.eq_ignore_ascii_case("Content-Type") {
96            return false;
97        }
98
99        let content_type = value.to_lowercase();
100        content_type.starts_with("text/")
101            || content_type.contains("javascript")
102            || content_type.contains("json")
103            || content_type.contains("xml")
104            || content_type.contains("font")
105    })
106}
107
108#[cfg(feature = "gzip")]
109fn gzip(response: &mut Response) {
110    use deflate::deflate_bytes_gzip;
111    use std::io;
112    use std::mem;
113    use ResponseBody;
114
115    response
116        .headers
117        .push(("Content-Encoding".into(), "gzip".into()));
118    let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
119    let (mut raw_data, size) = previous_body.into_reader_and_size();
120    let mut src = match size {
121        Some(size) => Vec::with_capacity(size),
122        None => Vec::new(),
123    };
124    io::copy(&mut raw_data, &mut src).expect("Failed reading response body while gzipping");
125    let zipped = deflate_bytes_gzip(&src);
126    response.data = ResponseBody::from_data(zipped);
127}
128
129#[cfg(not(feature = "gzip"))]
130#[inline]
131fn gzip(response: &mut Response) {}
132
133#[cfg(feature = "brotli")]
134fn brotli(response: &mut Response) {
135    use brotli::enc::reader::CompressorReader;
136    use std::mem;
137    use ResponseBody;
138
139    response
140        .headers
141        .push(("Content-Encoding".into(), "br".into()));
142    let previous_body = mem::replace(&mut response.data, ResponseBody::empty());
143    let (raw_data, _) = previous_body.into_reader_and_size();
144    // Using default Brotli parameters: 0 buffer_size == 4096, compression level 6, lgwin == 22
145    response.data = ResponseBody::from_reader(CompressorReader::new(raw_data, 0, 6, 22));
146}
147
148#[cfg(not(feature = "brotli"))]
149#[inline]
150fn brotli(response: &mut Response) {}
151
152#[cfg(test)]
153mod tests {
154    use content_encoding;
155    use Request;
156    use Response;
157
158    // TODO: more tests for encoding stuff
159    #[test]
160    fn text_response() {
161        assert!(content_encoding::response_is_text(&Response::text("")));
162    }
163
164    #[test]
165    fn non_text_response() {
166        assert!(!content_encoding::response_is_text(&Response::from_data(
167            "image/jpeg",
168            ""
169        )));
170    }
171
172    #[test]
173    fn no_req_encodings() {
174        let request = Request::fake_http("GET", "/", vec![], vec![]);
175        let response = Response::html("<p>Hello world</p>");
176        let encoded_response = content_encoding::apply(&request, response);
177        assert!(!encoded_response
178            .headers
179            .iter()
180            .any(|(header_name, _)| header_name == "Content-Encoding")); // No Content-Encoding header
181        let mut encoded_content = vec![];
182        encoded_response
183            .data
184            .into_reader_and_size()
185            .0
186            .read_to_end(&mut encoded_content)
187            .unwrap();
188        assert_eq!(
189            String::from_utf8(encoded_content).unwrap(),
190            "<p>Hello world</p>"
191        ); // No encoding applied
192    }
193
194    #[test]
195    fn empty_req_encodings() {
196        let request = {
197            let h = vec![("Accept-Encoding".to_owned(), "".to_owned())];
198            Request::fake_http("GET", "/", h, vec![])
199        };
200        let response = Response::html("<p>Hello world</p>");
201
202        let encoded_response = content_encoding::apply(&request, response);
203        assert!(!encoded_response
204            .headers
205            .iter()
206            .any(|(header_name, _)| header_name == "Content-Encoding")); // No Content-Encoding header
207        let mut encoded_content = vec![];
208        encoded_response
209            .data
210            .into_reader_and_size()
211            .0
212            .read_to_end(&mut encoded_content)
213            .unwrap();
214        assert_eq!(
215            String::from_utf8(encoded_content).unwrap(),
216            "<p>Hello world</p>"
217        ); // No encoding applied
218    }
219
220    #[test]
221    fn multi_req_encoding() {
222        let request = {
223            let h = vec![("Accept-Encoding".to_owned(), "foo".to_owned())];
224            Request::fake_http("GET", "/", h, vec![])
225        };
226        let response = Response::html("<p>Hello world</p>");
227
228        let encoded_response = content_encoding::apply(&request, response);
229        assert!(!encoded_response
230            .headers
231            .iter()
232            .any(|(header_name, _)| header_name == "Content-Encoding")); // No Content-Encoding header
233        let mut encoded_content = vec![];
234        encoded_response
235            .data
236            .into_reader_and_size()
237            .0
238            .read_to_end(&mut encoded_content)
239            .unwrap();
240        assert_eq!(
241            String::from_utf8(encoded_content).unwrap(),
242            "<p>Hello world</p>"
243        ); // No encoding applied
244    }
245
246    #[test]
247    fn unknown_req_encoding() {
248        let request = {
249            let h = vec![("Accept-Encoding".to_owned(), "x-gzip, br".to_owned())];
250            Request::fake_http("GET", "/", h, vec![])
251        };
252        let response = Response::html("<p>Hello world</p>");
253
254        let encoded_response = content_encoding::apply(&request, response);
255        assert!(encoded_response
256            .headers
257            .contains(&("Content-Encoding".into(), "br".into()))); // Brotli Content-Encoding header
258    }
259
260    #[test]
261    fn brotli_encoding() {
262        let request = {
263            let h = vec![("Accept-Encoding".to_owned(), "br".to_owned())];
264            Request::fake_http("GET", "/", h, vec![])
265        };
266        let response = Response::html(
267            "<html><head><title>Hello world</title><body><p>Hello world</p></body></html>",
268        );
269
270        let encoded_response = content_encoding::apply(&request, response);
271        assert!(encoded_response
272            .headers
273            .contains(&("Content-Encoding".into(), "br".into()))); // Brotli Content-Encoding header
274        let mut encoded_content = vec![];
275        encoded_response
276            .data
277            .into_reader_and_size()
278            .0
279            .read_to_end(&mut encoded_content)
280            .unwrap();
281        assert_eq!(
282            encoded_content,
283            vec![
284                27, 75, 0, 0, 4, 28, 114, 164, 129, 5, 210, 206, 25, 30, 90, 114, 224, 114, 73,
285                109, 45, 196, 23, 126, 240, 144, 77, 40, 26, 211, 228, 67, 73, 40, 236, 55, 101,
286                254, 127, 147, 194, 129, 132, 65, 130, 120, 152, 249, 68, 56, 93, 2
287            ]
288        ); // Applied proper Brotli encoding
289    }
290
291    #[test]
292    fn gzip_encoding() {
293        let request = {
294            let h = vec![("Accept-Encoding".to_owned(), "gzip".to_owned())];
295            Request::fake_http("GET", "/", h, vec![])
296        };
297        let response = Response::html(
298            "<html><head><title>Hello world</title><body><p>Hello world</p></body></html>",
299        );
300
301        let encoded_response = content_encoding::apply(&request, response);
302        assert!(encoded_response
303            .headers
304            .contains(&("Content-Encoding".into(), "gzip".into()))); // gzip Content-Encoding header
305        let mut encoded_content = vec![];
306        encoded_response
307            .data
308            .into_reader_and_size()
309            .0
310            .read_to_end(&mut encoded_content)
311            .unwrap();
312
313        // The 10-byte Gzip header contains an OS ID and a 4 byte timestamp
314        // which are not stable, so we skip them in this comparison. Doing a
315        // literal compare here is slightly silly, but the `deflate` crate has
316        // no public decompressor functions for us to test a round-trip
317        assert_eq!(
318            encoded_content[10..],
319            vec![
320                179, 201, 40, 201, 205, 177, 179, 201, 72, 77, 76, 177, 179, 41, 201, 44, 201, 73,
321                181, 243, 72, 205, 201, 201, 87, 40, 207, 47, 202, 73, 177, 209, 135, 8, 217, 36,
322                229, 167, 84, 218, 217, 20, 160, 202, 21, 216, 217, 232, 67, 36, 244, 193, 166, 0,
323                0, 202, 239, 44, 120, 76, 0, 0, 0
324            ]
325        ); // Applied proper gzip encoding
326    }
327}