1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 23:51:06 +01:00

add vary header to all handled responses (#224)

This commit is contained in:
Rob Ede 2022-02-07 16:34:01 +00:00 committed by GitHub
parent f9beeecaf6
commit 695800c9bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 156 additions and 29 deletions

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
- Ensure that preflight responses contain a Vary header. [#224]
[#224]: https://github.com/actix/actix-extras/pull/224
## 0.6.0-beta.9 - 2022-02-07 ## 0.6.0-beta.9 - 2022-02-07

View File

@ -1,12 +1,16 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{http::header, web, App, HttpServer}; use actix_web::{http::header, middleware::Logger, web, App, HttpServer};
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
// `permissive` is a wide-open development config
// .wrap(Cors::permissive())
.wrap( .wrap(
// default settings are overly restrictive to reduce chance of // default settings are overly restrictive to reduce chance of
// misconfiguration leading to security concerns // misconfiguration leading to security concerns
@ -38,9 +42,11 @@ async fn main() -> std::io::Result<()> {
// set preflight cache TTL // set preflight cache TTL
.max_age(3600), .max_age(3600),
) )
.wrap(Logger::default())
.default_service(web::to(|| async { "Hello, cross-origin world!" })) .default_service(web::to(|| async { "Hello, cross-origin world!" }))
}) })
.bind("127.0.0.1:8080")? .workers(1)
.bind(("127.0.0.1", 8080))?
.run() .run()
.await .await
} }

View File

@ -2,6 +2,8 @@ use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use crate::inner::add_vary_header;
/// Errors that can occur when processing CORS guarded requests. /// Errors that can occur when processing CORS guarded requests.
#[derive(Debug, Clone, Display, Error)] #[derive(Debug, Clone, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
@ -45,6 +47,8 @@ impl ResponseError for CorsError {
} }
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
HttpResponse::with_body(StatusCode::BAD_REQUEST, self.to_string()).map_into_boxed_body() let mut res = HttpResponse::with_body(self.status_code(), self.to_string());
add_vary_header(res.headers_mut());
res.map_into_boxed_body()
} }
} }

View File

@ -4,7 +4,7 @@ use actix_web::{
dev::RequestHead, dev::RequestHead,
error::Result, error::Result,
http::{ http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderMap, HeaderName, HeaderValue},
Method, Method,
}, },
}; };
@ -199,6 +199,25 @@ impl Inner {
} }
} }
/// Add CORS related request headers to response's Vary header.
///
/// See <https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches>.
pub(crate) fn add_vary_header(headers: &mut HeaderMap) {
let value = match headers.get(header::VARY) {
Some(hdr) => {
let mut val: Vec<u8> = Vec::with_capacity(hdr.len() + 71);
val.extend(hdr.as_bytes());
val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
val.try_into().unwrap()
}
None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
),
};
headers.insert(header::VARY, value);
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::rc::Rc; use std::rc::Rc;
@ -327,4 +346,37 @@ mod test {
let resp = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_web::test]
async fn allow_fn_origin_equals_head_origin() {
let cors = Cors::default()
.allowed_origin_fn(|origin, head| {
let head_origin = head
.headers()
.get(header::ORIGIN)
.expect("unwrapping origin header should never fail in allowed_origin_fn");
assert!(origin == head_origin);
true
})
.allow_any_method()
.allow_any_header()
.new_transform(test::simple_service(StatusCode::NO_CONTENT))
.await
.unwrap();
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::default()
.method(Method::GET)
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
} }

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, convert::TryInto, rc::Rc}; use std::{collections::HashSet, rc::Rc};
use actix_utils::future::ok; use actix_utils::future::ok;
use actix_web::{ use actix_web::{
@ -13,7 +13,7 @@ use actix_web::{
use futures_util::future::{FutureExt as _, LocalBoxFuture}; use futures_util::future::{FutureExt as _, LocalBoxFuture};
use log::debug; use log::debug;
use crate::{builder::intersperse_header_values, AllOrSome, Inner}; use crate::{builder::intersperse_header_values, inner::add_vary_header, AllOrSome, Inner};
/// Service wrapper for Cross-Origin Resource Sharing support. /// Service wrapper for Cross-Origin Resource Sharing support.
/// ///
@ -67,7 +67,9 @@ impl<S> CorsMiddleware<S> {
res.insert_header((header::ACCESS_CONTROL_MAX_AGE, max_age.to_string())); res.insert_header((header::ACCESS_CONTROL_MAX_AGE, max_age.to_string()));
} }
let res = res.finish(); let mut res = res.finish();
add_vary_header(res.headers_mut());
req.into_response(res) req.into_response(res)
} }
@ -116,21 +118,7 @@ impl<S> CorsMiddleware<S> {
} }
if inner.vary_header { if inner.vary_header {
let value = match res.headers_mut().get(header::VARY) { add_vary_header(res.headers_mut());
Some(hdr) => {
let mut val: Vec<u8> = Vec::with_capacity(hdr.len() + 71);
val.extend(hdr.as_bytes());
val.extend(
b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
val.try_into().unwrap()
}
None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
),
};
res.headers_mut().insert(header::VARY, value);
} }
res res
@ -172,12 +160,7 @@ where
async move { async move {
let res = fut.await; let res = fut.await;
if origin.is_some() { Ok(Self::augment_response(&inner, res?).map_into_left_body())
Ok(Self::augment_response(&inner, res?))
} else {
res.map_err(Into::into)
}
.map(|res| res.map_into_left_body())
} }
.boxed_local() .boxed_local()
} }

View File

@ -314,8 +314,8 @@ async fn test_response() {
.to_srv_request(); .to_srv_request();
let resp = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!( assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
Some(&b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]), Some(&b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]),
resp.headers().get(header::VARY).map(HeaderValue::as_bytes)
); );
let cors = Cors::default() let cors = Cors::default()
@ -399,6 +399,85 @@ async fn validate_origin_allows_all_origins() {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_web::test]
async fn vary_header_on_all_handled_responses() {
let cors = Cors::permissive()
.new_transform(test::ok_service())
.await
.unwrap();
// preflight request
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header((header::ORIGIN, "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "GET"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
// follow-up regular request
let req = TestRequest::default()
.method(Method::PUT)
.insert_header((header::ORIGIN, "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
let cors = Cors::default()
.allow_any_method()
.new_transform(test::ok_service())
.await
.unwrap();
// regular request bad origin
let req = TestRequest::default()
.method(Method::PUT)
.insert_header((header::ORIGIN, "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
// regular request no origin
let req = TestRequest::default().method(Method::PUT).to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
}
#[actix_web::test] #[actix_web::test]
async fn test_allow_any_origin_any_method_any_header() { async fn test_allow_any_origin_any_method_any_header() {
let cors = Cors::default() let cors = Cors::default()