ocre/security/policy.rs
1//! `Content-Security-Policy` and `Permissions-Policy` headers, as tower layers.
2
3use std::{
4 convert::Infallible,
5 fmt,
6 future::Future,
7 pin::Pin,
8 sync::Arc,
9 task::{Context, Poll},
10};
11
12use axum::{
13 extract::{FromRequestParts, Request},
14 http::{HeaderName, HeaderValue, header, request::Parts},
15 response::Response,
16};
17use base64::{Engine as _, engine::general_purpose::STANDARD};
18use tower_layer::Layer;
19use tower_service::Service;
20
21use crate::{
22 Error,
23 session::{Rejection, reject},
24};
25
26/// `'self'`: the app's own origin, in a [`ContentSecurityPolicy`] or [`PermissionsPolicy`] source list.
27///
28/// # Examples
29///
30/// ```
31/// use ocre::security::{ContentSecurityPolicy, SELF};
32///
33/// assert_eq!(ContentSecurityPolicy::new().default_src(&[SELF]).header_value(None), "default-src 'self'");
34/// ```
35pub const SELF: &str = "'self'";
36/// `'none'`: nothing is allowed, in a [`ContentSecurityPolicy`] source list.
37///
38/// # Examples
39///
40/// ```
41/// use ocre::security::{ContentSecurityPolicy, NONE};
42///
43/// assert_eq!(ContentSecurityPolicy::new().object_src(&[NONE]).header_value(None), "object-src 'none'");
44/// ```
45pub const NONE: &str = "'none'";
46/// Placeholder for the request's nonce: sent as `'nonce-<value>'` (Rails' `content_security_policy_nonce`).
47///
48/// Put it in `script_src` or `style_src`; the layer generates a new random
49/// nonce per request and handlers get it with the [`CspNonce`] extractor, to
50/// write `<script nonce="{{ nonce }}">`.
51///
52/// # Examples
53///
54/// ```
55/// use ocre::security::{ContentSecurityPolicy, NONCE, SELF};
56///
57/// let csp = ContentSecurityPolicy::new().script_src(&[SELF, NONCE]);
58/// assert_eq!(csp.header_value(Some("abc")), "script-src 'self' 'nonce-abc'");
59/// ```
60pub const NONCE: &str = "'nonce'";
61/// `'unsafe-inline'`: allows inline `<style>`/`<script>` and `style=` attributes. Avoid it for scripts.
62///
63/// # Examples
64///
65/// ```
66/// assert_eq!(ocre::security::UNSAFE_INLINE, "'unsafe-inline'");
67/// ```
68pub const UNSAFE_INLINE: &str = "'unsafe-inline'";
69/// `'unsafe-eval'`: allows `eval` and `new Function` (htmx's `hx-on` and `js:` need it). Avoid it.
70///
71/// # Examples
72///
73/// ```
74/// assert_eq!(ocre::security::UNSAFE_EVAL, "'unsafe-eval'");
75/// ```
76pub const UNSAFE_EVAL: &str = "'unsafe-eval'";
77/// `'strict-dynamic'`: scripts loaded by a nonced script are trusted too.
78///
79/// # Examples
80///
81/// ```
82/// assert_eq!(ocre::security::STRICT_DYNAMIC, "'strict-dynamic'");
83/// ```
84pub const STRICT_DYNAMIC: &str = "'strict-dynamic'";
85/// `data:` URLs (inline images, fonts).
86///
87/// # Examples
88///
89/// ```
90/// assert_eq!(ocre::security::DATA, "data:");
91/// ```
92pub const DATA: &str = "data:";
93/// `blob:` URLs (files built in the browser).
94///
95/// # Examples
96///
97/// ```
98/// assert_eq!(ocre::security::BLOB, "blob:");
99/// ```
100pub const BLOB: &str = "blob:";
101/// Any `https:` URL.
102///
103/// # Examples
104///
105/// ```
106/// assert_eq!(ocre::security::HTTPS, "https:");
107/// ```
108pub const HTTPS: &str = "https:";
109
110/// A `Content-Security-Policy` header, built directive by directive, and the layer that sends it.
111///
112/// Rails' `content_security_policy` initializer as plain Rust: build the
113/// policy in `src/lib.rs` and add it to the router with `.layer(policy)`
114/// (generated apps do, in `content_security_policy()`). Every response gets
115/// the header unless the handler (or a layer closer to it) set one already,
116/// which is how routes override the global policy: give a nested router its
117/// own `.layer(...)`, or return the header from the handler.
118///
119/// Sources are written as in the header: [`SELF`], [`NONE`], [`DATA`],
120/// `"https://unpkg.com"`, ... [`NONCE`] stands for the request's random nonce
121/// (see [`CspNonce`]). Calling a directive twice replaces it.
122/// [`report_only`](Self::report_only) sends
123/// `Content-Security-Policy-Report-Only` instead, to try a policy without
124/// breaking pages; [`report_uri`](Self::report_uri) and
125/// [`report_to`](Self::report_to) collect violations.
126///
127/// # Free plan
128///
129/// A header per response and, when the policy uses [`NONCE`], 16 random
130/// bytes per request: no binding call, microseconds of CPU.
131///
132/// # Examples
133///
134/// ```
135/// use axum::{Router, routing::get};
136/// use ocre::security::{ContentSecurityPolicy, DATA, NONCE, NONE, SELF};
137///
138/// let policy = ContentSecurityPolicy::new()
139/// .default_src(&[SELF])
140/// .script_src(&[SELF, NONCE, "https://unpkg.com"])
141/// .img_src(&[SELF, DATA])
142/// .object_src(&[NONE])
143/// .report_uri("/csp-reports");
144/// assert_eq!(
145/// policy.header_value(Some("r4nd0m")),
146/// "default-src 'self'; script-src 'self' 'nonce-r4nd0m' https://unpkg.com; img-src 'self' data:; \
147/// object-src 'none'; report-uri /csp-reports"
148/// );
149///
150/// let app: Router = Router::new().route("/", get(|| async { "home" })).layer(policy);
151/// # let _ = app;
152/// ```
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
154pub struct ContentSecurityPolicy {
155 directives: Vec<(String, Vec<String>)>,
156 report_only: bool,
157}
158
159impl ContentSecurityPolicy {
160 /// An empty policy: add directives with the builder methods.
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// assert_eq!(ocre::security::ContentSecurityPolicy::new().header_value(None), "");
166 /// ```
167 pub fn new() -> Self {
168 Self::default()
169 }
170
171 /// Sets any directive, e.g. `directive("sandbox", &["allow-forms"])`; replaces a previous value.
172 ///
173 /// An empty `sources` list writes the directive alone
174 /// (`upgrade-insecure-requests`).
175 ///
176 /// # Examples
177 ///
178 /// ```
179 /// use ocre::security::{ContentSecurityPolicy, SELF};
180 ///
181 /// let csp = ContentSecurityPolicy::new().directive("worker-src", &[SELF]).directive("worker-src", &["blob:"]);
182 /// assert_eq!(csp.header_value(None), "worker-src blob:");
183 /// ```
184 #[must_use]
185 pub fn directive(mut self, name: &str, sources: &[&str]) -> Self {
186 let sources = sources.iter().map(|source| (*source).to_owned()).collect();
187 match self.directives.iter_mut().find(|(existing, _)| existing == name) {
188 Some((_, existing)) => *existing = sources,
189 None => self.directives.push((name.to_owned(), sources)),
190 }
191 self
192 }
193
194 /// `default-src`: the fallback for every fetch directive not set.
195 ///
196 /// # Examples
197 ///
198 /// ```
199 /// use ocre::security::{ContentSecurityPolicy, SELF};
200 ///
201 /// assert_eq!(ContentSecurityPolicy::new().default_src(&[SELF]).header_value(None), "default-src 'self'");
202 /// ```
203 #[must_use]
204 pub fn default_src(self, sources: &[&str]) -> Self {
205 self.directive("default-src", sources)
206 }
207
208 /// `script-src`: where scripts may come from.
209 ///
210 /// # Examples
211 ///
212 /// ```
213 /// use ocre::security::{ContentSecurityPolicy, SELF};
214 ///
215 /// let csp = ContentSecurityPolicy::new().script_src(&[SELF, "https://unpkg.com"]);
216 /// assert_eq!(csp.header_value(None), "script-src 'self' https://unpkg.com");
217 /// ```
218 #[must_use]
219 pub fn script_src(self, sources: &[&str]) -> Self {
220 self.directive("script-src", sources)
221 }
222
223 /// `style-src`: where stylesheets and inline styles may come from.
224 ///
225 /// # Examples
226 ///
227 /// ```
228 /// use ocre::security::{ContentSecurityPolicy, SELF, UNSAFE_INLINE};
229 ///
230 /// let csp = ContentSecurityPolicy::new().style_src(&[SELF, UNSAFE_INLINE]);
231 /// assert_eq!(csp.header_value(None), "style-src 'self' 'unsafe-inline'");
232 /// ```
233 #[must_use]
234 pub fn style_src(self, sources: &[&str]) -> Self {
235 self.directive("style-src", sources)
236 }
237
238 /// `img-src`: images and favicons.
239 ///
240 /// # Examples
241 ///
242 /// ```
243 /// use ocre::security::{ContentSecurityPolicy, DATA, SELF};
244 ///
245 /// assert_eq!(ContentSecurityPolicy::new().img_src(&[SELF, DATA]).header_value(None), "img-src 'self' data:");
246 /// ```
247 #[must_use]
248 pub fn img_src(self, sources: &[&str]) -> Self {
249 self.directive("img-src", sources)
250 }
251
252 /// `font-src`: web fonts.
253 ///
254 /// # Examples
255 ///
256 /// ```
257 /// use ocre::security::{ContentSecurityPolicy, SELF};
258 ///
259 /// assert_eq!(ContentSecurityPolicy::new().font_src(&[SELF]).header_value(None), "font-src 'self'");
260 /// ```
261 #[must_use]
262 pub fn font_src(self, sources: &[&str]) -> Self {
263 self.directive("font-src", sources)
264 }
265
266 /// `connect-src`: `fetch`, XHR (htmx requests), WebSockets and `EventSource`.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use ocre::security::{ContentSecurityPolicy, SELF};
272 ///
273 /// assert_eq!(ContentSecurityPolicy::new().connect_src(&[SELF]).header_value(None), "connect-src 'self'");
274 /// ```
275 #[must_use]
276 pub fn connect_src(self, sources: &[&str]) -> Self {
277 self.directive("connect-src", sources)
278 }
279
280 /// `media-src`: `<audio>` and `<video>`.
281 ///
282 /// # Examples
283 ///
284 /// ```
285 /// use ocre::security::{ContentSecurityPolicy, SELF};
286 ///
287 /// assert_eq!(ContentSecurityPolicy::new().media_src(&[SELF]).header_value(None), "media-src 'self'");
288 /// ```
289 #[must_use]
290 pub fn media_src(self, sources: &[&str]) -> Self {
291 self.directive("media-src", sources)
292 }
293
294 /// `object-src`: `<object>` and `<embed>`; set it to [`NONE`].
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use ocre::security::{ContentSecurityPolicy, NONE};
300 ///
301 /// assert_eq!(ContentSecurityPolicy::new().object_src(&[NONE]).header_value(None), "object-src 'none'");
302 /// ```
303 #[must_use]
304 pub fn object_src(self, sources: &[&str]) -> Self {
305 self.directive("object-src", sources)
306 }
307
308 /// `frame-src`: pages this app may put in an `<iframe>`.
309 ///
310 /// # Examples
311 ///
312 /// ```
313 /// use ocre::security::ContentSecurityPolicy;
314 ///
315 /// let csp = ContentSecurityPolicy::new().frame_src(&["https://www.youtube-nocookie.com"]);
316 /// assert_eq!(csp.header_value(None), "frame-src https://www.youtube-nocookie.com");
317 /// ```
318 #[must_use]
319 pub fn frame_src(self, sources: &[&str]) -> Self {
320 self.directive("frame-src", sources)
321 }
322
323 /// `frame-ancestors`: sites that may put this app in a frame (the modern `X-Frame-Options`).
324 ///
325 /// # Examples
326 ///
327 /// ```
328 /// use ocre::security::{ContentSecurityPolicy, SELF};
329 ///
330 /// assert_eq!(ContentSecurityPolicy::new().frame_ancestors(&[SELF]).header_value(None), "frame-ancestors 'self'");
331 /// ```
332 #[must_use]
333 pub fn frame_ancestors(self, sources: &[&str]) -> Self {
334 self.directive("frame-ancestors", sources)
335 }
336
337 /// `form-action`: where forms may be submitted.
338 ///
339 /// # Examples
340 ///
341 /// ```
342 /// use ocre::security::{ContentSecurityPolicy, SELF};
343 ///
344 /// assert_eq!(ContentSecurityPolicy::new().form_action(&[SELF]).header_value(None), "form-action 'self'");
345 /// ```
346 #[must_use]
347 pub fn form_action(self, sources: &[&str]) -> Self {
348 self.directive("form-action", sources)
349 }
350
351 /// `base-uri`: allowed `<base href>` values.
352 ///
353 /// # Examples
354 ///
355 /// ```
356 /// use ocre::security::{ContentSecurityPolicy, SELF};
357 ///
358 /// assert_eq!(ContentSecurityPolicy::new().base_uri(&[SELF]).header_value(None), "base-uri 'self'");
359 /// ```
360 #[must_use]
361 pub fn base_uri(self, sources: &[&str]) -> Self {
362 self.directive("base-uri", sources)
363 }
364
365 /// `upgrade-insecure-requests`: browsers load `http:` resources over HTTPS.
366 ///
367 /// # Examples
368 ///
369 /// ```
370 /// use ocre::security::ContentSecurityPolicy;
371 ///
372 /// let csp = ContentSecurityPolicy::new().upgrade_insecure_requests();
373 /// assert_eq!(csp.header_value(None), "upgrade-insecure-requests");
374 /// ```
375 #[must_use]
376 pub fn upgrade_insecure_requests(self) -> Self {
377 self.directive("upgrade-insecure-requests", &[])
378 }
379
380 /// `report-uri`: where browsers POST violation reports (JSON), e.g. a route of the app.
381 ///
382 /// # Examples
383 ///
384 /// ```
385 /// use ocre::security::ContentSecurityPolicy;
386 ///
387 /// let csp = ContentSecurityPolicy::new().report_uri("/csp-reports");
388 /// assert_eq!(csp.header_value(None), "report-uri /csp-reports");
389 /// ```
390 #[must_use]
391 pub fn report_uri(self, uri: &str) -> Self {
392 self.directive("report-uri", &[uri])
393 }
394
395 /// `report-to`: the `Reporting-Endpoints` group violation reports go to.
396 ///
397 /// Send the `Reporting-Endpoints: csp="/csp-reports"` header too
398 /// (browsers without Reporting API support use [`report_uri`](Self::report_uri)).
399 ///
400 /// # Examples
401 ///
402 /// ```
403 /// use ocre::security::ContentSecurityPolicy;
404 ///
405 /// assert_eq!(ContentSecurityPolicy::new().report_to("csp").header_value(None), "report-to csp");
406 /// ```
407 #[must_use]
408 pub fn report_to(self, group: &str) -> Self {
409 self.directive("report-to", &[group])
410 }
411
412 /// Sends `Content-Security-Policy-Report-Only`: browsers report violations but block nothing.
413 ///
414 /// # Examples
415 ///
416 /// ```
417 /// use ocre::security::{ContentSecurityPolicy, SELF};
418 ///
419 /// let csp = ContentSecurityPolicy::new().default_src(&[SELF]).report_only();
420 /// assert_eq!(csp.header_name(), "content-security-policy-report-only");
421 /// ```
422 #[must_use]
423 pub fn report_only(mut self) -> Self {
424 self.report_only = true;
425 self
426 }
427
428 /// `content-security-policy`, or `content-security-policy-report-only` after
429 /// [`report_only`](Self::report_only).
430 ///
431 /// # Examples
432 ///
433 /// ```
434 /// assert_eq!(ocre::security::ContentSecurityPolicy::new().header_name(), "content-security-policy");
435 /// ```
436 pub fn header_name(&self) -> HeaderName {
437 if self.report_only { header::CONTENT_SECURITY_POLICY_REPORT_ONLY } else { header::CONTENT_SECURITY_POLICY }
438 }
439
440 /// The header value, with [`NONCE`] replaced by `'nonce-<nonce>'` (and dropped when `nonce` is `None`).
441 ///
442 /// # Examples
443 ///
444 /// ```
445 /// use ocre::security::{ContentSecurityPolicy, NONCE, SELF};
446 ///
447 /// let csp = ContentSecurityPolicy::new().default_src(&[SELF]).script_src(&[SELF, NONCE]);
448 /// assert_eq!(csp.header_value(Some("n0nce")), "default-src 'self'; script-src 'self' 'nonce-n0nce'");
449 /// assert_eq!(csp.header_value(None), "default-src 'self'; script-src 'self'");
450 /// ```
451 pub fn header_value(&self, nonce: Option<&str>) -> String {
452 let mut value = String::new();
453 for (name, sources) in &self.directives {
454 if !value.is_empty() {
455 value.push_str("; ");
456 }
457 value.push_str(name);
458 for source in sources {
459 if source == NONCE {
460 let Some(nonce) = nonce else { continue };
461 value.push_str(" 'nonce-");
462 value.push_str(nonce);
463 value.push('\'');
464 } else {
465 value.push(' ');
466 value.push_str(source);
467 }
468 }
469 }
470 value
471 }
472
473 fn uses_nonce(&self) -> bool {
474 self.directives.iter().any(|(_, sources)| sources.iter().any(|source| source == NONCE))
475 }
476}
477
478/// The request's Content-Security-Policy nonce, as an extractor (Rails' `content_security_policy_nonce`).
479///
480/// A new random value (16 bytes, base64) per request, created by the
481/// [`ContentSecurityPolicy`] layer and sent as `'nonce-<value>'` wherever the
482/// policy lists [`NONCE`]. Pass it to the template and write
483/// `<script nonce="{{ nonce }}">`; `<meta name="csp-nonce" content="{{ nonce }}">`
484/// exposes it to JavaScript (Rails' `csp_meta_tag`). Rejects with
485/// [`Error::Internal`] when no policy layer wraps the route.
486///
487/// # Examples
488///
489/// ```no_run
490/// use axum::response::Html;
491/// use ocre::security::CspNonce;
492///
493/// async fn page(nonce: CspNonce) -> Html<String> {
494/// Html(format!(r#"<script nonce="{nonce}">console.log("allowed")</script>"#))
495/// }
496/// # let _ = page;
497/// ```
498#[derive(Debug, Clone, PartialEq, Eq)]
499pub struct CspNonce(String);
500
501impl CspNonce {
502 fn generate() -> Self {
503 Self(STANDARD.encode(crate::token::random_bytes::<16>()))
504 }
505
506 /// The nonce value, without the `'nonce-...'` wrapper.
507 ///
508 /// # Examples
509 ///
510 /// ```no_run
511 /// async fn page(nonce: ocre::security::CspNonce) -> String {
512 /// nonce.as_str().to_owned()
513 /// }
514 /// # let _ = page;
515 /// ```
516 pub fn as_str(&self) -> &str {
517 &self.0
518 }
519}
520
521impl fmt::Display for CspNonce {
522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523 f.write_str(&self.0)
524 }
525}
526
527impl<S: Sync> FromRequestParts<S> for CspNonce {
528 type Rejection = Rejection;
529
530 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Rejection> {
531 parts.extensions.get::<CspNonce>().cloned().ok_or_else(|| {
532 reject(Error::internal(
533 "no CSP nonce on this request. Fix: add `.layer(content_security_policy())` (an \
534 `ocre::security::ContentSecurityPolicy`) to the router serving this route",
535 ))
536 })
537 }
538}
539
540/// A `Permissions-Policy` header, built feature by feature, and the layer that sends it.
541///
542/// Rails' `permissions_policy` initializer: it turns browser features
543/// (camera, microphone, geolocation, ...) off for the app and for the frames
544/// it embeds. Add it with `.layer(policy)`; like [`ContentSecurityPolicy`],
545/// a handler's own header wins, so routes can override it.
546///
547/// In allowlists, [`SELF`] (or `"self"`) and `"*"` are keywords; other
548/// entries are origins, quoted in the header.
549///
550/// # Free plan
551///
552/// One header per response; no binding call.
553///
554/// # Examples
555///
556/// ```
557/// use axum::{Router, routing::get};
558/// use ocre::security::{PermissionsPolicy, SELF};
559///
560/// let policy = PermissionsPolicy::new()
561/// .deny(&["camera", "microphone", "geolocation"])
562/// .allow("fullscreen", &[SELF, "https://player.example"]);
563/// assert_eq!(
564/// policy.header_value(),
565/// r#"camera=(), microphone=(), geolocation=(), fullscreen=(self "https://player.example")"#
566/// );
567///
568/// let app: Router = Router::new().route("/", get(|| async { "home" })).layer(policy);
569/// # let _ = app;
570/// ```
571#[derive(Debug, Clone, Default, PartialEq, Eq)]
572pub struct PermissionsPolicy {
573 features: Vec<(String, Vec<String>)>,
574}
575
576impl PermissionsPolicy {
577 /// An empty policy: add features with [`allow`](Self::allow) and [`deny`](Self::deny).
578 ///
579 /// # Examples
580 ///
581 /// ```
582 /// assert_eq!(ocre::security::PermissionsPolicy::new().header_value(), "");
583 /// ```
584 pub fn new() -> Self {
585 Self::default()
586 }
587
588 /// Allows `feature` for the listed origins only; replaces a previous rule for it.
589 ///
590 /// # Examples
591 ///
592 /// ```
593 /// use ocre::security::{PermissionsPolicy, SELF};
594 ///
595 /// assert_eq!(PermissionsPolicy::new().allow("geolocation", &[SELF]).header_value(), "geolocation=(self)");
596 /// assert_eq!(PermissionsPolicy::new().allow("autoplay", &["*"]).header_value(), "autoplay=*");
597 /// ```
598 #[must_use]
599 pub fn allow(mut self, feature: &str, allowlist: &[&str]) -> Self {
600 let allowlist = allowlist
601 .iter()
602 .map(|origin| match *origin {
603 SELF | "self" => "self".to_owned(),
604 "*" => "*".to_owned(),
605 origin => format!("\"{}\"", origin.replace(['"', '\\'], "")),
606 })
607 .collect();
608 match self.features.iter_mut().find(|(existing, _)| existing == feature) {
609 Some((_, existing)) => *existing = allowlist,
610 None => self.features.push((feature.to_owned(), allowlist)),
611 }
612 self
613 }
614
615 /// Turns `features` off everywhere: `camera=()`.
616 ///
617 /// # Examples
618 ///
619 /// ```
620 /// use ocre::security::PermissionsPolicy;
621 ///
622 /// assert_eq!(PermissionsPolicy::new().deny(&["camera", "usb"]).header_value(), "camera=(), usb=()");
623 /// ```
624 #[must_use]
625 pub fn deny(self, features: &[&str]) -> Self {
626 features.iter().fold(self, |policy, feature| policy.allow(feature, &[]))
627 }
628
629 /// The header value.
630 ///
631 /// # Examples
632 ///
633 /// ```
634 /// let policy = ocre::security::PermissionsPolicy::new().deny(&["payment"]);
635 /// assert_eq!(policy.header_value(), "payment=()");
636 /// ```
637 pub fn header_value(&self) -> String {
638 let rules: Vec<String> = self
639 .features
640 .iter()
641 .map(|(feature, allowlist)| match allowlist.as_slice() {
642 [star] if star == "*" => format!("{feature}=*"),
643 _ => format!("{feature}=({})", allowlist.join(" ")),
644 })
645 .collect();
646 rules.join(", ")
647 }
648}
649
650mod sealed {
651 use axum::{
652 extract::Request,
653 http::{HeaderName, HeaderValue},
654 };
655
656 /// What a policy layer adds to a request and its response. Sealed:
657 /// implemented by [`ContentSecurityPolicy`](super::ContentSecurityPolicy)
658 /// and [`PermissionsPolicy`](super::PermissionsPolicy) only.
659 pub trait Policy: Send + Sync + 'static {
660 /// The header name and value for this request; may add extensions (the nonce).
661 fn prepare(&self, req: &mut Request) -> (HeaderName, HeaderValue);
662 /// Headers that, when the response already has one, mean the handler chose its own policy.
663 fn overridden_by(&self) -> [HeaderName; 2];
664 }
665}
666
667use sealed::Policy;
668
669impl Policy for ContentSecurityPolicy {
670 fn prepare(&self, req: &mut Request) -> (HeaderName, HeaderValue) {
671 let nonce = match req.extensions().get::<CspNonce>() {
672 Some(nonce) => nonce.clone(),
673 None => CspNonce::generate(),
674 };
675 let value = self.header_value(self.uses_nonce().then_some(nonce.as_str()));
676 req.extensions_mut().insert(nonce);
677 (self.header_name(), header_value(value))
678 }
679
680 fn overridden_by(&self) -> [HeaderName; 2] {
681 [header::CONTENT_SECURITY_POLICY, header::CONTENT_SECURITY_POLICY_REPORT_ONLY]
682 }
683}
684
685impl Policy for PermissionsPolicy {
686 fn prepare(&self, _req: &mut Request) -> (HeaderName, HeaderValue) {
687 (permissions_policy(), header_value(self.header_value()))
688 }
689
690 fn overridden_by(&self) -> [HeaderName; 2] {
691 [permissions_policy(), permissions_policy()]
692 }
693}
694
695fn permissions_policy() -> HeaderName {
696 HeaderName::from_static("permissions-policy")
697}
698
699/// Sources come from app code; characters a header cannot carry are dropped.
700fn header_value(value: String) -> HeaderValue {
701 HeaderValue::try_from(value).unwrap_or_else(|err| {
702 crate::error::log_internal(&format!("invalid security policy header, not sent: {err}"));
703 HeaderValue::from_static("")
704 })
705}
706
707impl<S> Layer<S> for ContentSecurityPolicy {
708 type Service = PolicyService<S, ContentSecurityPolicy>;
709
710 fn layer(&self, inner: S) -> Self::Service {
711 PolicyService { inner, policy: Arc::new(self.clone()) }
712 }
713}
714
715impl<S> Layer<S> for PermissionsPolicy {
716 type Service = PolicyService<S, PermissionsPolicy>;
717
718 fn layer(&self, inner: S) -> Self::Service {
719 PolicyService { inner, policy: Arc::new(self.clone()) }
720 }
721}
722
723/// The service a [`ContentSecurityPolicy`] or [`PermissionsPolicy`] layer wraps routes in.
724///
725/// Not used directly: `router.layer(policy)` builds it.
726///
727/// # Examples
728///
729/// ```
730/// use axum::{Router, routing::get};
731/// use ocre::security::{PermissionsPolicy, PolicyService};
732/// use tower_layer::Layer as _;
733///
734/// let router: Router = Router::new().route("/", get(|| async { "home" }));
735/// let service: PolicyService<Router, PermissionsPolicy> = PermissionsPolicy::new().layer(router);
736/// # let _ = service;
737/// ```
738pub struct PolicyService<S, P> {
739 inner: S,
740 policy: Arc<P>,
741}
742
743impl<S: Clone, P> Clone for PolicyService<S, P> {
744 fn clone(&self) -> Self {
745 Self { inner: self.inner.clone(), policy: Arc::clone(&self.policy) }
746 }
747}
748
749impl<S, P> Service<Request> for PolicyService<S, P>
750where
751 S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
752 S::Future: Send + 'static,
753 P: Policy,
754{
755 type Response = Response;
756 type Error = Infallible;
757 type Future = Pin<Box<dyn Future<Output = Result<Response, Infallible>> + Send>>;
758
759 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
760 self.inner.poll_ready(cx)
761 }
762
763 fn call(&mut self, mut req: Request) -> Self::Future {
764 let (name, value) = self.policy.prepare(&mut req);
765 let overridden_by = self.policy.overridden_by();
766 // The service polled ready is the one that must be called.
767 let clone = self.inner.clone();
768 let mut inner = std::mem::replace(&mut self.inner, clone);
769 Box::pin(async move {
770 let mut response = inner.call(req).await?;
771 let headers = response.headers_mut();
772 if !overridden_by.iter().any(|name| headers.contains_key(name)) && !value.is_empty() {
773 headers.insert(name, value);
774 }
775 Ok(response)
776 })
777 }
778}
779
780#[cfg(test)]
781#[path = "../../tests/security/policy.rs"]
782mod tests;