Skip to main content

rskit_http/
headers.rs

1//! HTTP response security headers.
2
3use std::sync::Arc;
4use std::task::{Context, Poll};
5
6use http::{HeaderName, HeaderValue, Request, Response};
7use rskit_errors::AppResult;
8pub use rskit_security::{SecurityHeadersConfig, TransportSecurity};
9use tower::{Layer, Service};
10
11/// Tower layer that applies secure response headers.
12#[derive(Debug, Clone)]
13pub struct SecurityHeadersLayer {
14    headers: Arc<Vec<(HeaderName, HeaderValue)>>,
15}
16
17impl SecurityHeadersLayer {
18    /// Create a new layer from validated configuration.
19    ///
20    /// # Errors
21    /// Returns an error when the configuration is invalid or a header value cannot be constructed.
22    pub fn new(config: &SecurityHeadersConfig) -> AppResult<Self> {
23        let headers = Arc::new(config.header_pairs()?);
24        Ok(Self { headers })
25    }
26}
27
28impl<S> Layer<S> for SecurityHeadersLayer {
29    type Service = SecurityHeadersService<S>;
30
31    fn layer(&self, inner: S) -> Self::Service {
32        SecurityHeadersService {
33            inner,
34            headers: Arc::clone(&self.headers),
35        }
36    }
37}
38
39/// Service produced by [`SecurityHeadersLayer`].
40#[derive(Debug, Clone)]
41pub struct SecurityHeadersService<S> {
42    inner: S,
43    headers: Arc<Vec<(HeaderName, HeaderValue)>>,
44}
45
46impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for SecurityHeadersService<S>
47where
48    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
49    S::Future: Send + 'static,
50    ReqBody: Send + 'static,
51    ResBody: Send + 'static,
52{
53    type Response = Response<ResBody>;
54    type Error = S::Error;
55    type Future = std::pin::Pin<
56        Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
57    >;
58
59    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
60        self.inner.poll_ready(cx)
61    }
62
63    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
64        let clone = self.inner.clone();
65        let mut inner = std::mem::replace(&mut self.inner, clone);
66        let headers = Arc::clone(&self.headers);
67        Box::pin(async move {
68            let mut response = inner.call(req).await?;
69            let response_headers = response.headers_mut();
70            for (name, value) in headers.iter() {
71                response_headers
72                    .entry(name)
73                    .or_insert_with(|| value.clone());
74            }
75            Ok(response)
76        })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use http::{
84        StatusCode,
85        header::{
86            CONTENT_SECURITY_POLICY, HeaderValue, REFERRER_POLICY, STRICT_TRANSPORT_SECURITY,
87            X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
88        },
89    };
90    use tower::{ServiceBuilder, ServiceExt, service_fn};
91
92    #[tokio::test]
93    async fn defaults_apply_secure_headers() {
94        let service = ServiceBuilder::new()
95            .layer(SecurityHeadersLayer::new(&SecurityHeadersConfig::default()).unwrap())
96            .service(service_fn(|_req: Request<()>| async {
97                Ok::<_, std::convert::Infallible>(
98                    Response::builder().status(StatusCode::OK).body(()).unwrap(),
99                )
100            }));
101
102        let response = service.oneshot(Request::new(())).await.unwrap();
103        let headers = response.headers();
104
105        assert_eq!(
106            headers.get(STRICT_TRANSPORT_SECURITY),
107            Some(&HeaderValue::from_static(
108                "max-age=63072000; includeSubDomains; preload"
109            ))
110        );
111        assert_eq!(
112            headers.get(X_CONTENT_TYPE_OPTIONS),
113            Some(&HeaderValue::from_static("nosniff"))
114        );
115        assert_eq!(
116            headers.get(X_FRAME_OPTIONS),
117            Some(&HeaderValue::from_static("DENY"))
118        );
119    }
120
121    #[tokio::test]
122    async fn insecure_local_mode_omits_hsts() {
123        let service = ServiceBuilder::new()
124            .layer(
125                SecurityHeadersLayer::new(
126                    &SecurityHeadersConfig::default()
127                        .with_transport_security(TransportSecurity::AllowInsecureLocal),
128                )
129                .unwrap(),
130            )
131            .service(service_fn(|_req: Request<()>| async {
132                Ok::<_, std::convert::Infallible>(
133                    Response::builder().status(StatusCode::OK).body(()).unwrap(),
134                )
135            }));
136
137        let response = service.oneshot(Request::new(())).await.unwrap();
138        assert!(response.headers().get(STRICT_TRANSPORT_SECURITY).is_none());
139        assert!(response.headers().get(CONTENT_SECURITY_POLICY).is_some());
140    }
141
142    #[tokio::test]
143    async fn existing_headers_are_not_overwritten() {
144        let service = ServiceBuilder::new()
145            .layer(SecurityHeadersLayer::new(&SecurityHeadersConfig::default()).unwrap())
146            .service(service_fn(|_req: Request<()>| async {
147                let mut response = Response::builder().status(StatusCode::OK).body(()).unwrap();
148                response
149                    .headers_mut()
150                    .insert(REFERRER_POLICY, HeaderValue::from_static("same-origin"));
151                Ok::<_, std::convert::Infallible>(response)
152            }));
153
154        let response = service.oneshot(Request::new(())).await.unwrap();
155        assert_eq!(
156            response.headers().get(REFERRER_POLICY),
157            Some(&HeaderValue::from_static("same-origin"))
158        );
159    }
160}