rocket/shield/policy.rs
1//! Module containing the [`Policy`] trait and types that implement it.
2
3use std::fmt;
4use std::borrow::Cow;
5
6use indexmap::IndexMap;
7use rocket_http::{ext::IntoCollection, private::SmallVec};
8use time::Duration;
9
10use crate::http::{Header, uri::Absolute, uncased::{UncasedStr, Uncased}};
11
12/// Trait implemented by security and privacy policy headers.
13///
14/// Types that implement this trait can be [`enable()`]d and [`disable()`]d on
15/// instances of [`Shield`].
16///
17/// [`Shield`]: crate::shield::Shield
18/// [`enable()`]: crate::shield::Shield::enable()
19/// [`disable()`]: crate::shield::Shield::disable()
20pub trait Policy: Default + Send + Sync + 'static {
21 /// The actual name of the HTTP header.
22 ///
23 /// This name must uniquely identify the header as it is used to determine
24 /// whether two implementations of `Policy` are for the same header. Use the
25 /// real HTTP header's name.
26 ///
27 /// # Example
28 ///
29 /// ```rust
30 /// # extern crate rocket;
31 /// # use rocket::http::Header;
32 /// use rocket::shield::Policy;
33 ///
34 /// #[derive(Default)]
35 /// struct MyPolicy;
36 ///
37 /// impl Policy for MyPolicy {
38 /// const NAME: &'static str = "X-My-Policy";
39 /// # fn header(&self) -> Header<'static> { unimplemented!() }
40 /// }
41 /// ```
42 const NAME: &'static str;
43
44 /// Returns the [`Header`](../../rocket/http/struct.Header.html) to attach
45 /// to all outgoing responses.
46 ///
47 /// # Example
48 ///
49 /// ```rust
50 /// # extern crate rocket;
51 /// use rocket::http::Header;
52 /// use rocket::shield::Policy;
53 ///
54 /// #[derive(Default)]
55 /// struct MyPolicy;
56 ///
57 /// impl Policy for MyPolicy {
58 /// # const NAME: &'static str = "X-My-Policy";
59 /// fn header(&self) -> Header<'static> {
60 /// Header::new(Self::NAME, "value-to-enable")
61 /// }
62 /// }
63 /// ```
64 fn header(&self) -> Header<'static>;
65}
66
67/// Hack to make `Policy` Object-Safe.
68pub(crate) trait SubPolicy: Send + Sync {
69 fn name(&self) -> &'static UncasedStr;
70 fn header(&self) -> Header<'static>;
71}
72
73impl<P: Policy> SubPolicy for P {
74 fn name(&self) -> &'static UncasedStr {
75 P::NAME.into()
76 }
77
78 fn header(&self) -> Header<'static> {
79 Policy::header(self)
80 }
81}
82
83macro_rules! impl_policy {
84 ($T:ty, $name:expr) => (
85 impl Policy for $T {
86 const NAME: &'static str = $name;
87
88 fn header(&self) -> Header<'static> {
89 self.into()
90 }
91 }
92 )
93}
94
95// Keep this in-sync with the top-level module docs.
96impl_policy!(XssFilter, "X-XSS-Protection");
97impl_policy!(NoSniff, "X-Content-Type-Options");
98impl_policy!(Frame, "X-Frame-Options");
99impl_policy!(Hsts, "Strict-Transport-Security");
100impl_policy!(ExpectCt, "Expect-CT");
101impl_policy!(Referrer, "Referrer-Policy");
102impl_policy!(Prefetch, "X-DNS-Prefetch-Control");
103impl_policy!(Permission, "Permissions-Policy");
104
105/// The [Referrer-Policy] header: controls the value set by the browser for the
106/// [Referer] header.
107///
108/// Tells the browser if it should send all or part of URL of the current page
109/// to the next site the user navigates to via the [Referer] header. This can be
110/// important for security as the URL itself might expose sensitive data, such
111/// as a hidden file path or personal identifier.
112///
113/// [Referrer-Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
114/// [Referer]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
115pub enum Referrer {
116 /// Omits the `Referer` header (returned by [`Referrer::default()`]).
117 NoReferrer,
118
119 /// Omits the `Referer` header on connection downgrade i.e. following HTTP
120 /// link from HTTPS site (_Browser default_).
121 NoReferrerWhenDowngrade,
122
123 /// Only send the origin of part of the URL, e.g. the origin of
124 /// `https://foo.com/bob.html` is `https://foo.com`.
125 Origin,
126
127 /// Send full URL for same-origin requests, only send origin part when
128 /// replying to [cross-origin] requests.
129 ///
130 /// [cross-origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
131 OriginWhenCrossOrigin,
132
133 /// Send full URL for same-origin requests only.
134 SameOrigin,
135
136 /// Only send origin part of URL, only send if protocol security level
137 /// remains the same e.g. HTTPS to HTTPS.
138 StrictOrigin,
139
140 /// Send full URL for same-origin requests. For cross-origin requests, only
141 /// send origin part of URL if protocol security level remains the same e.g.
142 /// HTTPS to HTTPS.
143 StrictOriginWhenCrossOrigin,
144
145 /// Send full URL for same-origin or cross-origin requests. _This will leak
146 /// the full URL of TLS protected resources to insecure origins. Use with
147 /// caution._
148 UnsafeUrl,
149 }
150
151/// Defaults to [`Referrer::NoReferrer`]. Tells the browser to omit the
152/// `Referer` header.
153impl Default for Referrer {
154 fn default() -> Referrer {
155 Referrer::NoReferrer
156 }
157}
158
159impl From<&Referrer> for Header<'static> {
160 fn from(referrer: &Referrer) -> Self {
161 let policy_string = match referrer {
162 Referrer::NoReferrer => "no-referrer",
163 Referrer::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
164 Referrer::Origin => "origin",
165 Referrer::OriginWhenCrossOrigin => "origin-when-cross-origin",
166 Referrer::SameOrigin => "same-origin",
167 Referrer::StrictOrigin => "strict-origin",
168 Referrer::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
169 Referrer::UnsafeUrl => "unsafe-url",
170 };
171
172 Header::new(Referrer::NAME, policy_string)
173 }
174}
175
176/// The [Expect-CT] header: enables reporting and/or enforcement of [Certificate
177/// Transparency].
178///
179/// [Certificate Transparency] can detect and prevent the use of incorrectly
180/// issued malicious, or revoked TLS certificates. It solves a variety of
181/// problems with public TLS/SSL certificate management and is valuable measure
182/// for all public TLS applications.
183///
184/// If you're just [getting started] with certificate transparency, ensure that
185/// your [site is in compliance][getting started] before you enable enforcement
186/// with [`ExpectCt::Enforce`] or [`ExpectCt::ReportAndEnforce`]. Failure to do
187/// so will result in the browser refusing to communicate with your application.
188/// _You have been warned_.
189///
190/// [Expect-CT]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
191/// [Certificate Transparency]: http://www.certificate-transparency.org/what-is-ct
192/// [getting started]: http://www.certificate-transparency.org/getting-started
193pub enum ExpectCt {
194 /// Enforce certificate compliance for the next [`Duration`]. Ensure that
195 /// your certificates are in compliance before turning on enforcement.
196 /// (_Shield_ default).
197 Enforce(Duration),
198
199 /// Report to `Absolute`, but do not enforce, compliance violations for the
200 /// next [`Duration`]. Doesn't provide any protection but is a good way make
201 /// sure things are working correctly before turning on enforcement in
202 /// production.
203 Report(Duration, Absolute<'static>),
204
205 /// Enforce compliance and report violations to `Absolute` for the next
206 /// [`Duration`].
207 ReportAndEnforce(Duration, Absolute<'static>),
208}
209
210/// Defaults to [`ExpectCt::Enforce`] with a 30 day duration, enforce CT
211/// compliance, see [draft] standard for more.
212///
213/// [draft]: https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-03#page-15
214impl Default for ExpectCt {
215 fn default() -> ExpectCt {
216 ExpectCt::Enforce(Duration::days(30))
217 }
218}
219
220impl From<&ExpectCt> for Header<'static> {
221 fn from(expect: &ExpectCt) -> Self {
222 let policy_string = match expect {
223 ExpectCt::Enforce(age) => format!("max-age={}, enforce", age.whole_seconds()),
224 ExpectCt::Report(age, uri) => {
225 format!(r#"max-age={}, report-uri="{}""#, age.whole_seconds(), uri)
226 }
227 ExpectCt::ReportAndEnforce(age, uri) => {
228 format!("max-age={}, enforce, report-uri=\"{}\"", age.whole_seconds(), uri)
229 }
230 };
231
232 Header::new(ExpectCt::NAME, policy_string)
233 }
234}
235
236/// The [X-Content-Type-Options] header: turns off [mime sniffing] which can
237/// prevent certain [attacks].
238///
239/// [mime sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing
240/// [X-Content-Type-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
241/// [attacks]: https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/
242pub enum NoSniff {
243 /// Turns off mime sniffing.
244 Enable,
245}
246
247/// Defaults to [`NoSniff::Enable`], turns off mime sniffing.
248impl Default for NoSniff {
249 fn default() -> NoSniff {
250 NoSniff::Enable
251 }
252}
253
254impl From<&NoSniff> for Header<'static> {
255 fn from(_: &NoSniff) -> Self {
256 Header::new(NoSniff::NAME, "nosniff")
257 }
258}
259
260/// The HTTP [Strict-Transport-Security] (HSTS) header: enforces strict HTTPS
261/// usage.
262///
263/// HSTS tells the browser that the site should only be accessed using HTTPS
264/// instead of HTTP. HSTS prevents a variety of downgrading attacks and should
265/// always be used when [TLS] is enabled. `Shield` will turn HSTS on and issue a
266/// warning if you enable TLS without enabling HSTS when the application is run
267/// in non-debug profiles.
268///
269/// While HSTS is important for HTTPS security, incorrectly configured HSTS can
270/// lead to problems as you are disallowing access to non-HTTPS enabled parts of
271/// your site. [Yelp engineering] has good discussion of potential challenges
272/// that can arise and how to roll this out in a large scale setting. So, if
273/// you use TLS, use HSTS, but roll it out with care.
274///
275/// [TLS]: https://rocket.rs/guide/configuration/#configuring-tls
276/// [Strict-Transport-Security]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
277/// [Yelp engineering]: https://engineeringblog.yelp.com/2017/09/the-road-to-hsts.html
278#[derive(PartialEq, Copy, Clone)]
279pub enum Hsts {
280 /// Browser should only permit this site to be accesses by HTTPS for the
281 /// next [`Duration`].
282 Enable(Duration),
283
284 /// Like [`Hsts::Enable`], but also apply to all of the site's subdomains.
285 IncludeSubDomains(Duration),
286
287 /// Send a "preload" HSTS header, which requests inclusion in the HSTS
288 /// preload list. This variant implies [`Hsts::IncludeSubDomains`], which
289 /// implies [`Hsts::Enable`].
290 ///
291 /// The provided `Duration` must be _at least_ 365 days. If the duration
292 /// provided is less than 365 days, the header will be written out with a
293 /// `max-age` of 365 days.
294 ///
295 /// # Details
296 ///
297 /// Google maintains an [HSTS preload service] that can be used to prevent
298 /// the browser from ever connecting to your site over an insecure
299 /// connection. Read more at [MDN]. Don't enable this before you have
300 /// registered your site and you ensure that it meets the requirements
301 /// specified by the preload service.
302 ///
303 /// [HSTS preload service]: https://hstspreload.org/
304 /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security
305 Preload(Duration),
306}
307
308/// Defaults to `Hsts::Enable(Duration::days(365))`.
309impl Default for Hsts {
310 fn default() -> Hsts {
311 Hsts::Enable(Duration::days(365))
312 }
313}
314
315impl From<&Hsts> for Header<'static> {
316 fn from(hsts: &Hsts) -> Self {
317 if hsts == &Hsts::default() {
318 static DEFAULT: Header<'static> = Header {
319 name: Uncased::from_borrowed(Hsts::NAME),
320 value: Cow::Borrowed("max-age=31536000")
321 };
322
323 return DEFAULT.clone();
324 }
325
326 let policy_string = match hsts {
327 Hsts::Enable(age) => format!("max-age={}", age.whole_seconds()),
328 Hsts::IncludeSubDomains(age) => {
329 format!("max-age={}; includeSubDomains", age.whole_seconds())
330 }
331 Hsts::Preload(age) => {
332 // Google says it needs to be >= 365 days for preload list.
333 static YEAR: Duration = Duration::seconds(31536000);
334
335 format!("max-age={}; includeSubDomains; preload", age.max(&YEAR).whole_seconds())
336 }
337 };
338
339 Header::new(Hsts::NAME, policy_string)
340 }
341}
342
343/// The [X-Frame-Options] header: helps prevent [clickjacking] attacks.
344///
345/// Controls whether the browser should allow the page to render in a `<frame>`,
346/// [`<iframe>`][iframe] or `<object>`. This can be used to prevent
347/// [clickjacking] attacks.
348///
349/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
350/// [clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
351/// [owasp-clickjacking]: https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet
352/// [iframe]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
353pub enum Frame {
354 /// Page cannot be displayed in a frame.
355 Deny,
356
357 /// Page can only be displayed in a frame if the page trying to render it is
358 /// in the same origin. Interpretation of same-origin is [browser
359 /// dependent][X-Frame-Options].
360 ///
361 /// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
362 SameOrigin,
363}
364
365/// Defaults to [`Frame::SameOrigin`].
366impl Default for Frame {
367 fn default() -> Frame {
368 Frame::SameOrigin
369 }
370}
371
372impl From<&Frame> for Header<'static> {
373 fn from(frame: &Frame) -> Self {
374 let policy_string: &'static str = match frame {
375 Frame::Deny => "DENY",
376 Frame::SameOrigin => "SAMEORIGIN",
377 };
378
379 Header::new(Frame::NAME, policy_string)
380 }
381}
382
383/// The [X-XSS-Protection] header: filters some forms of reflected [XSS]
384/// attacks. Modern browsers do not support or enforce this header.
385///
386/// [X-XSS-Protection]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
387/// [XSS]: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting
388pub enum XssFilter {
389 /// Disables XSS filtering.
390 Disable,
391
392 /// Enables XSS filtering. If XSS is detected, the browser will sanitize
393 /// before rendering the page (_Shield default_).
394 Enable,
395
396 /// Enables XSS filtering. If XSS is detected, the browser will not
397 /// render the page.
398 EnableBlock,
399}
400
401/// Defaults to [`XssFilter::Enable`].
402impl Default for XssFilter {
403 fn default() -> XssFilter {
404 XssFilter::Enable
405 }
406}
407
408impl From<&XssFilter> for Header<'static> {
409 fn from(filter: &XssFilter) -> Self {
410 let policy_string: &'static str = match filter {
411 XssFilter::Disable => "0",
412 XssFilter::Enable => "1",
413 XssFilter::EnableBlock => "1; mode=block",
414 };
415
416 Header::new(XssFilter::NAME, policy_string)
417 }
418}
419
420/// The [X-DNS-Prefetch-Control] header: controls browser DNS prefetching.
421///
422/// Tells the browser if it should perform domain name resolution on both links
423/// that the user may choose to follow as well as URLs for items referenced by
424/// the document including images, CSS, JavaScript, and so forth. Disabling
425/// prefetching is useful if you don't control the link on the pages, or know
426/// that you don't want to leak information to these domains.
427///
428/// [X-DNS-Prefetch-Control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
429pub enum Prefetch {
430 /// Enables DNS prefetching. This is the browser default.
431 On,
432 /// Disables DNS prefetching. This is the shield policy default.
433 Off,
434}
435
436impl Default for Prefetch {
437 fn default() -> Prefetch {
438 Prefetch::Off
439 }
440}
441
442impl From<&Prefetch> for Header<'static> {
443 fn from(prefetch: &Prefetch) -> Self {
444 let policy_string = match prefetch {
445 Prefetch::On => "on",
446 Prefetch::Off => "off",
447 };
448
449 Header::new(Prefetch::NAME, policy_string)
450 }
451}
452
453/// The [Permissions-Policy] header: allow or block the use of browser features.
454///
455/// Tells the browser to allow or block the use of a browser feature in the
456/// top-level page as well as allow or block _requesting access to_ (via the
457/// `allow` `iframe` attribute) features in embedded iframes.
458///
459/// By default, the top-level page may access ~all features and any embedded
460/// iframes may request access to ~any feature. This header allows the server to
461/// control exactly _which_ (if any) origins may access or request access to
462/// browser features.
463///
464/// Features are enabled via the [`Permission::allowed()`] constructor and
465/// chainable [`allow()`](Self::allow()) build method. Features can be blocked
466/// via the [`Permission::blocked()`] and chainable [`block()`](Self::block())
467/// builder method.
468///
469/// ```rust
470 /// # #[macro_use] extern crate rocket;
471/// use rocket::shield::{Shield, Permission, Feature, Allow};
472///
473/// // In addition to defaults, block access to geolocation and USB features.
474/// // Enable camera and microphone features only for the serving origin. Enable
475/// // payment request access for the current origin and `https://rocket.rs`.
476/// let permission = Permission::default()
477/// .block(Feature::Geolocation)
478/// .block(Feature::Usb)
479/// .allow(Feature::Camera, Allow::This)
480/// .allow(Feature::Microphone, Allow::This)
481/// .allow(Feature::Payment, [Allow::This, Allow::Origin(uri!("https://rocket.rs"))]);
482///
483/// rocket::build().attach(Shield::default().enable(permission));
484/// ```
485///
486/// # Default
487///
488/// The default returned via [`Permission::default()`] blocks access to the
489/// `interest-cohort` feature, otherwise known as FLoC, which disables using the
490/// current site in ad targeting tracking computations.
491///
492/// [Permissions-Policy]: https://github.com/w3c/webappsec-permissions-policy/blob/a45df7b237e2a85e1909d7f226ca4eb4ce5095ba/permissions-policy-explainer.md
493#[derive(PartialEq, Clone)]
494pub struct Permission(IndexMap<Feature, Option<SmallVec<[Allow; 1]>>>);
495
496impl Default for Permission {
497 /// The default `Permission` policy blocks access to the `interest-cohort`
498 /// feature, otherwise known as FLoC, which disables using the current site
499 /// in ad targeting tracking computations.
500 fn default() -> Self {
501 Permission::blocked(Feature::InterestCohort)
502 }
503}
504
505impl Permission {
506 /// Constructs a new `Permission` policy with only `feature` allowed for the
507 /// set of origins in `allow` which may be a single [`Allow`], a slice
508 /// (`[Allow]` or `&[Allow]`), or a vector (`Vec<Allow>`).
509 ///
510 /// If `allow` is empty, the use of the feature is blocked unless another
511 /// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
512 /// feature is allowable for all origins. Otherwise, the feature is
513 /// allowable only for the origin specified in `allow`.
514 ///
515 /// # Panics
516 ///
517 /// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
518 /// host part.
519 ///
520 /// # Example
521 ///
522 /// ```rust
523 /// # #[macro_use] extern crate rocket;
524 /// use rocket::shield::{Permission, Feature, Allow};
525 ///
526 /// let rocket = Allow::Origin(uri!("https://rocket.rs"));
527 ///
528 /// let perm = Permission::allowed(Feature::Usb, Allow::This);
529 /// let perm = Permission::allowed(Feature::Usb, Allow::Any);
530 /// let perm = Permission::allowed(Feature::Usb, [Allow::This, rocket]);
531 /// ```
532 pub fn allowed<L>(feature: Feature, allow: L) -> Self
533 where L: IntoCollection<Allow>
534 {
535 Permission(IndexMap::new()).allow(feature, allow)
536 }
537
538 /// Constructs a new `Permission` policy with only `feature` blocked.
539 ///
540 /// # Example
541 ///
542 /// ```rust
543 /// use rocket::shield::{Permission, Feature};
544 ///
545 /// let perm = Permission::blocked(Feature::Usb);
546 /// let perm = Permission::blocked(Feature::Payment);
547 /// ```
548 pub fn blocked(feature: Feature) -> Self {
549 Permission(IndexMap::new()).block(feature)
550 }
551
552 /// Adds `feature` as allowable for the set of origins in `allow` which may
553 /// be a single [`Allow`], a slice (`[Allow]` or `&[Allow]`), or a vector
554 /// (`Vec<Allow>`).
555 ///
556 /// This policy supersedes any previous policy set for `feature`.
557 ///
558 /// If `allow` is empty, the use of the feature is blocked unless another
559 /// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
560 /// feature is allowable for all origins. Otherwise, the feature is
561 /// allowable only for the origin specified in `allow`.
562 ///
563 /// # Panics
564 ///
565 /// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
566 /// host part.
567 ///
568 /// # Example
569 ///
570 /// ```rust
571 /// # #[macro_use] extern crate rocket;
572 /// use rocket::shield::{Permission, Feature, Allow};
573 ///
574 /// let rocket = Allow::Origin(uri!("https://rocket.rs"));
575 /// let perm = Permission::allowed(Feature::Usb, Allow::This)
576 /// .allow(Feature::Payment, [rocket, Allow::This]);
577 /// ```
578 pub fn allow<L>(mut self, feature: Feature, allow: L) -> Self
579 where L: IntoCollection<Allow>
580 {
581 let mut allow = allow.into_collection();
582
583 if allow.contains(&Allow::Any) {
584 allow = Allow::Any.into_collection();
585 }
586
587 for allow in &allow {
588 if let Allow::Origin(absolute) = allow {
589 let auth = absolute.authority();
590 if auth.is_none() || matches!(auth, Some(a) if a.host().is_empty()) {
591 panic!("...")
592 }
593 }
594 }
595
596 self.0.insert(feature, Some(allow));
597 self
598 }
599
600 /// Blocks `feature`. This policy supersedes any previous policy set for
601 /// `feature`.
602 ///
603 /// # Example
604 ///
605 /// ```rust
606 /// use rocket::shield::{Permission, Feature};
607 ///
608 /// let perm = Permission::default()
609 /// .block(Feature::Usb)
610 /// .block(Feature::Payment);
611 /// ```
612 pub fn block(mut self, feature: Feature) -> Self {
613 self.0.insert(feature, None);
614 self
615 }
616
617 /// Returns the allow list (so far) for `feature` if feature is allowed.
618 ///
619 /// # Example
620 ///
621 /// ```rust
622 /// use rocket::shield::{Permission, Feature, Allow};
623 ///
624 /// let perm = Permission::default();
625 /// assert!(perm.get(Feature::Usb).is_none());
626 ///
627 /// let perm = perm.allow(Feature::Usb, Allow::Any);
628 /// assert_eq!(perm.get(Feature::Usb).unwrap(), &[Allow::Any]);
629 /// ```
630 pub fn get(&self, feature: Feature) -> Option<&[Allow]> {
631 self.0.get(&feature)?.as_deref()
632 }
633
634 /// Returns an iterator over the pairs of features and their allow lists,
635 /// `None` if the feature is blocked.
636 ///
637 /// Features are returned in the order in which they were first added.
638 ///
639 /// # Example
640 ///
641 /// ```rust
642 /// # #[macro_use] extern crate rocket;
643 /// use rocket::shield::{Permission, Feature, Allow};
644 ///
645 /// let foo = uri!("https://foo.com:1234");
646 /// let perm = Permission::blocked(Feature::Camera)
647 /// .allow(Feature::Gyroscope, [Allow::This, Allow::Origin(foo.clone())])
648 /// .block(Feature::Payment)
649 /// .allow(Feature::Camera, Allow::Any);
650 ///
651 /// let perms: Vec<_> = perm.iter().collect();
652 /// assert_eq!(perms.len(), 3);
653 /// assert_eq!(perms, vec![
654 /// (Feature::Camera, Some(&[Allow::Any][..])),
655 /// (Feature::Gyroscope, Some(&[Allow::This, Allow::Origin(foo)][..])),
656 /// (Feature::Payment, None),
657 /// ]);
658 /// ```
659 pub fn iter(&self) -> impl Iterator<Item = (Feature, Option<&[Allow]>)> {
660 self.0.iter().map(|(feature, list)| (*feature, list.as_deref()))
661 }
662}
663
664impl From<&Permission> for Header<'static> {
665 fn from(perm: &Permission) -> Self {
666 if perm == &Permission::default() {
667 static DEFAULT: Header<'static> = Header {
668 name: Uncased::from_borrowed(Permission::NAME),
669 value: Cow::Borrowed("interest-cohort=()")
670 };
671
672 return DEFAULT.clone();
673 }
674
675 let value = perm.0.iter()
676 .map(|(feature, allow)| {
677 let list = allow.as_ref()
678 .into_iter()
679 .flatten()
680 .map(|origin| origin.rendered())
681 .collect::<Vec<_>>()
682 .join(" ");
683
684 format!("{}=({})", feature, list)
685 })
686 .collect::<Vec<_>>()
687 .join(", ");
688
689 Header::new(Permission::NAME, value)
690 }
691}
692
693/// Specifies the origin(s) allowed to access a browser [`Feature`] via
694/// [`Permission`].
695#[derive(Debug, PartialEq, Clone)]
696pub enum Allow {
697 /// Allow this specific origin. The feature is allowed only for this
698 /// specific origin.
699 ///
700 /// The `user_info`, `path`, and `query` parts of the URI, if any, are
701 /// ignored.
702 Origin(Absolute<'static>),
703 /// Any origin at all.
704 ///
705 /// The feature will be allowed in all browsing contexts regardless of their
706 /// origin.
707 Any,
708 /// The current origin.
709 ///
710 /// The feature will be allowed in the immediately returned document and in
711 /// all nested browsing contexts (iframes) in the same origin.
712 This,
713}
714
715impl Allow {
716 fn rendered(&self) -> Cow<'static, str> {
717 match self {
718 Allow::Origin(uri) => {
719 let mut string = String::with_capacity(32);
720 string.push('"');
721 string.push_str(uri.scheme());
722
723 // This should never fail when rendering a header for `Shield`
724 // due to `panic` in `.allow()`.
725 if let Some(auth) = uri.authority() {
726 use std::fmt::Write;
727
728 let _ = write!(string, "://{}", auth.host());
729 if let Some(port) = auth.port() {
730 let _ = write!(string, ":{}", port);
731 }
732 }
733
734 string.push('"');
735 string.into()
736 }
737 Allow::Any => "*".into(),
738 Allow::This => "self".into(),
739 }
740 }
741}
742
743/// A browser feature that can be enabled or blocked via [`Permission`].
744#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
745#[non_exhaustive]
746pub enum Feature {
747 // Standardized.
748
749 /// The "accelerometer" feature.
750 Accelerometer,
751 /// The "ambient-light-sensor" feature.
752 AmbientLightSensor,
753 /// The "autoplay" feature.
754 Autoplay,
755 /// The "battery" feature.
756 Battery,
757 /// The "camera" feature.
758 Camera,
759 /// The "cross-origin-isolated" feature.
760 CrossOriginIsolated,
761 /// The "display-capture" feature.
762 Displaycapture,
763 /// The "document-domain" feature.
764 DocumentDomain,
765 /// The "encrypted-media" feature.
766 EncryptedMedia,
767 /// The "execution-while-not-rendered" feature.
768 ExecutionWhileNotRendered,
769 /// The "execution-while-out-of-viewport" feature.
770 ExecutionWhileOutOfviewport,
771 /// The "fullscreen" feature.
772 Fullscreen,
773 /// The "geolocation" feature.
774 Geolocation,
775 /// The "gyroscope" feature.
776 Gyroscope,
777 /// The "magnetometer" feature.
778 Magnetometer,
779 /// The "microphone" feature.
780 Microphone,
781 /// The "midi" feature.
782 Midi,
783 /// The "navigation-override" feature.
784 NavigationOverride,
785 /// The "payment" feature.
786 Payment,
787 /// The "picture-in-picture" feature.
788 PictureInPicture,
789 /// The "publickey-credentials-get" feature.
790 PublickeyCredentialsGet,
791 /// The "screen-wake-lock" feature.
792 ScreenWakeLock,
793 /// The "sync-xhr" feature.
794 SyncXhr,
795 /// The "usb" feature.
796 Usb,
797 /// The "web-share" feature.
798 WebShare,
799 /// The "xr-spatial-tracking" feature.
800 XrSpatialTracking,
801
802 // Proposed.
803
804 /// The "clipboard-read" feature.
805 ClipboardRead,
806 /// The "clipboard-write" feature.
807 ClipboardWrite,
808 /// The "gamepad" feature.
809 Gamepad,
810 /// The "speaker-selection" feature.
811 SpeakerSelection,
812 /// The "interest-cohort" feature.
813 InterestCohort,
814
815 // Experimental.
816
817 /// The "conversion-measurement" feature.
818 ConversionMeasurement,
819 /// The "focus-without-user-activation" feature.
820 FocusWithoutUserActivation,
821 /// The "hid" feature.
822 Hid,
823 /// The "idle-detection" feature.
824 IdleDetection,
825 /// The "serial" feature.
826 Serial,
827 /// The "sync-script" feature.
828 SyncScript,
829 /// The "trust-token-redemption" feature.
830 TrustTokenRedemption,
831 /// The "vertical-scroll" feature.
832 VerticalScroll,
833}
834
835impl Feature {
836 /// Returns the feature string as it appears in the header.
837 ///
838 /// # Example
839 ///
840 /// ```rust
841 /// use rocket::shield::Feature;
842 ///
843 /// assert_eq!(Feature::Camera.as_str(), "camera");
844 /// assert_eq!(Feature::SyncScript.as_str(), "sync-script");
845 /// ```
846 pub const fn as_str(self) -> &'static str {
847 use Feature::*;
848
849 match self {
850 Accelerometer => "accelerometer",
851 AmbientLightSensor => "ambient-light-sensor",
852 Autoplay => "autoplay",
853 Battery => "battery",
854 Camera => "camera",
855 CrossOriginIsolated => "cross-origin-isolated",
856 Displaycapture => "display-capture",
857 DocumentDomain => "document-domain",
858 EncryptedMedia => "encrypted-media",
859 ExecutionWhileNotRendered => "execution-while-not-rendered",
860 ExecutionWhileOutOfviewport => "execution-while-out-of-viewport",
861 Fullscreen => "fullscreen",
862 Geolocation => "geolocation",
863 Gyroscope => "gyroscope",
864 Magnetometer => "magnetometer",
865 Microphone => "microphone",
866 Midi => "midi",
867 NavigationOverride => "navigation-override",
868 Payment => "payment",
869 PictureInPicture => "picture-in-picture",
870 PublickeyCredentialsGet => "publickey-credentials-get",
871 ScreenWakeLock => "screen-wake-lock",
872 SyncXhr => "sync-xhr",
873 Usb => "usb",
874 WebShare => "web-share",
875 XrSpatialTracking => "xr-spatial-tracking",
876
877 ClipboardRead => "clipboard-read",
878 ClipboardWrite => "clipboard-write",
879 Gamepad => "gamepad",
880 SpeakerSelection => "speaker-selection",
881 InterestCohort => "interest-cohort",
882
883 ConversionMeasurement => "conversion-measurement",
884 FocusWithoutUserActivation => "focus-without-user-activation",
885 Hid => "hid",
886 IdleDetection => "idle-detection",
887 Serial => "serial",
888 SyncScript => "sync-script",
889 TrustTokenRedemption => "trust-token-redemption",
890 VerticalScroll => "vertical-scroll",
891 }
892 }
893}
894
895impl fmt::Display for Feature {
896 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
897 self.as_str().fmt(f)
898 }
899}