koprogo_api/infrastructure/web/
security_headers.rs1use actix_web::{
2 dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
3 Error,
4};
5use futures_util::future::LocalBoxFuture;
6use std::future::{ready, Ready};
7
8pub struct SecurityHeaders;
19
20impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
21where
22 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
23 S::Future: 'static,
24 B: 'static,
25{
26 type Response = ServiceResponse<B>;
27 type Error = Error;
28 type InitError = ();
29 type Transform = SecurityHeadersMiddleware<S>;
30 type Future = Ready<Result<Self::Transform, Self::InitError>>;
31
32 fn new_transform(&self, service: S) -> Self::Future {
33 ready(Ok(SecurityHeadersMiddleware { service }))
34 }
35}
36
37pub struct SecurityHeadersMiddleware<S> {
38 service: S,
39}
40
41impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
42where
43 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
44 S::Future: 'static,
45 B: 'static,
46{
47 type Response = ServiceResponse<B>;
48 type Error = Error;
49 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
50
51 forward_ready!(service);
52
53 fn call(&self, req: ServiceRequest) -> Self::Future {
54 let fut = self.service.call(req);
55
56 Box::pin(async move {
57 let mut res = fut.await?;
58
59 let headers = res.headers_mut();
60
61 headers.insert(
65 actix_web::http::header::HeaderName::from_static("strict-transport-security"),
66 actix_web::http::header::HeaderValue::from_static(
67 "max-age=31536000; includeSubDomains; preload",
68 ),
69 );
70
71 headers.insert(
73 actix_web::http::header::HeaderName::from_static("x-content-type-options"),
74 actix_web::http::header::HeaderValue::from_static("nosniff"),
75 );
76
77 headers.insert(
79 actix_web::http::header::HeaderName::from_static("x-frame-options"),
80 actix_web::http::header::HeaderValue::from_static("DENY"),
81 );
82
83 headers.insert(
85 actix_web::http::header::HeaderName::from_static("x-xss-protection"),
86 actix_web::http::header::HeaderValue::from_static("1; mode=block"),
87 );
88
89 headers.insert(
100 actix_web::http::header::HeaderName::from_static("content-security-policy"),
101 actix_web::http::header::HeaderValue::from_static(
102 "default-src 'self'; \
103 script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
104 style-src 'self' 'unsafe-inline'; \
105 img-src 'self' data: https:; \
106 font-src 'self' data:; \
107 connect-src 'self'; \
108 frame-ancestors 'none'; \
109 base-uri 'self'; \
110 form-action 'self'",
111 ),
112 );
113
114 headers.insert(
116 actix_web::http::header::HeaderName::from_static("referrer-policy"),
117 actix_web::http::header::HeaderValue::from_static(
118 "strict-origin-when-cross-origin",
119 ),
120 );
121
122 headers.insert(
125 actix_web::http::header::HeaderName::from_static("permissions-policy"),
126 actix_web::http::header::HeaderValue::from_static(
127 "geolocation=(), microphone=(), camera=(), payment=(), usb=()",
128 ),
129 );
130
131 Ok(res)
132 })
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use actix_web::{test, web, App, HttpResponse};
140
141 #[actix_web::test]
142 async fn test_security_headers_are_added() {
143 let app = test::init_service(App::new().wrap(SecurityHeaders).route(
144 "/test",
145 web::get().to(|| async { HttpResponse::Ok().finish() }),
146 ))
147 .await;
148
149 let req = test::TestRequest::get().uri("/test").to_request();
150 let resp = test::call_service(&app, req).await;
151
152 assert!(resp.headers().contains_key("strict-transport-security"));
154 assert_eq!(
155 resp.headers().get("strict-transport-security").unwrap(),
156 "max-age=31536000; includeSubDomains; preload"
157 );
158
159 assert!(resp.headers().contains_key("x-content-type-options"));
161 assert_eq!(
162 resp.headers().get("x-content-type-options").unwrap(),
163 "nosniff"
164 );
165
166 assert!(resp.headers().contains_key("x-frame-options"));
168 assert_eq!(resp.headers().get("x-frame-options").unwrap(), "DENY");
169
170 assert!(resp.headers().contains_key("content-security-policy"));
172
173 assert!(resp.headers().contains_key("referrer-policy"));
175
176 assert!(resp.headers().contains_key("permissions-policy"));
178 }
179}