mirror of
https://github.com/actix/actix-web.git
synced 2024-12-20 07:06:42 +00:00
EtagHasher middleware
This commit is contained in:
parent
566b16c1f7
commit
b4df0837ca
2 changed files with 319 additions and 0 deletions
318
src/middleware/etaghasher.rs
Normal file
318
src/middleware/etaghasher.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
//! ETag header and `304 Not Modified` support for HTTP responses
|
||||
///
|
||||
/// The `EtagHasher` middleware generates RFC 7232 ETag headers for HTTP
|
||||
/// responses, and checks the ETag for a response against those provided
|
||||
/// in the `If-None-Match` header of the request, if present. In the
|
||||
/// event of a match, instead of returning the original response, an
|
||||
/// HTTP `304 Not Modified` response with no content is returned
|
||||
/// instead. Only response [Body](enum.Body.html)s of type `Binary` are
|
||||
/// supported; responses with other body types will be left unchanged.
|
||||
///
|
||||
/// ETag values are generated by computing a hash function over the
|
||||
/// bytes of the body of the original response. Thus, using this
|
||||
/// middleware amounts to trading CPU resources for bandwidth. Some CPU
|
||||
/// overhead is incurred by having to compute a hash for each response
|
||||
/// body, but in return one avoids sending response bodies to requesters
|
||||
/// that already have the body content cached.
|
||||
///
|
||||
/// An `EtagHasher` instance makes use of two functions, `hash` and
|
||||
/// `filter`. The `hash` function takes the bytes of the original
|
||||
/// response body as input and produces an ETag value. The `filter`
|
||||
/// function takes the original HTTP request and response, and returns
|
||||
/// `true` if ETag processing should be applied to this response and
|
||||
/// `false` otherwise. These functions are supplied by the user when the
|
||||
/// instance is created; the `DefaultHasher` and `DefaultFilter` can be
|
||||
/// used if desired. Currently `DefaultHasher` computes an SHA-1 hash,
|
||||
/// but this should not be relied upon. The `DefaultFilter` returns
|
||||
/// `true` when the request method is `GET` or `HEAD` and the original
|
||||
/// response status is `200 OK`. If you provide your own `filter`, you
|
||||
/// will want to check for these conditions as well.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::{http, middleware, App, HttpResponse};
|
||||
/// use middleware::etaghasher::{EtagHasher, DefaultHasher, DefaultFilter};
|
||||
///
|
||||
/// fn main() {
|
||||
/// let eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
/// let app = App::new()
|
||||
/// .middleware(eh)
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(http::Method::GET).f(|_| HttpResponse::Ok());
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// With custom `hash` and `filter` functions:
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::{http, middleware, App, HttpRequest, HttpResponse};
|
||||
/// use middleware::etaghasher::EtagHasher;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let eh = EtagHasher::new(
|
||||
/// |_input: &[u8]| "static".to_string(),
|
||||
/// |_req: &HttpRequest<()>, _res: &HttpResponse| true,
|
||||
/// );
|
||||
/// let app = App::new()
|
||||
/// .middleware(eh)
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(http::Method::GET).f(|_| HttpResponse::Ok());
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
use error::Result;
|
||||
use header::EntityTag;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middleware;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// `Middleware` for generating ETag headers and returning `304 Not Modified`
|
||||
/// responses upon receipt of a matching `If-None-Match` request header.
|
||||
|
||||
/// Can produce an ETag value from a byte slice. Per RFC 7232, **must only
|
||||
/// produce** bytes with hex values `21`, `23-7E`, or greater than or equal
|
||||
/// to `80`. Producing invalid bytes will result in a panic when the output
|
||||
/// is converted to an ETag.
|
||||
pub trait Hasher {
|
||||
/// Produce an ETag value given a byte slice.
|
||||
fn hash(&self, input: &[u8]) -> String;
|
||||
}
|
||||
/// Can test a (request, response) pair and return `true` or `false`
|
||||
pub trait RequestFilter<S> {
|
||||
/// Return `true` if ETag processing should be applied to this
|
||||
/// `(request, response)` pair and `false` otherwise. A `false` return
|
||||
/// value will immediately return the original response unchanged.
|
||||
fn filter(&self, req: &HttpRequest<S>, res: &HttpResponse) -> bool;
|
||||
}
|
||||
|
||||
// Closure implementations
|
||||
impl<F: Fn(&[u8]) -> String> Hasher for F {
|
||||
fn hash(&self, input: &[u8]) -> String {
|
||||
self(input)
|
||||
}
|
||||
}
|
||||
impl<S, F: Fn(&HttpRequest<S>, &HttpResponse) -> bool> RequestFilter<S> for F {
|
||||
fn filter(&self, req: &HttpRequest<S>, res: &HttpResponse) -> bool {
|
||||
self(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
/// Computes an ETag value from a byte slice using a default cryptographic hash
|
||||
/// function.
|
||||
pub struct DefaultHasher;
|
||||
impl Hasher for DefaultHasher {
|
||||
fn hash(&self, input: &[u8]) -> String {
|
||||
use sha1;
|
||||
let mut h = sha1::Sha1::new();
|
||||
h.update(input);
|
||||
h.digest().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when the request method is `GET` or `HEAD` and the
|
||||
/// original response status is `200 OK`, and `false` otherwise.
|
||||
pub struct DefaultFilter;
|
||||
impl<S> RequestFilter<S> for DefaultFilter {
|
||||
fn filter(&self, req: &HttpRequest<S>, res: &HttpResponse) -> bool {
|
||||
use http::{Method, StatusCode};
|
||||
(*req.method() == Method::GET || *req.method() == Method::HEAD)
|
||||
&& res.status() == StatusCode::OK
|
||||
}
|
||||
}
|
||||
|
||||
/// The middleware struct. Contains a Hasher to compute ETag values for byte
|
||||
/// slices and a filter to determine whether ETag computation and checking
|
||||
/// should be applied to a particular (request, response) pair.
|
||||
pub struct EtagHasher<S, H, F>
|
||||
where
|
||||
S: 'static,
|
||||
H: Hasher + 'static,
|
||||
F: RequestFilter<S> + 'static,
|
||||
{
|
||||
hasher: H,
|
||||
filter: F,
|
||||
_phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S, H, F> EtagHasher<S, H, F>
|
||||
where
|
||||
S: 'static,
|
||||
H: Hasher + 'static,
|
||||
F: RequestFilter<S> + 'static,
|
||||
{
|
||||
/// Create a new middleware struct with the given Hasher and RequestFilter.
|
||||
pub fn new(hasher: H, filter: F) -> Self {
|
||||
EtagHasher {
|
||||
hasher,
|
||||
filter,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, H, F> middleware::Middleware<S> for EtagHasher<S, H, F>
|
||||
where
|
||||
S: 'static,
|
||||
H: Hasher + 'static,
|
||||
F: RequestFilter<S> + 'static,
|
||||
{
|
||||
fn response(
|
||||
&mut self, req: &mut HttpRequest<S>, mut res: HttpResponse,
|
||||
) -> Result<middleware::Response> {
|
||||
use header;
|
||||
use Body;
|
||||
|
||||
if !self.filter.filter(req, &res) {
|
||||
return Ok(middleware::Response::Done(res));
|
||||
}
|
||||
|
||||
let e = if let Body::Binary(b) = res.body() {
|
||||
Some(EntityTag::strong(self.hasher.hash(b.as_ref())))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(etag) = e {
|
||||
if !none_match(&etag, req) {
|
||||
let mut not_modified =
|
||||
HttpResponse::NotModified().set(header::ETag(etag)).finish();
|
||||
|
||||
// RFC 7232 requires copying over these headers:
|
||||
copy_header(header::CACHE_CONTROL, &res, &mut not_modified);
|
||||
copy_header(header::CONTENT_LOCATION, &res, &mut not_modified);
|
||||
copy_header(header::DATE, &res, &mut not_modified);
|
||||
copy_header(header::EXPIRES, &res, &mut not_modified);
|
||||
copy_header(header::VARY, &res, &mut not_modified);
|
||||
|
||||
return Ok(middleware::Response::Done(not_modified));
|
||||
}
|
||||
etag.to_string()
|
||||
.parse::<header::HeaderValue>()
|
||||
.map(|v| {
|
||||
res.headers_mut().insert(header::ETAG, v);
|
||||
})
|
||||
.unwrap_or(());
|
||||
}
|
||||
Ok(middleware::Response::Done(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn copy_header(h: ::header::HeaderName, src: &HttpResponse, dst: &mut HttpResponse) {
|
||||
if let Some(val) = src.headers().get(&h) {
|
||||
dst.headers_mut().insert(h, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
#[inline]
|
||||
fn none_match<S>(etag: &EntityTag, req: &HttpRequest<S>) -> bool {
|
||||
use header::IfNoneMatch;
|
||||
use httpmessage::HttpMessage;
|
||||
match req.get_header::<IfNoneMatch>() {
|
||||
Some(IfNoneMatch::Items(ref items)) => {
|
||||
for item in items {
|
||||
if item.weak_eq(etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Some(IfNoneMatch::Any) => false,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use header::ETAG;
|
||||
use http::StatusCode;
|
||||
use httpmessage::HttpMessage;
|
||||
use middleware::Middleware;
|
||||
use test::{TestRequest, TestServer};
|
||||
|
||||
const TEST_ETAG: &'static str = "\"a94a8fe5ccb19ba61c4c0873d391e987982fbbd3\"";
|
||||
struct TestState {
|
||||
_state: u32,
|
||||
}
|
||||
fn test_index<S>(_req: HttpRequest<S>) -> &'static str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn mwres(r: Result<middleware::Response>) -> HttpResponse {
|
||||
match r {
|
||||
Ok(middleware::Response::Done(hr)) => hr,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_create_etag() {
|
||||
let mut eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
let mut req = TestRequest::default().finish();
|
||||
let res = HttpResponse::Ok().body("test");
|
||||
let res = mwres(eh.response(&mut req, res));
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers().get(ETAG).unwrap(), TEST_ETAG);
|
||||
}
|
||||
#[test]
|
||||
fn test_default_with_state_create_etag() {
|
||||
let state = TestState { _state: 0 };
|
||||
let mut eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
let mut req = TestRequest::with_state(state).finish();
|
||||
let res = HttpResponse::Ok().body("test");
|
||||
let res = mwres(eh.response(&mut req, res));
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers().get(ETAG).unwrap(), TEST_ETAG);
|
||||
}
|
||||
#[test]
|
||||
fn test_default_none_match() {
|
||||
let mut eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
let mut req = TestRequest::with_header("If-None-Match", "_").finish();
|
||||
let res = HttpResponse::Ok().body("test");
|
||||
let res = mwres(eh.response(&mut req, res));
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers().get(ETAG).unwrap(), TEST_ETAG);
|
||||
}
|
||||
#[test]
|
||||
fn test_default_match() {
|
||||
let mut eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
let mut req = TestRequest::with_header("If-None-Match", TEST_ETAG).finish();
|
||||
let res = HttpResponse::Ok().body("test");
|
||||
let res = mwres(eh.response(&mut req, res));
|
||||
assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
#[test]
|
||||
fn test_custom_match() {
|
||||
let mut eh = EtagHasher::new(
|
||||
|_input: &[u8]| "static".to_string(),
|
||||
|_req: &HttpRequest<()>, _res: &HttpResponse| true,
|
||||
);
|
||||
let mut req = TestRequest::with_header("If-None-Match", "\"static\"").finish();
|
||||
let res = HttpResponse::Ok().body("test");
|
||||
let res = mwres(eh.response(&mut req, res));
|
||||
assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
#[test]
|
||||
fn test_srv_default_create_etag() {
|
||||
let mut srv =
|
||||
TestServer::build_with_state(|| TestState { _state: 0 }).start(|app| {
|
||||
let eh = EtagHasher::new(DefaultHasher, DefaultFilter);
|
||||
app.middleware(eh).handler(test_index)
|
||||
});
|
||||
|
||||
let req = srv.get().finish().unwrap();
|
||||
let response = srv.execute(req.send()).unwrap();
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(response.headers().get(ETAG).unwrap(), TEST_ETAG);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ mod logger;
|
|||
|
||||
pub mod cors;
|
||||
pub mod csrf;
|
||||
pub mod etaghasher;
|
||||
mod defaultheaders;
|
||||
mod errhandlers;
|
||||
#[cfg(feature = "session")]
|
||||
|
|
Loading…
Reference in a new issue