rocket_community/shield/
policy.rs

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