1use 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#[derive(Debug, Clone)]
13pub struct SecurityHeadersLayer {
14 headers: Arc<Vec<(HeaderName, HeaderValue)>>,
15}
16
17impl SecurityHeadersLayer {
18 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#[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}