use crate::http::{
HeaderValue,
headers::{
ContentSecurityPolicy, HostSource, ReferrerPolicy, SourceList, XContentTypeOptions,
XFrameOptions,
},
layer::set_header::{
SetResponseHeaderLayer,
response::{MakeHeaderValueDefault, TypedHeaderAsMaker},
},
};
use crate::net::{Protocol, address::Domain};
#[must_use]
pub fn rama_html_csp() -> ContentSecurityPolicy {
ContentSecurityPolicy::strict_self().with_img_src(
SourceList::self_origin().with_data().with_host(
HostSource::new(Domain::from_static("raw.githubusercontent.com"))
.with_scheme(Protocol::HTTPS),
),
)
}
pub fn defence_in_depth_layer(
csp: ContentSecurityPolicy,
) -> (
SetResponseHeaderLayer<Option<HeaderValue>>,
SetResponseHeaderLayer<MakeHeaderValueDefault<TypedHeaderAsMaker<XContentTypeOptions>>>,
SetResponseHeaderLayer<Option<HeaderValue>>,
SetResponseHeaderLayer<Option<HeaderValue>>,
) {
(
SetResponseHeaderLayer::if_not_present_typed(csp),
SetResponseHeaderLayer::<XContentTypeOptions>::if_not_present_default_typed(),
SetResponseHeaderLayer::if_not_present_typed(ReferrerPolicy::NO_REFERRER),
SetResponseHeaderLayer::if_not_present_typed(XFrameOptions::Deny),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Layer, Service,
http::{
Body, Request, Response,
headers::HeaderMapExt as _,
service::web::{IntoEndpointService, response::IntoResponse},
},
service::service_fn,
};
use std::convert::Infallible;
async fn invoke(csp: ContentSecurityPolicy) -> crate::http::HeaderMap {
let svc = defence_in_depth_layer(csp).into_layer(
service_fn(async || Ok::<_, Infallible>(Response::new(Body::empty())))
.into_endpoint_service(),
);
let resp = svc
.serve(Request::new(Body::empty()))
.await
.expect("infallible service");
resp.headers().clone()
}
#[tokio::test]
async fn baseline_strict_self_emits_all_four_headers() {
let headers = invoke(rama_html_csp()).await;
let csp: ContentSecurityPolicy = headers.typed_get().expect("CSP set");
let rendered = csp.to_string();
assert!(rendered.contains("default-src 'self'"), "{rendered}");
assert!(rendered.contains("script-src 'self'"), "{rendered}");
assert!(rendered.contains("frame-ancestors 'none'"), "{rendered}");
assert!(
rendered.contains("img-src 'self' data: https://raw.githubusercontent.com"),
"{rendered}",
);
assert!(!rendered.contains("ws:"), "{rendered}");
assert!(!rendered.contains("wss:"), "{rendered}");
let xcto: XContentTypeOptions = headers.typed_get().expect("X-Content-Type-Options set");
assert_eq!(xcto, XContentTypeOptions::nosniff());
let rp: ReferrerPolicy = headers.typed_get().expect("Referrer-Policy set");
assert_eq!(rp, ReferrerPolicy::NO_REFERRER);
let xfo: XFrameOptions = headers.typed_get().expect("X-Frame-Options set");
assert_eq!(xfo, XFrameOptions::Deny);
}
#[tokio::test]
async fn does_not_overwrite_existing_headers() {
let inner_csp = ContentSecurityPolicy::empty().with_default_src(SourceList::none());
let inner_csp_for_layer = inner_csp.clone();
let svc = defence_in_depth_layer(rama_html_csp()).into_layer(
service_fn(move |_req: Request| {
let csp = inner_csp_for_layer.clone();
async move {
let mut resp = Response::new(Body::empty());
resp.headers_mut().typed_insert(csp);
Ok::<_, Infallible>(resp.into_response())
}
})
.into_endpoint_service(),
);
let resp = svc.serve(Request::new(Body::empty())).await.unwrap();
let got: ContentSecurityPolicy = resp.headers().typed_get().expect("CSP set");
assert_eq!(got, inner_csp);
}
#[tokio::test]
async fn caller_widening_with_connect_src_is_preserved() {
let csp = rama_html_csp().with_connect_src(SourceList::self_origin());
let headers = invoke(csp).await;
let got: ContentSecurityPolicy = headers.typed_get().expect("CSP set");
assert!(got.to_string().contains("connect-src 'self'"));
}
}