tako_rs_plugins/middleware/
security_headers.rs1use std::future::Future;
27use std::pin::Pin;
28use std::sync::Arc;
29
30use http::HeaderValue;
31use tako_rs_core::middleware::IntoMiddleware;
32use tako_rs_core::middleware::Next;
33use tako_rs_core::types::Request;
34use tako_rs_core::types::Response;
35
36#[derive(Debug, Clone)]
46pub struct CspNonce(pub String);
47
48#[derive(Clone)]
49enum CspMode {
50 Static(HeaderValue),
51 WithNonce { template: String, header: bool },
52}
53
54pub struct SecurityHeaders {
56 frame_options: HeaderValue,
57 hsts: bool,
58 hsts_max_age: u64,
59 hsts_include_subdomains: bool,
60 hsts_preload: bool,
61 referrer_policy: HeaderValue,
62 csp: Option<CspMode>,
63 coop: Option<HeaderValue>,
64 coep: Option<HeaderValue>,
65 corp: Option<HeaderValue>,
66 permissions_policy: Option<HeaderValue>,
67}
68
69impl Default for SecurityHeaders {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl SecurityHeaders {
76 pub fn new() -> Self {
78 Self {
79 frame_options: HeaderValue::from_static("DENY"),
80 hsts: false,
81 hsts_max_age: 31_536_000,
82 hsts_include_subdomains: true,
83 hsts_preload: false,
84 referrer_policy: HeaderValue::from_static("strict-origin-when-cross-origin"),
85 csp: None,
86 coop: None,
87 coep: None,
88 corp: None,
89 permissions_policy: None,
90 }
91 }
92
93 pub fn frame_options(mut self, value: &'static str) -> Self {
95 self.frame_options = HeaderValue::from_static(value);
96 self
97 }
98
99 pub fn hsts(mut self, enable: bool) -> Self {
101 self.hsts = enable;
102 self
103 }
104
105 pub fn hsts_max_age(mut self, seconds: u64) -> Self {
107 self.hsts_max_age = seconds;
108 self
109 }
110
111 pub fn hsts_include_subdomains(mut self, on: bool) -> Self {
113 self.hsts_include_subdomains = on;
114 self
115 }
116
117 pub fn hsts_preload(mut self, on: bool) -> Self {
120 self.hsts_preload = on;
121 self
122 }
123
124 pub fn referrer_policy(mut self, value: &'static str) -> Self {
126 self.referrer_policy = HeaderValue::from_static(value);
127 self
128 }
129
130 pub fn csp(mut self, value: &'static str) -> Self {
132 self.csp = Some(CspMode::Static(HeaderValue::from_static(value)));
133 self
134 }
135
136 pub fn csp_with_nonce(mut self, template: impl Into<String>) -> Self {
141 self.csp = Some(CspMode::WithNonce {
142 template: template.into(),
143 header: false,
144 });
145 self
146 }
147
148 pub fn csp_report_only(mut self, template: impl Into<String>) -> Self {
150 self.csp = Some(CspMode::WithNonce {
151 template: template.into(),
152 header: true,
153 });
154 self
155 }
156
157 pub fn coop(mut self, value: &'static str) -> Self {
159 self.coop = Some(HeaderValue::from_static(value));
160 self
161 }
162
163 pub fn coep(mut self, value: &'static str) -> Self {
165 self.coep = Some(HeaderValue::from_static(value));
166 self
167 }
168
169 pub fn corp(mut self, value: &'static str) -> Self {
171 self.corp = Some(HeaderValue::from_static(value));
172 self
173 }
174
175 pub fn permissions_policy(mut self, value: &'static str) -> Self {
177 self.permissions_policy = Some(HeaderValue::from_static(value));
178 self
179 }
180}
181
182fn rand_nonce() -> String {
183 use base64::Engine;
186
187 let u1 = uuid::Uuid::new_v4().into_bytes();
188 let u2 = uuid::Uuid::new_v4().into_bytes();
189 let mut buf = [0u8; 18];
190 buf[..16].copy_from_slice(&u1);
191 buf[16..].copy_from_slice(&u2[..2]);
192 base64::engine::general_purpose::STANDARD_NO_PAD.encode(buf)
193}
194
195impl IntoMiddleware for SecurityHeaders {
196 fn into_middleware(
197 self,
198 ) -> impl Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>>
199 + Clone
200 + Send
201 + Sync
202 + 'static {
203 let frame_options = self.frame_options;
204 let hsts_value = if self.hsts {
205 let mut buf = format!("max-age={}", self.hsts_max_age);
206 if self.hsts_include_subdomains {
207 buf.push_str("; includeSubDomains");
208 }
209 if self.hsts_preload {
210 buf.push_str("; preload");
211 }
212 Some(HeaderValue::from_str(&buf).expect("valid HSTS header"))
213 } else {
214 None
215 };
216 let referrer_policy = self.referrer_policy;
217 let csp = Arc::new(self.csp);
218 let coop = self.coop;
219 let coep = self.coep;
220 let corp = self.corp;
221 let permissions_policy = self.permissions_policy;
222
223 move |mut req: Request, next: Next| {
224 let frame_options = frame_options.clone();
225 let hsts_value = hsts_value.clone();
226 let referrer_policy = referrer_policy.clone();
227 let csp = csp.clone();
228 let coop = coop.clone();
229 let coep = coep.clone();
230 let corp = corp.clone();
231 let permissions_policy = permissions_policy.clone();
232
233 Box::pin(async move {
234 let prepared_csp: Option<(HeaderValue, bool)> = match csp.as_ref() {
237 None => None,
238 Some(CspMode::Static(v)) => Some((v.clone(), false)),
239 Some(CspMode::WithNonce { template, header }) => {
240 let nonce = rand_nonce();
241 let value = template.replace("{nonce}", &nonce);
242 req.extensions_mut().insert(CspNonce(nonce));
243 HeaderValue::from_str(&value).ok().map(|hv| (hv, *header))
244 }
245 };
246
247 let mut resp = next.run(req).await;
248 let headers = resp.headers_mut();
249
250 headers.insert(
251 "x-content-type-options",
252 HeaderValue::from_static("nosniff"),
253 );
254 headers.insert("x-frame-options", frame_options);
255 headers.insert("referrer-policy", referrer_policy);
256
257 if let Some(hsts) = hsts_value {
258 headers.insert("strict-transport-security", hsts);
259 }
260
261 if let Some((v, report_only)) = prepared_csp {
262 let key = if report_only {
263 "content-security-policy-report-only"
264 } else {
265 "content-security-policy"
266 };
267 headers.insert(key, v);
268 }
269
270 if let Some(v) = coop {
271 headers.insert("cross-origin-opener-policy", v);
272 }
273 if let Some(v) = coep {
274 headers.insert("cross-origin-embedder-policy", v);
275 }
276 if let Some(v) = corp {
277 headers.insert("cross-origin-resource-policy", v);
278 }
279 if let Some(v) = permissions_policy {
280 headers.insert("permissions-policy", v);
281 }
282
283 resp
284 })
285 }
286 }
287}