mirror of https://github.com/rwf2/Rocket.git
Add 'compression' contrib module.
This commit is contained in:
parent
3c51a2f401
commit
6a55aa7253
|
@ -23,6 +23,9 @@ tera_templates = ["tera", "templates"]
|
||||||
handlebars_templates = ["handlebars", "templates"]
|
handlebars_templates = ["handlebars", "templates"]
|
||||||
helmet = ["time"]
|
helmet = ["time"]
|
||||||
serve = []
|
serve = []
|
||||||
|
compression = ["brotli_compression", "gzip_compression"]
|
||||||
|
brotli_compression = ["brotli"]
|
||||||
|
gzip_compression = ["flate2"]
|
||||||
|
|
||||||
# The barage of user-facing database features.
|
# The barage of user-facing database features.
|
||||||
diesel_sqlite_pool = ["databases", "diesel/sqlite", "diesel/r2d2"]
|
diesel_sqlite_pool = ["databases", "diesel/sqlite", "diesel/r2d2"]
|
||||||
|
@ -76,6 +79,10 @@ r2d2-memcache = { version = "0.3", optional = true }
|
||||||
# SpaceHelmet dependencies
|
# SpaceHelmet dependencies
|
||||||
time = { version = "0.1.40", optional = true }
|
time = { version = "0.1.40", optional = true }
|
||||||
|
|
||||||
|
# Compression dependencies
|
||||||
|
brotli = { version = "2.5", optional = true }
|
||||||
|
flate2 = { version = "1.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(debug_assertions)'.dependencies]
|
[target.'cfg(debug_assertions)'.dependencies]
|
||||||
notify = { version = "^4.0.6" }
|
notify = { version = "^4.0.6" }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
//! Automatic response compression.
|
||||||
|
//!
|
||||||
|
//! See the [`Compression`](compression::fairing::Compression) type for further
|
||||||
|
//! details.
|
||||||
|
|
||||||
|
use rocket::config::{ConfigError, Value};
|
||||||
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
|
use rocket::http::MediaType;
|
||||||
|
use rocket::Rocket;
|
||||||
|
use rocket::{Request, Response};
|
||||||
|
|
||||||
|
crate use super::CompressionUtils;
|
||||||
|
|
||||||
|
crate struct Context {
|
||||||
|
crate exclusions: Vec<MediaType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
crate fn new() -> Context {
|
||||||
|
Context {
|
||||||
|
exclusions: vec![
|
||||||
|
MediaType::parse_flexible("application/gzip").unwrap(),
|
||||||
|
MediaType::parse_flexible("application/zip").unwrap(),
|
||||||
|
MediaType::parse_flexible("image/*").unwrap(),
|
||||||
|
MediaType::parse_flexible("video/*").unwrap(),
|
||||||
|
MediaType::parse_flexible("application/wasm").unwrap(),
|
||||||
|
MediaType::parse_flexible("application/octet-stream").unwrap(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate fn with_exclusions(excls: Vec<MediaType>) -> Context {
|
||||||
|
Context { exclusions: excls }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Compression type implements brotli and gzip compression for responses in
|
||||||
|
/// accordance with the Accept-Encoding header. If accepted, brotli compression
|
||||||
|
/// is preferred over gzip.
|
||||||
|
///
|
||||||
|
/// In the brotli compression mode (using the
|
||||||
|
/// [rust-brotli](https://github.com/dropbox/rust-brotli) crate), quality is set
|
||||||
|
/// to 2 in order to achieve fast compression with a compression ratio similar
|
||||||
|
/// to gzip. When appropriate, brotli's text and font compression modes are
|
||||||
|
/// used.
|
||||||
|
///
|
||||||
|
/// In the gzip compression mode (using the
|
||||||
|
/// [flate2](https://github.com/alexcrichton/flate2-rs) crate), quality is set
|
||||||
|
/// to the default (9) in order to have good compression ratio.
|
||||||
|
///
|
||||||
|
/// This fairing does not compress responses that already have a
|
||||||
|
/// `Content-Encoding` header.
|
||||||
|
///
|
||||||
|
/// This fairing ignores the responses with a `Content-Type` matching any of
|
||||||
|
/// the following default types:
|
||||||
|
///
|
||||||
|
/// - application/gzip
|
||||||
|
/// - application/brotli
|
||||||
|
/// - image/*
|
||||||
|
/// - video/*
|
||||||
|
/// - application/wasm
|
||||||
|
/// - application/octet-stream
|
||||||
|
///
|
||||||
|
/// The excluded types can be changed changing the `compress.exclude` Rocket
|
||||||
|
/// configuration property.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
/// To use, add the `brotli_compression` feature, the `gzip_compression`
|
||||||
|
/// feature, or the `compression` feature (to enable both algorithms) to the
|
||||||
|
/// `rocket_contrib` dependencies section of your `Cargo.toml`:
|
||||||
|
///
|
||||||
|
/// ```toml,ignore
|
||||||
|
/// [dependencies.rocket_contrib]
|
||||||
|
/// version = "*"
|
||||||
|
/// default-features = false
|
||||||
|
/// features = ["compression"]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Then, ensure that the compression [fairing](/rocket/fairing/) is attached to
|
||||||
|
/// your Rocket application:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// extern crate rocket;
|
||||||
|
/// extern crate rocket_contrib;
|
||||||
|
///
|
||||||
|
/// use rocket_contrib::compression::Compression;
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// rocket::ignite()
|
||||||
|
/// // ...
|
||||||
|
/// .attach(Compression::fairing())
|
||||||
|
/// // ...
|
||||||
|
/// # ;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Compression(());
|
||||||
|
|
||||||
|
impl Compression {
|
||||||
|
/// Returns a fairing that compresses outgoing requests.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// To attach this fairing, simply call `attach` on the application's
|
||||||
|
/// `Rocket` instance with `Compression::fairing()`:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// extern crate rocket;
|
||||||
|
/// extern crate rocket_contrib;
|
||||||
|
///
|
||||||
|
/// use rocket_contrib::compression::Compression;
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// rocket::ignite()
|
||||||
|
/// // ...
|
||||||
|
/// .attach(Compression::fairing())
|
||||||
|
/// // ...
|
||||||
|
/// # ;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn fairing() -> Compression {
|
||||||
|
Compression { 0: () }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fairing for Compression {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Response compression",
|
||||||
|
kind: Kind::Attach | Kind::Response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
|
||||||
|
let mut ctxt = Context::new();
|
||||||
|
match rocket.config().get_table("compress").and_then(|t| {
|
||||||
|
t.get("exclude")
|
||||||
|
.ok_or(ConfigError::Missing(String::from("exclude")))
|
||||||
|
}) {
|
||||||
|
Ok(excls) => match excls.as_array() {
|
||||||
|
Some(excls) => {
|
||||||
|
let mut error = false;
|
||||||
|
let mut exclusions_vec = Vec::with_capacity(excls.len());
|
||||||
|
for e in excls {
|
||||||
|
match e {
|
||||||
|
Value::String(s) => match MediaType::parse_flexible(s) {
|
||||||
|
Some(media_type) => exclusions_vec.push(media_type),
|
||||||
|
None => {
|
||||||
|
error = true;
|
||||||
|
warn_!(
|
||||||
|
"Exclusions must be valid content types, using default compression exclusions '{:?}'",
|
||||||
|
ctxt.exclusions
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
error = true;
|
||||||
|
warn_!(
|
||||||
|
"Exclusions must be strings, using default compression exclusions '{:?}'",
|
||||||
|
ctxt.exclusions
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !error {
|
||||||
|
ctxt = Context::with_exclusions(exclusions_vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn_!(
|
||||||
|
"Exclusions must be an array of strings, using default compression exclusions '{:?}'",
|
||||||
|
ctxt.exclusions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(ConfigError::Missing(_)) => { /* ignore missing */ }
|
||||||
|
Err(e) => {
|
||||||
|
e.pretty_print();
|
||||||
|
warn_!(
|
||||||
|
"Using default compression exclusions '{:?}'",
|
||||||
|
ctxt.exclusions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(rocket.manage(ctxt))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_response(&self, request: &Request, response: &mut Response) {
|
||||||
|
CompressionUtils::compress_response(request, response, true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
//! `Compression` fairing and `Compressed` responder to automatically and
|
||||||
|
//! on demand respectively compressing responses.
|
||||||
|
mod fairing;
|
||||||
|
mod responder;
|
||||||
|
|
||||||
|
pub use self::fairing::Compression;
|
||||||
|
pub use self::responder::Compressed;
|
||||||
|
|
||||||
|
crate use self::fairing::Context;
|
||||||
|
use rocket::http::hyper::header::{ContentEncoding, Encoding};
|
||||||
|
use rocket::{Request, Response};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
#[cfg(feature = "brotli_compression")]
|
||||||
|
use brotli::enc::backward_references::BrotliEncoderMode;
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip_compression")]
|
||||||
|
use flate2::read::GzEncoder;
|
||||||
|
|
||||||
|
crate struct CompressionUtils;
|
||||||
|
|
||||||
|
impl CompressionUtils {
|
||||||
|
fn accepts_encoding(request: &Request, encoding: &str) -> bool {
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.get("Accept-Encoding")
|
||||||
|
.flat_map(|accept| accept.split(","))
|
||||||
|
.map(|accept| accept.trim())
|
||||||
|
.any(|accept| accept == encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn already_encoded(response: &Response) -> bool {
|
||||||
|
response.headers().get("Content-Encoding").next().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_body_and_encoding<'r, B: Read + 'r>(
|
||||||
|
response: &mut Response<'r>,
|
||||||
|
body: B,
|
||||||
|
encoding: Encoding,
|
||||||
|
) {
|
||||||
|
response.set_header(ContentEncoding(vec![encoding]));
|
||||||
|
response.set_streamed_body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_encoding(
|
||||||
|
content_type: &Option<rocket::http::ContentType>,
|
||||||
|
context: &rocket::State<Context>,
|
||||||
|
) -> bool {
|
||||||
|
match content_type {
|
||||||
|
Some(content_type) => context.exclusions.iter().any(|exc_media_type| {
|
||||||
|
if exc_media_type.sub() == "*" {
|
||||||
|
*exc_media_type.top() == *content_type.top()
|
||||||
|
} else {
|
||||||
|
*exc_media_type == *content_type.media_type()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compress_response(request: &Request, response: &mut Response, respect_excludes: bool) {
|
||||||
|
if CompressionUtils::already_encoded(response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_type = response.content_type();
|
||||||
|
|
||||||
|
if respect_excludes {
|
||||||
|
let context = request
|
||||||
|
.guard::<::rocket::State<Context>>()
|
||||||
|
.expect("Compression Context registered in on_attach");
|
||||||
|
|
||||||
|
if CompressionUtils::skip_encoding(&content_type, &context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression is done when the request accepts brotli or gzip encoding
|
||||||
|
// and the corresponding feature is enabled
|
||||||
|
if cfg!(feature = "brotli_compression") && CompressionUtils::accepts_encoding(request, "br")
|
||||||
|
{
|
||||||
|
if let Some(plain) = response.take_body() {
|
||||||
|
let content_type_top = content_type.as_ref().map(|ct| ct.top());
|
||||||
|
let mut params = brotli::enc::BrotliEncoderInitParams();
|
||||||
|
params.quality = 2;
|
||||||
|
if content_type_top == Some("text".into()) {
|
||||||
|
params.mode = BrotliEncoderMode::BROTLI_MODE_TEXT;
|
||||||
|
} else if content_type_top == Some("font".into()) {
|
||||||
|
params.mode = BrotliEncoderMode::BROTLI_MODE_FONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let compressor =
|
||||||
|
brotli::CompressorReader::with_params(plain.into_inner(), 4096, ¶ms);
|
||||||
|
|
||||||
|
CompressionUtils::set_body_and_encoding(
|
||||||
|
response,
|
||||||
|
compressor,
|
||||||
|
Encoding::EncodingExt("br".into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if cfg!(feature = "gzip_compression")
|
||||||
|
&& CompressionUtils::accepts_encoding(request, "gzip")
|
||||||
|
{
|
||||||
|
if let Some(plain) = response.take_body() {
|
||||||
|
let compressor = GzEncoder::new(plain.into_inner(), flate2::Compression::default());
|
||||||
|
|
||||||
|
CompressionUtils::set_body_and_encoding(response, compressor, Encoding::Gzip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
//! Response on demand compression.
|
||||||
|
//!
|
||||||
|
//! See the [`Compression`](compression::responder::Compressed) type for
|
||||||
|
//! further details.
|
||||||
|
|
||||||
|
use rocket::response::{self, Responder, Response};
|
||||||
|
use rocket::Request;
|
||||||
|
|
||||||
|
crate use super::CompressionUtils;
|
||||||
|
|
||||||
|
/// Compress a `Responder` response ignoring the compression exclusions.
|
||||||
|
///
|
||||||
|
/// Delegates the remainder of the response to the wrapped `Responder`.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
/// To use, add the `brotli_compression` feature, the `gzip_compression`
|
||||||
|
/// feature, or the `compression` feature (to enable both algorithms) to the
|
||||||
|
/// `rocket_contrib` dependencies section of your `Cargo.toml`:
|
||||||
|
///
|
||||||
|
/// ```toml,ignore
|
||||||
|
/// [dependencies.rocket_contrib]
|
||||||
|
/// version = "*"
|
||||||
|
/// default-features = false
|
||||||
|
/// features = ["compression"]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Then, compress the desired response wrapping a `Responder` inside
|
||||||
|
/// `Compressed`:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rocket_contrib::compression::Compressed;
|
||||||
|
///
|
||||||
|
/// # #[allow(unused_variables)]
|
||||||
|
/// let response = Compressed("Hi.");
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Compressed<R>(pub R);
|
||||||
|
|
||||||
|
impl<'r, R: Responder<'r>> Compressed<R> {
|
||||||
|
pub fn new(response: R) -> Compressed<R> {
|
||||||
|
Compressed { 0: response }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r, R: Responder<'r>> Responder<'r> for Compressed<R> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn respond_to(self, request: &Request) -> response::Result<'r> {
|
||||||
|
let mut response = Response::build()
|
||||||
|
.merge(self.0.respond_to(request)?)
|
||||||
|
.finalize();
|
||||||
|
|
||||||
|
CompressionUtils::compress_response(request, &mut response, false);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,3 +54,12 @@
|
||||||
|
|
||||||
#[cfg(feature="databases")] extern crate rocket_contrib_codegen;
|
#[cfg(feature="databases")] extern crate rocket_contrib_codegen;
|
||||||
#[cfg(feature="databases")] #[doc(hidden)] pub use rocket_contrib_codegen::*;
|
#[cfg(feature="databases")] #[doc(hidden)] pub use rocket_contrib_codegen::*;
|
||||||
|
|
||||||
|
#[cfg(any(feature="brotli_compression", feature="gzip_compression"))]
|
||||||
|
pub mod compression;
|
||||||
|
|
||||||
|
#[cfg(feature="brotli_compression")]
|
||||||
|
extern crate brotli;
|
||||||
|
|
||||||
|
#[cfg(feature="gzip_compression")]
|
||||||
|
extern crate flate2;
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
#![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))]
|
||||||
|
mod compressed_tests {
|
||||||
|
extern crate brotli;
|
||||||
|
extern crate flate2;
|
||||||
|
|
||||||
|
use rocket::http::hyper::header::{ContentEncoding, Encoding};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::http::{ContentType, Header};
|
||||||
|
use rocket::local::Client;
|
||||||
|
use rocket::response::{Content, Response};
|
||||||
|
use rocket::routes;
|
||||||
|
use rocket_contrib::compression::Compressed;
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use self::flate2::read::{GzDecoder, GzEncoder};
|
||||||
|
|
||||||
|
const HELLO: &str = r"This is a message to hello with more than 100 bytes \
|
||||||
|
in order to have to read more than one buffer when gzipping. こんにちは!";
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub fn index() -> Compressed<String> {
|
||||||
|
Compressed::new(String::from(HELLO))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/font")]
|
||||||
|
pub fn font() -> Compressed<Content<&'static str>> {
|
||||||
|
Compressed::new(Content(ContentType::WOFF, HELLO))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/image")]
|
||||||
|
pub fn image() -> Compressed<Content<&'static str>> {
|
||||||
|
Compressed::new(Content(ContentType::PNG, HELLO))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/already_encoded")]
|
||||||
|
pub fn already_encoded() -> Compressed<Response<'static>> {
|
||||||
|
let mut encoder = GzEncoder::new(
|
||||||
|
Cursor::new(String::from(HELLO)),
|
||||||
|
flate2::Compression::default(),
|
||||||
|
);
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
encoder.read_to_end(&mut encoded).unwrap();
|
||||||
|
Compressed::new(
|
||||||
|
Response::build()
|
||||||
|
.header(ContentEncoding(vec![Encoding::Gzip]))
|
||||||
|
.sized_body(Cursor::new(encoded))
|
||||||
|
.finalize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/identity")]
|
||||||
|
pub fn identity() -> Compressed<Response<'static>> {
|
||||||
|
Compressed::new(
|
||||||
|
Response::build()
|
||||||
|
.header(ContentEncoding(vec![Encoding::Identity]))
|
||||||
|
.sized_body(Cursor::new(String::from(HELLO)))
|
||||||
|
.finalize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rocket() -> rocket::Rocket {
|
||||||
|
rocket::ignite().mount("/", routes![index, font, image, already_encoded, identity])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prioritizes_brotli() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_br_font() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/font")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_gzip() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "gzip"));
|
||||||
|
let mut s = String::new();
|
||||||
|
GzDecoder::new(&response.body_bytes().unwrap()[..])
|
||||||
|
.read_to_string(&mut s)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(s, String::from(HELLO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_recompress() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/already_encoded")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "gzip"));
|
||||||
|
let mut s = String::new();
|
||||||
|
GzDecoder::new(&response.body_bytes().unwrap()[..])
|
||||||
|
.read_to_string(&mut s)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(s, String::from(HELLO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_compress_explicit_identity() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/identity")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ignore_exceptions() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/image")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ignores_unimplemented_encodings() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_respects_identity_only() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "identity"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,307 @@
|
||||||
|
#![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))]
|
||||||
|
mod compression_tests {
|
||||||
|
extern crate brotli;
|
||||||
|
extern crate flate2;
|
||||||
|
|
||||||
|
use rocket::config::{Config, Environment};
|
||||||
|
use rocket::http::hyper::header::{ContentEncoding, Encoding};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::http::{ContentType, Header};
|
||||||
|
use rocket::local::Client;
|
||||||
|
use rocket::response::Response;
|
||||||
|
use rocket::routes;
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use self::flate2::read::{GzDecoder, GzEncoder};
|
||||||
|
|
||||||
|
const HELLO: &str = r"This is a message to hello with more than 100 bytes \
|
||||||
|
in order to have to read more than one buffer when gzipping. こんにちは!";
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub fn index() -> String {
|
||||||
|
String::from(HELLO)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/font")]
|
||||||
|
pub fn font() -> Response<'static> {
|
||||||
|
Response::build()
|
||||||
|
.header(ContentType::WOFF)
|
||||||
|
.sized_body(Cursor::new(String::from(HELLO)))
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/image")]
|
||||||
|
pub fn image() -> Response<'static> {
|
||||||
|
Response::build()
|
||||||
|
.header(ContentType::PNG)
|
||||||
|
.sized_body(Cursor::new(String::from(HELLO)))
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/tar")]
|
||||||
|
pub fn tar() -> Response<'static> {
|
||||||
|
Response::build()
|
||||||
|
.header(ContentType::TAR)
|
||||||
|
.sized_body(Cursor::new(String::from(HELLO)))
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/already_encoded")]
|
||||||
|
pub fn already_encoded() -> Response<'static> {
|
||||||
|
let mut encoder = GzEncoder::new(
|
||||||
|
Cursor::new(String::from(HELLO)),
|
||||||
|
flate2::Compression::default(),
|
||||||
|
);
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
encoder.read_to_end(&mut encoded).unwrap();
|
||||||
|
Response::build()
|
||||||
|
.header(ContentEncoding(vec![Encoding::Gzip]))
|
||||||
|
.sized_body(Cursor::new(encoded))
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/identity")]
|
||||||
|
pub fn identity() -> Response<'static> {
|
||||||
|
Response::build()
|
||||||
|
.header(ContentEncoding(vec![Encoding::Identity]))
|
||||||
|
.sized_body(Cursor::new(String::from(HELLO)))
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rocket() -> rocket::Rocket {
|
||||||
|
rocket::ignite()
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![index, font, image, tar, already_encoded, identity],
|
||||||
|
)
|
||||||
|
.attach(rocket_contrib::compression::Compression::fairing())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rocket_tar_exception() -> rocket::Rocket {
|
||||||
|
let mut table = std::collections::BTreeMap::new();
|
||||||
|
table.insert("exclude".to_string(), vec!["application/x-tar"]);
|
||||||
|
let config = Config::build(Environment::Development)
|
||||||
|
.extra("compress", table)
|
||||||
|
.expect("valid configuration");
|
||||||
|
|
||||||
|
rocket::custom(config)
|
||||||
|
.mount("/", routes![image, tar])
|
||||||
|
.attach(rocket_contrib::compression::Compression::fairing())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prioritizes_brotli() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_br_font() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/font")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_gzip() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "gzip"));
|
||||||
|
let mut s = String::new();
|
||||||
|
GzDecoder::new(&response.body_bytes().unwrap()[..])
|
||||||
|
.read_to_string(&mut s)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(s, String::from(HELLO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_recompress() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/already_encoded")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "gzip"));
|
||||||
|
let mut s = String::new();
|
||||||
|
GzDecoder::new(&response.body_bytes().unwrap()[..])
|
||||||
|
.read_to_string(&mut s)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(s, String::from(HELLO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_compress_explicit_identity() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/identity")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_compress_image() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/image")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ignores_unimplemented_encodings() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_respects_identity_only() {
|
||||||
|
let client = Client::new(rocket()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/")
|
||||||
|
.header(Header::new("Accept-Encoding", "identity"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_does_not_compress_custom_exception() {
|
||||||
|
let client = Client::new(rocket_tar_exception()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/tar")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(!response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x != "identity"));
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(response.body_bytes().unwrap()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compress_custom_removed_exception() {
|
||||||
|
let client = Client::new(rocket_tar_exception()).expect("valid rocket instance");
|
||||||
|
let mut response = client
|
||||||
|
.get("/image")
|
||||||
|
.header(Header::new("Accept-Encoding", "deflate, gzip, br"))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert!(response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Encoding")
|
||||||
|
.any(|x| x == "br"));
|
||||||
|
let mut body_plain = Cursor::new(Vec::<u8>::new());
|
||||||
|
brotli::BrotliDecompress(
|
||||||
|
&mut Cursor::new(response.body_bytes().unwrap()),
|
||||||
|
&mut body_plain,
|
||||||
|
)
|
||||||
|
.expect("decompress response");
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8(body_plain.get_mut().to_vec()).unwrap(),
|
||||||
|
String::from(HELLO)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue