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 time::Duration;
8
9use crate::http::{Header, uri::Absolute, uncased::Uncased};
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;
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;
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!("max-age={}, enforce, report-uri=\"{}\"", age.whole_seconds(), uri)
212            }
213        };
214
215        Header::new(ExpectCt::NAME, policy_string)
216    }
217}
218
219/// The [X-Content-Type-Options] header: turns off [mime sniffing] which can
220/// prevent certain [attacks].
221///
222/// [mime sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing
223/// [X-Content-Type-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
224/// [attacks]: https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/
225pub enum NoSniff {
226    /// Turns off mime sniffing.
227    Enable,
228}
229
230/// Defaults to [`NoSniff::Enable`], turns off mime sniffing.
231impl Default for NoSniff {
232    fn default() -> NoSniff {
233        NoSniff::Enable
234    }
235}
236
237impl From<&NoSniff> for Header<'static> {
238    fn from(_: &NoSniff) -> Self {
239        Header::new(NoSniff::NAME, "nosniff")
240    }
241}
242
243/// The HTTP [Strict-Transport-Security] (HSTS) header: enforces strict HTTPS
244/// usage.
245///
246/// HSTS tells the browser that the site should only be accessed using HTTPS
247/// instead of HTTP. HSTS prevents a variety of downgrading attacks and should
248/// always be used when [TLS] is enabled. `Shield` will turn HSTS on and issue a
249/// warning if you enable TLS without enabling HSTS when the application is run
250/// in non-debug profiles.
251///
252/// While HSTS is important for HTTPS security, incorrectly configured HSTS can
253/// lead to problems as you are disallowing access to non-HTTPS enabled parts of
254/// your site. [Yelp engineering] has good discussion of potential challenges
255/// that can arise and how to roll this out in a large scale setting. So, if
256/// you use TLS, use HSTS, but roll it out with care.
257///
258/// [TLS]: https://rocket.rs/guide/configuration/#configuring-tls
259/// [Strict-Transport-Security]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
260/// [Yelp engineering]: https://engineeringblog.yelp.com/2017/09/the-road-to-hsts.html
261#[derive(PartialEq, Copy, Clone)]
262pub enum Hsts {
263    /// Browser should only permit this site to be accesses by HTTPS for the
264    /// next [`Duration`].
265    Enable(Duration),
266
267    /// Like [`Hsts::Enable`], but also apply to all of the site's subdomains.
268    IncludeSubDomains(Duration),
269
270    /// Send a "preload" HSTS header, which requests inclusion in the HSTS
271    /// preload list. This variant implies [`Hsts::IncludeSubDomains`], which
272    /// implies [`Hsts::Enable`].
273    ///
274    /// The provided `Duration` must be _at least_ 365 days. If the duration
275    /// provided is less than 365 days, the header will be written out with a
276    /// `max-age` of 365 days.
277    ///
278    /// # Details
279    ///
280    /// Google maintains an [HSTS preload service] that can be used to prevent
281    /// the browser from ever connecting to your site over an insecure
282    /// connection. Read more at [MDN]. Don't enable this before you have
283    /// registered your site and you ensure that it meets the requirements
284    /// specified by the preload service.
285    ///
286    /// [HSTS preload service]: https://hstspreload.org/
287    /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security
288    Preload(Duration),
289}
290
291/// Defaults to `Hsts::Enable(Duration::days(365))`.
292impl Default for Hsts {
293    fn default() -> Hsts {
294        Hsts::Enable(Duration::days(365))
295    }
296}
297
298impl From<&Hsts> for Header<'static> {
299    fn from(hsts: &Hsts) -> Self {
300        if hsts == &Hsts::default() {
301            static DEFAULT: Header<'static> = Header {
302                name: Uncased::from_borrowed(Hsts::NAME),
303                value: Cow::Borrowed("max-age=31536000")
304            };
305
306            return DEFAULT.clone();
307        }
308
309        let policy_string = match hsts {
310            Hsts::Enable(age) => format!("max-age={}", age.whole_seconds()),
311            Hsts::IncludeSubDomains(age) => {
312                format!("max-age={}; includeSubDomains", age.whole_seconds())
313            }
314            Hsts::Preload(age) => {
315                // Google says it needs to be >= 365 days for preload list.
316                static YEAR: Duration = Duration::seconds(31536000);
317
318                format!("max-age={}; includeSubDomains; preload", age.max(&YEAR).whole_seconds())
319            }
320        };
321
322        Header::new(Hsts::NAME, policy_string)
323    }
324}
325
326/// The [X-Frame-Options] header: helps prevent [clickjacking] attacks.
327///
328/// Controls whether the browser should allow the page to render in a `<frame>`,
329/// [`<iframe>`][iframe] or `<object>`. This can be used to prevent
330/// [clickjacking] attacks.
331///
332/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
333/// [clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
334/// [owasp-clickjacking]: https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet
335/// [iframe]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
336pub enum Frame {
337    /// Page cannot be displayed in a frame.
338    Deny,
339
340    /// Page can only be displayed in a frame if the page trying to render it is
341    /// in the same origin. Interpretation of same-origin is [browser
342    /// dependent][X-Frame-Options].
343    ///
344    /// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
345    SameOrigin,
346}
347
348/// Defaults to [`Frame::SameOrigin`].
349impl Default for Frame {
350    fn default() -> Frame {
351        Frame::SameOrigin
352    }
353}
354
355impl From<&Frame> for Header<'static> {
356    fn from(frame: &Frame) -> Self {
357        let policy_string: &'static str = match frame {
358            Frame::Deny => "DENY",
359            Frame::SameOrigin => "SAMEORIGIN",
360        };
361
362        Header::new(Frame::NAME, policy_string)
363    }
364}
365
366/// The [X-XSS-Protection] header: filters some forms of reflected [XSS]
367/// attacks. Modern browsers do not support or enforce this header.
368///
369/// [X-XSS-Protection]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
370/// [XSS]: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting
371pub enum XssFilter {
372    /// Disables XSS filtering.
373    Disable,
374
375    /// Enables XSS filtering. If XSS is detected, the browser will sanitize
376    /// before rendering the page (_Shield default_).
377    Enable,
378
379    /// Enables XSS filtering. If XSS is detected, the browser will not
380    /// render the page.
381    EnableBlock,
382}
383
384/// Defaults to [`XssFilter::Enable`].
385impl Default for XssFilter {
386    fn default() -> XssFilter {
387        XssFilter::Enable
388    }
389}
390
391impl From<&XssFilter> for Header<'static> {
392    fn from(filter: &XssFilter) -> Self {
393        let policy_string: &'static str = match filter {
394            XssFilter::Disable => "0",
395            XssFilter::Enable => "1",
396            XssFilter::EnableBlock => "1; mode=block",
397        };
398
399        Header::new(XssFilter::NAME, policy_string)
400    }
401}
402
403/// The [X-DNS-Prefetch-Control] header: controls browser DNS prefetching.
404///
405/// Tells the browser if it should perform domain name resolution on both links
406/// that the user may choose to follow as well as URLs for items referenced by
407/// the document including images, CSS, JavaScript, and so forth. Disabling
408/// prefetching is useful if you don't control the link on the pages, or know
409/// that you don't want to leak information to these domains.
410///
411/// [X-DNS-Prefetch-Control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
412#[derive(Default)]
413pub enum Prefetch {
414    /// Enables DNS prefetching. This is the browser default.
415    On,
416    /// Disables DNS prefetching. This is the shield policy default.
417    #[default]
418    Off,
419}
420
421impl From<&Prefetch> for Header<'static> {
422    fn from(prefetch: &Prefetch) -> Self {
423        let policy_string = match prefetch {
424            Prefetch::On => "on",
425            Prefetch::Off => "off",
426        };
427
428        Header::new(Prefetch::NAME, policy_string)
429    }
430}
431
432/// The [Permissions-Policy] header: allow or block the use of browser features.
433///
434/// Tells the browser to allow or block the use of a browser feature in the
435/// top-level page as well as allow or block _requesting access to_ (via the
436/// `allow` `iframe` attribute) features in embedded iframes.
437///
438/// By default, the top-level page may access ~all features and any embedded
439/// iframes may request access to ~any feature. This header allows the server to
440/// control exactly _which_ (if any) origins may access or request access to
441/// browser features.
442///
443/// Features are enabled via the [`Permission::allowed()`] constructor and
444/// chainable [`allow()`](Self::allow()) build method. Features can be blocked
445/// via the [`Permission::blocked()`] and chainable [`block()`](Self::block())
446/// builder method.
447///
448/// ```rust
449    /// # #[macro_use] extern crate rocket;
450/// use rocket::shield::{Shield, Permission, Feature, Allow};
451///
452/// // In addition to defaults, block access to geolocation and USB features.
453/// // Enable camera and microphone features only for the serving origin. Enable
454/// // payment request access for the current origin and `https://rocket.rs`.
455/// let permission = Permission::default()
456///     .block(Feature::Geolocation)
457///     .block(Feature::Usb)
458///     .allow(Feature::Camera, Allow::This)
459///     .allow(Feature::Microphone, Allow::This)
460///     .allow(Feature::Payment, [Allow::This, Allow::Origin(uri!("https://rocket.rs"))]);
461///
462/// rocket::build().attach(Shield::default().enable(permission));
463/// ```
464///
465/// # Default
466///
467/// The default returned via [`Permission::default()`] blocks access to the
468/// `interest-cohort` feature, otherwise known as FLoC, which disables using the
469/// current site in ad targeting tracking computations.
470///
471/// [Permissions-Policy]: https://github.com/w3c/webappsec-permissions-policy/blob/a45df7b237e2a85e1909d7f226ca4eb4ce5095ba/permissions-policy-explainer.md
472#[derive(PartialEq, Clone)]
473pub struct Permission(IndexMap<Feature, Vec<Allow>>);
474
475impl Default for Permission {
476    /// The default `Permission` policy blocks access to the `interest-cohort`
477    /// feature, otherwise known as FLoC, which disables using the current site
478    /// in ad targeting tracking computations.
479    fn default() -> Self {
480        Permission::blocked(Feature::InterestCohort)
481    }
482}
483
484impl Permission {
485    /// Constructs a new `Permission` policy with only `feature` allowed for the
486    /// set of origins in `allow` which may be a single [`Allow`], a slice
487    /// (`[Allow]` or `&[Allow]`), or a vector (`Vec<Allow>`).
488    ///
489    /// If `allow` is empty, the use of the feature is blocked unless another
490    /// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
491    /// feature is allowable for all origins. Otherwise, the feature is
492    /// allowable only for the origin specified in `allow`.
493    ///
494    /// # Panics
495    ///
496    /// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
497    /// host part.
498    ///
499    /// # Example
500    ///
501    /// ```rust
502    /// # #[macro_use] extern crate rocket;
503    /// use rocket::shield::{Permission, Feature, Allow};
504    ///
505    /// let rocket = Allow::Origin(uri!("https://rocket.rs"));
506    ///
507    /// let perm = Permission::allowed(Feature::Usb, Allow::This);
508    /// let perm = Permission::allowed(Feature::Usb, Allow::Any);
509    /// let perm = Permission::allowed(Feature::Usb, [Allow::This, rocket]);
510    /// ```
511    pub fn allowed<L>(feature: Feature, allow: L) -> Self
512        where L: IntoIterator<Item = Allow>
513    {
514        Permission(IndexMap::new()).allow(feature, allow)
515    }
516
517    /// Constructs a new `Permission` policy with only `feature` blocked.
518    ///
519    /// # Example
520    ///
521    /// ```rust
522    /// use rocket::shield::{Permission, Feature};
523    ///
524    /// let perm = Permission::blocked(Feature::Usb);
525    /// let perm = Permission::blocked(Feature::Payment);
526    /// ```
527    pub fn blocked(feature: Feature) -> Self {
528        Permission(IndexMap::new()).block(feature)
529    }
530
531    /// Adds `feature` as allowable for the set of origins in `allow` which may
532    /// be a single [`Allow`], a slice (`[Allow]` or `&[Allow]`), or a vector
533    /// (`Vec<Allow>`).
534    ///
535    /// This policy supersedes any previous policy set for `feature`.
536    ///
537    /// If `allow` is empty, the use of the feature is blocked unless another
538    /// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
539    /// feature is allowable for all origins. Otherwise, the feature is
540    /// allowable only for the origin specified in `allow`.
541    ///
542    /// # Panics
543    ///
544    /// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
545    /// host part.
546    ///
547    /// # Example
548    ///
549    /// ```rust
550    /// # #[macro_use] extern crate rocket;
551    /// use rocket::shield::{Permission, Feature, Allow};
552    ///
553    /// let rocket = Allow::Origin(uri!("https://rocket.rs"));
554    /// let perm = Permission::allowed(Feature::Usb, Allow::This)
555    ///     .allow(Feature::Payment, [rocket, Allow::This]);
556    /// ```
557    pub fn allow<L>(mut self, feature: Feature, allow: L) -> Self
558        where L: IntoIterator<Item = Allow>
559    {
560        let mut allow: Vec<_> = allow.into_iter().collect();
561        for allow in &allow {
562            if let Allow::Origin(absolute) = allow {
563                let auth = absolute.authority();
564                if auth.is_none() || matches!(auth, Some(a) if a.host().is_empty()) {
565                    panic!("...")
566                }
567            }
568        }
569
570        if allow.contains(&Allow::Any) {
571            allow = vec![Allow::Any];
572        }
573
574        self.0.insert(feature, allow);
575        self
576    }
577
578    /// Blocks `feature`. This policy supersedes any previous policy set for
579    /// `feature`.
580    ///
581    /// # Example
582    ///
583    /// ```rust
584    /// use rocket::shield::{Permission, Feature};
585    ///
586    /// let perm = Permission::default()
587    ///     .block(Feature::Usb)
588    ///     .block(Feature::Payment);
589    /// ```
590    pub fn block(mut self, feature: Feature) -> Self {
591        self.0.insert(feature, vec![]);
592        self
593    }
594
595    /// Returns the allow list (so far) for `feature` if feature is allowed.
596    ///
597    /// # Example
598    ///
599    /// ```rust
600    /// use rocket::shield::{Permission, Feature, Allow};
601    ///
602    /// let perm = Permission::default();
603    /// assert!(perm.get(Feature::Usb).is_none());
604    ///
605    /// let perm = perm.allow(Feature::Usb, Allow::Any);
606    /// assert_eq!(perm.get(Feature::Usb).unwrap(), &[Allow::Any]);
607    /// ```
608    pub fn get(&self, feature: Feature) -> Option<&[Allow]> {
609        Some(self.0.get(&feature)?)
610    }
611
612    /// Returns an iterator over the pairs of features and their allow lists,
613    /// empty if the feature is blocked.
614    ///
615    /// Features are returned in the order in which they were first added.
616    ///
617    /// # Example
618    ///
619    /// ```rust
620    /// # #[macro_use] extern crate rocket;
621    /// use rocket::shield::{Permission, Feature, Allow};
622    ///
623    /// let foo = uri!("https://foo.com:1234");
624    /// let perm = Permission::blocked(Feature::Camera)
625    ///     .allow(Feature::Gyroscope, [Allow::This, Allow::Origin(foo.clone())])
626    ///     .block(Feature::Payment)
627    ///     .allow(Feature::Camera, Allow::Any);
628    ///
629    /// let perms: Vec<_> = perm.iter().collect();
630    /// assert_eq!(perms.len(), 3);
631    /// assert_eq!(perms, vec![
632    ///     (Feature::Camera, &[Allow::Any][..]),
633    ///     (Feature::Gyroscope, &[Allow::This, Allow::Origin(foo)][..]),
634    ///     (Feature::Payment, &[][..]),
635    /// ]);
636    /// ```
637    pub fn iter(&self) -> impl Iterator<Item = (Feature, &[Allow])> {
638        self.0.iter().map(|(feature, list)| (*feature, &**list))
639    }
640}
641
642impl From<&Permission> for Header<'static> {
643    fn from(perm: &Permission) -> Self {
644        if perm == &Permission::default() {
645            static DEFAULT: Header<'static> = Header {
646                name: Uncased::from_borrowed(Permission::NAME),
647                value: Cow::Borrowed("interest-cohort=()")
648            };
649
650            return DEFAULT.clone();
651        }
652
653        let value = perm.0.iter()
654            .map(|(feature, allow)| {
655                let list = allow.iter()
656                    .map(|origin| origin.rendered())
657                    .collect::<Vec<_>>()
658                    .join(" ");
659
660                format!("{}=({})", feature, list)
661            })
662            .collect::<Vec<_>>()
663            .join(", ");
664
665        Header::new(Permission::NAME, value)
666    }
667}
668
669/// Specifies the origin(s) allowed to access a browser [`Feature`] via
670/// [`Permission`].
671#[derive(Debug, PartialEq, Clone)]
672pub enum Allow {
673    /// Allow this specific origin. The feature is allowed only for this
674    /// specific origin.
675    ///
676    /// The `user_info`, `path`, and `query` parts of the URI, if any, are
677    /// ignored.
678    Origin(Absolute<'static>),
679    /// Any origin at all.
680    ///
681    /// The feature will be allowed in all browsing contexts regardless of their
682    /// origin.
683    Any,
684    /// The current origin.
685    ///
686    /// The feature will be allowed in the immediately returned document and in
687    /// all nested browsing contexts (iframes) in the same origin.
688    This,
689}
690
691impl Allow {
692    fn rendered(&self) -> Cow<'static, str> {
693        match self {
694            Allow::Origin(uri) => {
695                let mut string = String::with_capacity(32);
696                string.push('"');
697                string.push_str(uri.scheme());
698
699                // This should never fail when rendering a header for `Shield`
700                // due to `panic` in `.allow()`.
701                if let Some(auth) = uri.authority() {
702                    use std::fmt::Write;
703
704                    let _ = write!(string, "://{}", auth.host());
705                    if let Some(port) = auth.port() {
706                        let _ = write!(string, ":{}", port);
707                    }
708                }
709
710                string.push('"');
711                string.into()
712            }
713            Allow::Any => "*".into(),
714            Allow::This => "self".into(),
715        }
716    }
717}
718
719impl IntoIterator for Allow {
720    type Item = Self;
721
722    type IntoIter = std::iter::Once<Self>;
723
724    fn into_iter(self) -> Self::IntoIter {
725        std::iter::once(self)
726    }
727}
728
729/// A browser feature that can be enabled or blocked via [`Permission`].
730#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
731#[non_exhaustive]
732pub enum Feature {
733    // Standardized.
734
735    /// The "accelerometer" feature.
736    Accelerometer,
737    /// The "ambient-light-sensor" feature.
738    AmbientLightSensor,
739    /// The "autoplay" feature.
740    Autoplay,
741    /// The "battery" feature.
742    Battery,
743    /// The "camera" feature.
744    Camera,
745    /// The "cross-origin-isolated" feature.
746    CrossOriginIsolated,
747    /// The "display-capture" feature.
748    Displaycapture,
749    /// The "document-domain" feature.
750    DocumentDomain,
751    /// The "encrypted-media" feature.
752    EncryptedMedia,
753    /// The "execution-while-not-rendered" feature.
754    ExecutionWhileNotRendered,
755    /// The "execution-while-out-of-viewport" feature.
756    ExecutionWhileOutOfviewport,
757    /// The "fullscreen" feature.
758    Fullscreen,
759    /// The "geolocation" feature.
760    Geolocation,
761    /// The "gyroscope" feature.
762    Gyroscope,
763    /// The "magnetometer" feature.
764    Magnetometer,
765    /// The "microphone" feature.
766    Microphone,
767    /// The "midi" feature.
768    Midi,
769    /// The "navigation-override" feature.
770    NavigationOverride,
771    /// The "payment" feature.
772    Payment,
773    /// The "picture-in-picture" feature.
774    PictureInPicture,
775    /// The "publickey-credentials-get" feature.
776    PublickeyCredentialsGet,
777    /// The "screen-wake-lock" feature.
778    ScreenWakeLock,
779    /// The "sync-xhr" feature.
780    SyncXhr,
781    /// The "usb" feature.
782    Usb,
783    /// The "web-share" feature.
784    WebShare,
785    /// The "xr-spatial-tracking" feature.
786    XrSpatialTracking,
787
788    // Proposed.
789
790    /// The "clipboard-read" feature.
791    ClipboardRead,
792    /// The "clipboard-write" feature.
793    ClipboardWrite,
794    /// The "gamepad" feature.
795    Gamepad,
796    /// The "speaker-selection" feature.
797    SpeakerSelection,
798    /// The "interest-cohort" feature.
799    InterestCohort,
800
801    // Experimental.
802
803    /// The "conversion-measurement" feature.
804    ConversionMeasurement,
805    /// The "focus-without-user-activation" feature.
806    FocusWithoutUserActivation,
807    /// The "hid" feature.
808    Hid,
809    /// The "idle-detection" feature.
810    IdleDetection,
811    /// The "serial" feature.
812    Serial,
813    /// The "sync-script" feature.
814    SyncScript,
815    /// The "trust-token-redemption" feature.
816    TrustTokenRedemption,
817    /// The "vertical-scroll" feature.
818    VerticalScroll,
819}
820
821impl Feature {
822    /// Returns the feature string as it appears in the header.
823    ///
824    /// # Example
825    ///
826    /// ```rust
827    /// use rocket::shield::Feature;
828    ///
829    /// assert_eq!(Feature::Camera.as_str(), "camera");
830    /// assert_eq!(Feature::SyncScript.as_str(), "sync-script");
831    /// ```
832    pub const fn as_str(self) -> &'static str {
833        use Feature::*;
834
835        match self {
836            Accelerometer => "accelerometer",
837            AmbientLightSensor => "ambient-light-sensor",
838            Autoplay => "autoplay",
839            Battery => "battery",
840            Camera => "camera",
841            CrossOriginIsolated => "cross-origin-isolated",
842            Displaycapture => "display-capture",
843            DocumentDomain => "document-domain",
844            EncryptedMedia => "encrypted-media",
845            ExecutionWhileNotRendered => "execution-while-not-rendered",
846            ExecutionWhileOutOfviewport => "execution-while-out-of-viewport",
847            Fullscreen => "fullscreen",
848            Geolocation => "geolocation",
849            Gyroscope => "gyroscope",
850            Magnetometer => "magnetometer",
851            Microphone => "microphone",
852            Midi => "midi",
853            NavigationOverride => "navigation-override",
854            Payment => "payment",
855            PictureInPicture => "picture-in-picture",
856            PublickeyCredentialsGet => "publickey-credentials-get",
857            ScreenWakeLock => "screen-wake-lock",
858            SyncXhr => "sync-xhr",
859            Usb => "usb",
860            WebShare => "web-share",
861            XrSpatialTracking => "xr-spatial-tracking",
862
863            ClipboardRead => "clipboard-read",
864            ClipboardWrite => "clipboard-write",
865            Gamepad => "gamepad",
866            SpeakerSelection => "speaker-selection",
867            InterestCohort => "interest-cohort",
868
869            ConversionMeasurement => "conversion-measurement",
870            FocusWithoutUserActivation => "focus-without-user-activation",
871            Hid => "hid",
872            IdleDetection => "idle-detection",
873            Serial => "serial",
874            SyncScript => "sync-script",
875            TrustTokenRedemption => "trust-token-redemption",
876            VerticalScroll => "vertical-scroll",
877        }
878    }
879}
880
881impl fmt::Display for Feature {
882    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
883        self.as_str().fmt(f)
884    }
885}