csp/lib.rs
1//! This crate is a helper to quickly construct a CSP and then turn it into a
2//! String.
3//!
4//! This library can help you when you don't want to remember some weird
5//! formatting rules of CSP, and want to avoid typos. And it certainly can be
6//! handy if you need to re-use things, for example a list of sources (just
7//! `.clone()` them everywhere and you're good to go!).
8//!
9//! WARNING: this library does not care if you create invalid CSP rules, and
10//! happily allows them and turns them into Strings. But it does force you to
11//! use a typed structure, so it'll be harder to mess up than when manually
12//! writing CSP. Another thing that this crate does not do: It does not do any
13//! base64 or percent encoding or anything like that.
14//!
15//! # Example usage
16//! ```rust
17//! use csp::{CSP, Directive, Sources, Source};
18//!
19//! let csp = CSP::new()
20//! .push(Directive::ImgSrc(
21//! Sources::new_with(Source::Self_)
22//! .push(Source::Host("https://*.example.org"))
23//! .push(Source::Host("https://shields.io")),
24//! ))
25//! .push(Directive::ConnectSrc(
26//! Sources::new()
27//! .push(Source::Host("http://crates.io"))
28//! .push(Source::Scheme("https"))
29//! .push(Source::Self_),
30//! ))
31//! .push(Directive::StyleSrc(
32//! Sources::new_with(Source::Self_).push(Source::UnsafeInline),
33//! ))
34//! .push(Directive::ObjectSrc(Sources::new()));
35//!
36//! let csp_header = "Content-Security-Policy: ".to_owned() + &csp.to_string();
37//! ```
38//! # Copyright notice for this crate's docs:
39//! Most of the comments for various CSP things are from [MDN](https://developer.mozilla.org/en-US/docs/MDN/About), so they licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)
40//! So attribution of most of the docs goes to [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/contributors.txt).
41//!
42//! Please go to MDN to read up to date docs, as these ones might not be up to
43//! date.
44
45#![deny(clippy::all)]
46#![deny(unsafe_code)]
47#![deny(clippy::cargo)]
48#![warn(missing_docs)]
49#![deny(rustdoc::invalid_html_tags)]
50#![warn(clippy::pedantic)]
51#![warn(clippy::nursery)]
52
53use std::fmt;
54
55#[derive(Debug, Default, Clone)]
56/// The starting point for building a Content Security Policy.
57///
58/// You'll add [`Directive`] into this struct, and later on call `.to_string()`
59/// on it to get it as a header compatible string. Doesn't include
60/// content-security-policy: part in it though.
61///
62/// [`Directive`]: Directive
63pub struct CSP<'a>(Vec<Directive<'a>>);
64
65#[derive(Debug, Default, Clone)]
66/// A struct to give source(s) to a [`Directive`] which might require it.
67///
68/// # Example usage
69/// ```rust
70/// use csp::{Sources, Source};
71///
72/// let sources = Sources::new().push(Source::Self_).push(Source::Scheme("data"));
73///
74/// assert_eq!(sources.to_string(), "'self' data:");
75/// ```
76///
77/// [`Directive`]: Directive
78pub struct Sources<'a>(Vec<Source<'a>>);
79
80#[derive(Debug, Default, Clone, PartialEq, Eq)]
81/// Used for `PluginTypes` [`Directive`].
82///
83/// # Example usage
84/// ```rust
85/// let flash = csp::Plugins::new().push(("application", "x-shockwave-flash"));
86/// ```
87/// to get `application/x-shockwave-flash`
88///
89/// [`Directive`]: Directive
90pub struct Plugins<'a>(Vec<(&'a str, &'a str)>);
91
92#[derive(Debug, Default, Clone)]
93/// Used for `ReportUri` [`Directive`].
94///
95/// # Example usage
96/// ```rust
97/// let report_uris = csp::ReportUris::new().push("https://example.org/report");
98/// ```
99///
100/// [`Directive`]: Directive
101pub struct ReportUris<'a>(Vec<&'a str>);
102
103#[derive(Debug, Default, Clone)]
104/// Used for `Sandbox` [`Directive`].
105///
106/// [`Directive`]: Directive
107pub struct SandboxAllowedList(Vec<SandboxAllow>);
108
109#[derive(Debug, Clone)]
110/// Used for `RequireSriFor` [`Directive`].
111///
112/// [`Directive`]: Directive
113pub enum SriFor {
114 /// Requires SRI for scripts.
115 Script,
116 /// Requires SRI for style sheets.
117 Style,
118 /// Requires SRI for both, scripts and style sheets.
119 ScriptStyle,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123/// The source that a bunch of directives can have multiple of.
124///
125/// If nothing gets added, becomes 'none'.
126pub enum Source<'a> {
127 /// Internet hosts by name or IP address, as well as an optional URL scheme
128 /// and/or port number.
129 ///
130 /// The site's address may include an optional leading wildcard (the
131 /// asterisk character, '*'), and you may use a wildcard (again, '*') as the
132 /// port number, indicating that all legal ports are valid for the source.
133 /// Examples:
134 /// - `http://*.example.com`: Matches all attempts to load from any subdomain
135 /// of example.com using the `http:` URL scheme.
136 /// - `mail.example.com:443`: Matches all attempts to access port 443 on
137 /// mail.example.com.
138 /// - `https://store.example.com`: Matches all attempts to access
139 /// store.example.com using https:.
140 Host(&'a str),
141 /// A schema such as 'http' or 'https'.
142 ///
143 /// The colon is automatically added to the end. You can also specify data
144 /// schemas (not recommended).
145 /// - `data` Allows data: URIs to be used as a content source. This is
146 /// insecure; an attacker can also inject arbitrary data: URIs. Use this
147 /// sparingly and definitely not for scripts.
148 /// - `mediastream` Allows `mediastream:` URIs to be used as a content source.
149 /// - `blob` Allows `blob:` URIs to be used as a content source.
150 /// - `filesystem` Allows `filesystem:` URIs to be used as a content source.
151 Scheme(&'a str),
152 /// Refers to the origin from which the protected document is being served,
153 /// including the same URL scheme and port number.
154 ///
155 /// Some browsers specifically exclude `blob` and `filesystem` from source
156 /// directives. Sites needing to allow these content types can specify them
157 /// using the Data attribute.
158 Self_,
159 /// Allows the use of `eval()` and similar methods for creating code from
160 /// strings.
161 UnsafeEval,
162 /// Allows the compilation and instantiation of WebAssembly.
163 WasmUnsafeEval,
164 /// Allows to enable specific inline event handlers. If you only need to
165 /// allow inline event handlers and not inline `<script>` elements or
166 /// `javascript:` URLs, this is a safer method compared to using the
167 /// `unsafe-inline` expression.
168 UnsafeHashes,
169 /// Allows the use of inline resources, such as inline `<script>` elements,
170 /// javascript: URLs, inline event handlers, and inline <\style> elements.
171 UnsafeInline,
172 /// A whitelist for specific inline scripts using a cryptographic nonce
173 /// (number used once). The server must generate a unique nonce value each
174 /// time it transmits a policy. It is critical to provide an unguessable
175 /// nonce, as bypassing a resource’s policy is otherwise trivial. See unsafe
176 /// inline script for an example. Specifying nonce makes a modern browser
177 /// ignore `'unsafe-inline'` which could still be set for older browsers
178 /// without nonce support.
179 Nonce(&'a str),
180 /// A sha256, sha384 or sha512 hash of scripts or styles. The use of this
181 /// source consists of two portions separated by a dash: the encryption
182 /// algorithm used to create the hash and the base64-encoded hash of the
183 /// script or style. When generating the hash, don't include the `<script>`
184 /// or `<style>` tags and note that capitalization and whitespace matter,
185 /// including leading or trailing whitespace. See unsafe inline script for
186 /// an example. In CSP 2.0 this applied only to inline scripts. CSP 3.0
187 /// allows it in the case of `script-src` for external scripts.
188 Hash((&'a str, &'a str)),
189 /// The `strict-dynamic` source expression specifies that the trust
190 /// explicitly given to a script present in the markup, by accompanying it
191 /// with a nonce or a hash, shall be propagated to all the scripts loaded by
192 /// that root script. At the same time, any whitelist or source expressions
193 /// such as `'self'` or `'unsafe-inline'` will be ignored. See script-src
194 /// for an example.
195 StrictDynamic,
196 /// Requires a sample of the violating code to be included in the violation
197 /// report.
198 ReportSample,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202/// Optionally used for the `Sandbox` directive. Not using it but using the
203/// sandbox directive disallows everything that you could allow with the
204/// optional values.
205pub enum SandboxAllow {
206 /// Allows for downloads to occur without a gesture from the user.
207 DownloadsWithoutUserActivation,
208 /// Allows the embedded browsing context to submit forms. If this keyword is
209 /// not used, this operation is not allowed.
210 Forms,
211 /// Allows the embedded browsing context to open modal windows.
212 Modals,
213 /// Allows the embedded browsing context to disable the ability to lock the
214 /// screen orientation.
215 OrientationLock,
216 /// Allows the embedded browsing context to use the Pointer Lock API.
217 PointerLock,
218 /// Allows popups (like from window.open, target="_blank", showModalDialog).
219 /// If this keyword is not used, that functionality will silently fail.
220 Popups,
221 /// Allows a sandboxed document to open new windows without forcing the
222 /// sandboxing flags upon them. This will allow, for example, a third-party
223 /// advertisement to be safely sandboxed without forcing the same
224 /// restrictions upon a landing page.
225 PopupsToEscapeSandbox,
226 /// Allows embedders to have control over whether an iframe can start a
227 /// presentation session.
228 Presentation,
229 /// Allows the content to be treated as being from its normal origin. If
230 /// this keyword is not used, the embedded content is treated as being from
231 /// a unique origin.
232 SameOrigin,
233 /// Allows the embedded browsing context to run scripts (but not create
234 /// pop-up windows). If this keyword is not used, this operation is not
235 /// allowed.
236 Scripts,
237 /// Lets the resource request access to the parent's storage capabilities
238 /// with the Storage Access API.
239 StorageAccessByUserActivation,
240 /// Allows the embedded browsing context to navigate (load) content to the
241 /// top-level browsing context. If this keyword is not used, this operation
242 /// is not allowed.
243 TopNavigation,
244 /// Lets the resource navigate the top-level browsing context, but only if
245 /// initiated by a user gesture.
246 TopNavigationByUserActivation,
247}
248
249#[derive(Debug, Clone)]
250/// A CSP directive.
251pub enum Directive<'a> {
252 /// Restricts the URLs which can be used in a document's `<base>` element.
253 ///
254 /// If this value is absent, then any URI is allowed. If this directive is
255 /// absent, the user agent will use the value in the `<base>` element.
256 BaseUri(Sources<'a>),
257 /// Prevents loading any assets using HTTP when the page is loaded using
258 /// HTTPS.
259 ///
260 ///All mixed content resource requests are blocked, including both active
261 /// and passive mixed content. This also applies to `<iframe>` documents,
262 /// ensuring the entire page is mixed content free.
263 /// The upgrade-insecure-requests directive is evaluated before
264 /// block-all-mixed-content and If the former is set, the latter is
265 /// effectively a no-op. It is recommended to set one directive or the other
266 /// – not both, unless you want to force HTTPS on older browsers that do not
267 /// force it after a redirect to HTTP.
268 BlockAllMixedContent,
269 /// Defines the valid sources for web workers and nested browsing contexts
270 /// loaded using elements such as `<frame>` and `<iframe>`.
271 ///
272 /// For workers, non-compliant requests are treated as fatal network errors
273 /// by the user agent.
274 ChildSrc(Sources<'a>),
275 /// restricts the URLs which can be loaded using script interfaces. The APIs
276 /// that are restricted are:
277 ///
278 /// - `<a>` ping,
279 /// - `Fetch`,
280 /// - `XMLHttpRequest`,
281 /// - `WebSocket`,
282 /// - `EventSource`,
283 /// - `Navigator.sendBeacon()`.
284 ///
285 /// Note: connect-src 'self' does not resolve to websocket schemas in all browsers, more info: <https://github.com/w3c/webappsec-csp/issues/7>
286 ConnectSrc(Sources<'a>),
287 /// Serves as a fallback for the other CSP fetch directives.
288 ///
289 /// For each of the following directives that are absent, the user agent
290 /// will look for the default-src directive and will use this value for it:
291 /// - child-src
292 /// - connect-src
293 /// - font-src
294 /// - frame-src
295 /// - img-src
296 /// - manifest-src
297 /// - media-src
298 /// - object-src
299 /// - prefetch-src
300 /// - script-src
301 /// - script-src-elem
302 /// - script-src-attr
303 /// - style-src
304 /// - style-src-elem
305 /// - style-src-attr
306 /// - worker-src
307 DefaultSrc(Sources<'a>),
308 /// Specifies valid sources for fonts loaded using @font-face.
309 FontSrc(Sources<'a>),
310 /// Restricts the URLs which can be used as the target of a form submissions
311 /// from a given context.
312 ///
313 /// Whether form-action should block redirects after a form submission is
314 /// debated and browser implementations of this aspect are inconsistent
315 /// (e.g. Firefox 57 doesn't block the redirects whereas Chrome 63 does).
316 FormAction(Sources<'a>),
317 /// specifies valid parents that may embed a page using `<frame>`, `<iframe>`,
318 /// `<object>`, `<embed>`, or `<applet>`.
319 ///
320 /// Setting this directive to 'none' is similar to X-Frame-Options: deny
321 /// (which is also supported in older browsers).
322 FrameAncestors(Sources<'a>),
323 /// Specifies valid sources for nested browsing contexts loading using
324 /// elements such as `<frame>` and `<iframe>`.
325 FrameSrc(Sources<'a>),
326 /// Specifies valid sources of images and favicons.
327 ImgSrc(Sources<'a>),
328 /// Specifies which manifest can be applied to the resource.
329 ManifestSrc(Sources<'a>),
330 /// Specifies valid sources for loading media using the `<audio>` and
331 /// `<video>` elements.
332 MediaSrc(Sources<'a>),
333 /// restricts the URLs to which a document can initiate navigations by any
334 /// means including `<form>` (if form-action is not specified), `<a>`,
335 /// window.location, window.open, etc.
336 ///
337 /// This is an enforcement on what navigations this document initiates not
338 /// on what this document is allowed to navigate to.
339 ///
340 /// Note: If the form-action directive is present, the navigate-to directive
341 /// will not act on navigations that are form submissions.
342 NavigateTo(Sources<'a>),
343 /// specifies valid sources for the `<object>`, `<embed>`, and `<applet>`
344 /// elements.
345 ///
346 /// To set allowed types for `<object>`, `<embed>`, and `<applet>` elements,
347 /// use the `PluginTypes` [`Directive`].
348 ///
349 /// Elements controlled by object-src are perhaps coincidentally considered
350 /// legacy HTML elements and aren't receiving new standardized features
351 /// (such as the security attributes sandbox or allow for `<iframe>`).
352 /// Therefore it is recommended to restrict this fetch-directive (e.g.
353 /// explicitly set object-src 'none' if possible).
354 ObjectSrc(Sources<'a>),
355 /// Restricts the set of plugins that can be embedded into a document by
356 /// limiting the types of resources which can be loaded.
357 ///
358 /// Instantiation of an `<embed>`, `<object>` or `<applet>` element will fail
359 /// if:
360 /// - the element to load does not declare a valid MIME type,
361 /// - the declared type does not match one of specified types in the
362 /// plugin-types directive,
363 /// - the fetched resource does not match the declared type.
364 PluginTypes(Plugins<'a>),
365 /// Specifies valid resources that may be prefetched or prerendered.
366 PrefetchSrc(Sources<'a>),
367 /// Instructs the user agent to store reporting endpoints for an origin.
368 ///
369 /// ```text
370 /// Content-Security-Policy: ...; report-to groupname
371 /// ```
372 ///
373 /// The directive has no effect in and of itself, but only gains meaning in
374 /// combination with other directives.
375 ReportTo(&'a str),
376 /// Deprecated.
377 ///
378 /// Instructs the user agent to report attempts to violate the Content
379 /// Security Policy. These violation reports consist of JSON documents sent
380 /// via an HTTP POST request to the specified URI.
381 ///
382 /// This feature is no longer recommended. Though some browsers might still
383 /// support it, it may have already been removed from the relevant web
384 /// standards, may be in the process of being dropped, or may only be kept
385 /// for compatibility purposes. Avoid using it, and update existing code if
386 /// possible.
387 ///
388 /// Though the report-to directive is intended to replace the deprecated
389 /// report-uri directive, report-to isn’t supported in most browsers yet. So
390 /// for compatibility with current browsers while also adding forward
391 /// compatibility when browsers get report-to support, you can specify both
392 /// report-uri and report-to:
393 ///
394 /// > `Content-Security-Policy: ...; report-uri <https://endpoint.com>;
395 /// > report-to groupname`
396 ///
397 /// In browsers that support report-to, the report-uri directive will be
398 /// ignored.
399 ReportUri(ReportUris<'a>),
400 /// Instructs the client to require the use of Subresource Integrity for
401 /// scripts or styles on the page.
402 RequireSriFor(SriFor),
403 /// Enables a sandbox for the requested resource similar to the `<iframe>`
404 /// sandbox attribute.
405 ///
406 /// It applies restrictions to a page's actions including preventing popups,
407 /// preventing the execution of plugins and scripts, and enforcing a
408 /// same-origin policy.
409 ///
410 /// You can leave the [`SandboxAllowedList`] empty
411 /// (`SandboxAllowedList::new_empty()`) to disallow everything.
412 Sandbox(SandboxAllowedList),
413 /// Specifies valid sources for JavaScript.
414 ///
415 /// This includes not only URLs loaded directly into `<script>` elements, but
416 /// also things like inline script event handlers (onclick) and XSLT
417 /// stylesheets which can trigger script execution.
418 ScriptSrc(Sources<'a>),
419 /// Specifies valid sources for JavaScript.
420 ///
421 /// This includes not only URLs loaded directly into `<script>` elements, but
422 /// also things like inline script event handlers (onclick) and XSLT
423 /// stylesheets which can trigger script execution.
424 ScriptSrcAttr(Sources<'a>),
425 /// Specifies valid sources for JavaScript `<script>` elements, but not
426 /// inline script event handlers like onclick.
427 ScriptSrcElem(Sources<'a>),
428 /// specifies valid sources for stylesheets.
429 StyleSrc(Sources<'a>),
430 /// Specifies valid sources for inline styles applied to individual DOM
431 /// elements.
432 StyleSrcAttr(Sources<'a>),
433 /// Specifies valid sources for stylesheets `<style>` elements and `<link>`
434 /// elements with rel="stylesheet".
435 StyleSrcElem(Sources<'a>),
436 /// Instructs user agents to restrict usage of known DOM XSS sinks to a
437 /// predefined set of functions that only accept non-spoofable, typed values
438 /// in place of strings.
439 ///
440 /// This allows authors to define rules guarding writing values to the DOM
441 /// and thus reducing the DOM XSS attack surface to small, isolated parts of
442 /// the web application codebase, facilitating their monitoring and code
443 /// review. This directive declares a white-list of trusted type policy
444 /// names created with TrustedTypes.createPolicy from Trusted Types API.
445 TrustedTypes(Vec<&'a str>),
446 /// Instructs user agents to treat all of a site's insecure URLs (those
447 /// served over HTTP) as though they have been replaced with secure URLs
448 /// (those served over HTTPS).
449 ///
450 /// This directive is intended for web sites with large numbers of insecure
451 /// legacy URLs that need to be rewritten.
452 ///
453 /// The upgrade-insecure-requests directive is evaluated before
454 /// block-all-mixed-content and if it is set, the latter is effectively a
455 /// no-op. It is recommended to set either directive, but not both, unless
456 /// you want to force HTTPS on older browsers that do not force it after a
457 /// redirect to HTTP. The upgrade-insecure-requests directive will not
458 /// ensure that users visiting your site via links on third-party sites will
459 /// be upgraded to HTTPS for the top-level navigation and thus does not
460 /// replace the Strict-Transport-Security (HSTS) header, which should still
461 /// be set with an appropriate max-age to ensure that users are not subject
462 /// to SSL stripping attacks.
463 UpgradeInsecureRequests,
464 /// Instructs user agents to restrict usage of known DOM XSS sinks to a
465 /// predefined set of functions that only accept non-spoofable, typed values
466 /// in place of strings.
467 ///
468 /// This allows authors to define rules guarding writing values to the DOM
469 /// and thus reducing the DOM XSS attack surface to small, isolated parts of
470 /// the web application codebase, facilitating their monitoring and code
471 /// review. This directive declares a white-list of trusted type policy
472 /// names created with TrustedTypes.createPolicy from Trusted Types API.
473 WorkerSrc(Sources<'a>),
474}
475
476impl<'a> CSP<'a> {
477 #[must_use]
478 /// Creates a new empty CSP
479 pub fn new() -> Self {
480 Self::default()
481 }
482
483 #[must_use]
484 /// Creates a new CSP with a given directive
485 pub fn new_with(directive: Directive<'a>) -> Self {
486 Self(vec![directive])
487 }
488
489 #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
490 #[allow(missing_docs)]
491 pub fn add_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
492 self.push_borrowed(directive);
493 self
494 }
495
496 /// Pushes a directive to the end of the borrowed CSP
497 pub fn push_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
498 self.0.push(directive);
499 self
500 }
501
502 #[allow(clippy::should_implement_trait)]
503 #[deprecated(since = "1.0.0", note = "please use `push` instead")]
504 #[must_use]
505 #[allow(missing_docs)]
506 pub fn add(self, directive: Directive<'a>) -> Self {
507 self.push(directive)
508 }
509
510 #[must_use]
511 /// Pushes a directive to the end of the CSP
512 pub fn push(mut self, directive: Directive<'a>) -> Self {
513 self.0.push(directive);
514 self
515 }
516
517 #[must_use]
518 /// Merge duplicate directives. For directives that carry repeated entries,
519 /// the entries are unioned (no duplicates); for other directives, the
520 /// *first* wins.
521 pub fn normalize(self) -> Self {
522 use Directive::*;
523 let mut out: Vec<Directive<'a>> = Vec::with_capacity(self.0.len());
524
525 for d in self.0 {
526 match d {
527 BaseUri(s) => {
528 if let Some(BaseUri(existing)) =
529 out.iter_mut().find(|x| matches!(x, BaseUri(_)))
530 {
531 existing.extend_unique(s);
532 } else {
533 out.push(BaseUri(s));
534 }
535 }
536 ChildSrc(s) => {
537 if let Some(ChildSrc(existing)) =
538 out.iter_mut().find(|x| matches!(x, ChildSrc(_)))
539 {
540 existing.extend_unique(s);
541 } else {
542 out.push(ChildSrc(s));
543 }
544 }
545 ConnectSrc(s) => {
546 if let Some(ConnectSrc(existing)) =
547 out.iter_mut().find(|x| matches!(x, ConnectSrc(_)))
548 {
549 existing.extend_unique(s);
550 } else {
551 out.push(ConnectSrc(s));
552 }
553 }
554 DefaultSrc(s) => {
555 if let Some(DefaultSrc(existing)) =
556 out.iter_mut().find(|x| matches!(x, DefaultSrc(_)))
557 {
558 existing.extend_unique(s);
559 } else {
560 out.push(DefaultSrc(s));
561 }
562 }
563 FontSrc(s) => {
564 if let Some(FontSrc(existing)) =
565 out.iter_mut().find(|x| matches!(x, FontSrc(_)))
566 {
567 existing.extend_unique(s);
568 } else {
569 out.push(FontSrc(s));
570 }
571 }
572 FormAction(s) => {
573 if let Some(FormAction(existing)) =
574 out.iter_mut().find(|x| matches!(x, FormAction(_)))
575 {
576 existing.extend_unique(s);
577 } else {
578 out.push(FormAction(s));
579 }
580 }
581 FrameAncestors(s) => {
582 if let Some(FrameAncestors(existing)) =
583 out.iter_mut().find(|x| matches!(x, FrameAncestors(_)))
584 {
585 existing.extend_unique(s);
586 } else {
587 out.push(FrameAncestors(s));
588 }
589 }
590 FrameSrc(s) => {
591 if let Some(FrameSrc(existing)) =
592 out.iter_mut().find(|x| matches!(x, FrameSrc(_)))
593 {
594 existing.extend_unique(s);
595 } else {
596 out.push(FrameSrc(s));
597 }
598 }
599 ImgSrc(s) => {
600 if let Some(ImgSrc(existing)) = out.iter_mut().find(|x| matches!(x, ImgSrc(_)))
601 {
602 existing.extend_unique(s);
603 } else {
604 out.push(ImgSrc(s));
605 }
606 }
607 ManifestSrc(s) => {
608 if let Some(ManifestSrc(existing)) =
609 out.iter_mut().find(|x| matches!(x, ManifestSrc(_)))
610 {
611 existing.extend_unique(s);
612 } else {
613 out.push(ManifestSrc(s));
614 }
615 }
616 MediaSrc(s) => {
617 if let Some(MediaSrc(existing)) =
618 out.iter_mut().find(|x| matches!(x, MediaSrc(_)))
619 {
620 existing.extend_unique(s);
621 } else {
622 out.push(MediaSrc(s));
623 }
624 }
625 NavigateTo(s) => {
626 if let Some(NavigateTo(existing)) =
627 out.iter_mut().find(|x| matches!(x, NavigateTo(_)))
628 {
629 existing.extend_unique(s);
630 } else {
631 out.push(NavigateTo(s));
632 }
633 }
634 ObjectSrc(s) => {
635 if let Some(ObjectSrc(existing)) =
636 out.iter_mut().find(|x| matches!(x, ObjectSrc(_)))
637 {
638 existing.extend_unique(s);
639 } else {
640 out.push(ObjectSrc(s));
641 }
642 }
643 PrefetchSrc(s) => {
644 if let Some(PrefetchSrc(existing)) =
645 out.iter_mut().find(|x| matches!(x, PrefetchSrc(_)))
646 {
647 existing.extend_unique(s);
648 } else {
649 out.push(PrefetchSrc(s));
650 }
651 }
652 ScriptSrc(s) => {
653 if let Some(ScriptSrc(existing)) =
654 out.iter_mut().find(|x| matches!(x, ScriptSrc(_)))
655 {
656 existing.extend_unique(s);
657 } else {
658 out.push(ScriptSrc(s));
659 }
660 }
661 ScriptSrcAttr(s) => {
662 if let Some(ScriptSrcAttr(existing)) =
663 out.iter_mut().find(|x| matches!(x, ScriptSrcAttr(_)))
664 {
665 existing.extend_unique(s);
666 } else {
667 out.push(ScriptSrcAttr(s));
668 }
669 }
670 ScriptSrcElem(s) => {
671 if let Some(ScriptSrcElem(existing)) =
672 out.iter_mut().find(|x| matches!(x, ScriptSrcElem(_)))
673 {
674 existing.extend_unique(s);
675 } else {
676 out.push(ScriptSrcElem(s));
677 }
678 }
679 StyleSrc(s) => {
680 if let Some(StyleSrc(existing)) =
681 out.iter_mut().find(|x| matches!(x, StyleSrc(_)))
682 {
683 existing.extend_unique(s);
684 } else {
685 out.push(StyleSrc(s));
686 }
687 }
688 StyleSrcAttr(s) => {
689 if let Some(StyleSrcAttr(existing)) =
690 out.iter_mut().find(|x| matches!(x, StyleSrcAttr(_)))
691 {
692 existing.extend_unique(s);
693 } else {
694 out.push(StyleSrcAttr(s));
695 }
696 }
697 StyleSrcElem(s) => {
698 if let Some(StyleSrcElem(existing)) =
699 out.iter_mut().find(|x| matches!(x, StyleSrcElem(_)))
700 {
701 existing.extend_unique(s);
702 } else {
703 out.push(StyleSrcElem(s));
704 }
705 }
706 WorkerSrc(s) => {
707 if let Some(WorkerSrc(existing)) =
708 out.iter_mut().find(|x| matches!(x, WorkerSrc(_)))
709 {
710 existing.extend_unique(s);
711 } else {
712 out.push(WorkerSrc(s));
713 }
714 }
715
716 Sandbox(allowed) => {
717 if let Some(Sandbox(existing)) =
718 out.iter_mut().find(|x| matches!(x, Sandbox(_)))
719 {
720 existing.extend_unique(allowed);
721 } else {
722 out.push(Sandbox(allowed));
723 }
724 }
725
726 PluginTypes(plugins) => {
727 if let Some(PluginTypes(existing)) =
728 out.iter_mut().find(|x| matches!(x, PluginTypes(_)))
729 {
730 existing.extend_unique(plugins);
731 } else {
732 out.push(PluginTypes(plugins));
733 }
734 }
735
736 TrustedTypes(types) => {
737 if let Some(TrustedTypes(existing)) =
738 out.iter_mut().find(|x| matches!(x, TrustedTypes(_)))
739 {
740 for t in types {
741 if !existing.contains(&t) {
742 existing.push(t);
743 }
744 }
745 } else {
746 out.push(TrustedTypes(types));
747 }
748 }
749
750 // For non sources cases, they can't be merged so we keep the *first* instance
751 // (same as the spec if directives are repeated)
752 directive @ (BlockAllMixedContent
753 | UpgradeInsecureRequests
754 | RequireSriFor(_)
755 | ReportUri(_)
756 | ReportTo(_)) => {
757 // check if directive already exists, skip adding this one if so.
758 if out
759 .iter()
760 .any(|x| core::mem::discriminant(x) == core::mem::discriminant(&directive))
761 {
762 continue;
763 }
764 out.push(directive);
765 }
766 }
767 }
768
769 CSP(out)
770 }
771}
772
773impl<'a> Sources<'a> {
774 #[must_use]
775 /// Creates a new empty Sources
776 pub const fn new() -> Self {
777 Self(vec![])
778 }
779
780 #[must_use]
781 /// Creates new Sources with a source
782 pub fn new_with(source: Source<'a>) -> Self {
783 Self(vec![source])
784 }
785
786 #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
787 #[allow(missing_docs)]
788 pub fn add_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
789 self.push_borrowed(source);
790 self
791 }
792
793 /// Pushes a source to the end of the borrowed Sources
794 pub fn push_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
795 self.0.push(source);
796 self
797 }
798
799 #[allow(clippy::should_implement_trait)]
800 #[deprecated(since = "1.0.0", note = "please use `push` instead")]
801 #[must_use]
802 #[allow(missing_docs)]
803 pub fn add(self, source: Source<'a>) -> Self {
804 self.push(source)
805 }
806
807 #[must_use]
808 /// Pushes a source to the end of the Sources
809 pub fn push(mut self, source: Source<'a>) -> Self {
810 self.0.push(source);
811 self
812 }
813
814 /// Add all unique sources from `other` into `self`, preserving order.
815 pub fn extend_unique(&mut self, other: Self) {
816 for s in other.0 {
817 if !self.0.contains(&s) {
818 self.0.push(s);
819 }
820 }
821 }
822}
823
824impl<'a> Plugins<'a> {
825 #[must_use]
826 /// Creates a new Plugins with a plugin
827 pub fn new_with(plugin: (&'a str, &'a str)) -> Self {
828 Self(vec![plugin])
829 }
830
831 #[must_use]
832 /// Creates a new empty plugins
833 pub const fn new() -> Self {
834 Self(vec![])
835 }
836
837 #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
838 #[allow(missing_docs)]
839 pub fn add_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
840 self.push_borrowed(plugin);
841 self
842 }
843
844 /// Pushes a plugin to the end of the borrowed Plugins
845 pub fn push_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
846 self.0.push(plugin);
847 self
848 }
849
850 #[allow(clippy::should_implement_trait)]
851 #[deprecated(since = "1.0.0", note = "please use `push` instead")]
852 #[must_use]
853 #[allow(missing_docs)]
854 pub fn add(self, plugin: (&'a str, &'a str)) -> Self {
855 self.push(plugin)
856 }
857
858 #[must_use]
859 /// Pushes a plugin to the end of the Plugins
860 pub fn push(mut self, plugin: (&'a str, &'a str)) -> Self {
861 self.0.push(plugin);
862 self
863 }
864
865 /// Add all unique sources from `other` into `self`, preserving order.
866 pub fn extend_unique(&mut self, other: Self) {
867 for s in other.0 {
868 if !self.0.contains(&s) {
869 self.0.push(s);
870 }
871 }
872 }
873}
874
875impl SandboxAllowedList {
876 #[must_use]
877 /// Creates a new `SandboxAllowedList` with only a certain sandbox allowance
878 pub fn new_with(sandbox_allow: SandboxAllow) -> Self {
879 Self(vec![sandbox_allow])
880 }
881
882 #[must_use]
883 /// Creates a new empty `SandboxAllowedList`
884 pub const fn new() -> Self {
885 Self(vec![])
886 }
887
888 #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
889 #[allow(missing_docs)]
890 pub fn add_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
891 self.push_borrowed(sandbox_allow);
892 self
893 }
894
895 /// Pushes a sandbox allow type to the end of the borrowed
896 /// `SandboxAllowedList`
897 pub fn push_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
898 self.0.push(sandbox_allow);
899 self
900 }
901
902 #[allow(clippy::should_implement_trait)]
903 #[deprecated(since = "1.0.0", note = "please use `push` instead")]
904 #[must_use]
905 #[allow(missing_docs)]
906 pub fn add(self, sandbox_allow: SandboxAllow) -> Self {
907 self.push(sandbox_allow)
908 }
909
910 #[must_use]
911 /// Pushes a sandbox allow type to the end of the `SandboxAllowedList`
912 pub fn push(mut self, sandbox_allow: SandboxAllow) -> Self {
913 self.0.push(sandbox_allow);
914 self
915 }
916
917 /// Add all unique sources from `other` into `self`, preserving order.
918 pub fn extend_unique(&mut self, other: Self) {
919 for s in other.0 {
920 if !self.0.contains(&s) {
921 self.0.push(s);
922 }
923 }
924 }
925}
926
927impl<'a> ReportUris<'a> {
928 #[must_use]
929 /// Creates a new `ReportUris` with a certain uri
930 pub fn new_with(report_uri: &'a str) -> Self {
931 ReportUris(vec![report_uri])
932 }
933
934 #[must_use]
935 /// Creates a new empty `ReportUris`
936 pub const fn new() -> Self {
937 ReportUris(vec![])
938 }
939
940 #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
941 #[allow(missing_docs)]
942 pub fn add_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
943 self.push_borrowed(report_uri);
944 self
945 }
946
947 /// Pushes a report uri to the end of the borrowed `ReportUris`
948 pub fn push_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
949 self.0.push(report_uri);
950 self
951 }
952
953 #[allow(clippy::should_implement_trait)]
954 #[deprecated(since = "1.0.0", note = "please use `push` instead")]
955 #[must_use]
956 #[allow(missing_docs)]
957 pub fn add(self, report_uri: &'a str) -> Self {
958 self.push(report_uri)
959 }
960
961 #[must_use]
962 /// Pushes a report uri to the end of the `ReportUris`
963 pub fn push(mut self, report_uri: &'a str) -> Self {
964 self.0.push(report_uri);
965 self
966 }
967}
968
969impl fmt::Display for Source<'_> {
970 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
971 match self {
972 Self::Host(s) => write!(fmt, "{s}"),
973 Self::Scheme(s) => write!(fmt, "{s}:"),
974 Self::Self_ => write!(fmt, "'self'"),
975 Self::UnsafeEval => write!(fmt, "'unsafe-eval'"),
976 Self::WasmUnsafeEval => write!(fmt, "'wasm-unsafe-eval'"),
977 Self::UnsafeHashes => write!(fmt, "'unsafe-hashes'"),
978 Self::UnsafeInline => write!(fmt, "'unsafe-inline'"),
979 Self::Nonce(s) => write!(fmt, "'nonce-{s}'"),
980 Self::Hash((algo, hash)) => write!(fmt, "'{algo}-{hash}'"),
981 Self::StrictDynamic => write!(fmt, "'strict-dynamic'"),
982 Self::ReportSample => write!(fmt, "'report-sample'"),
983 }
984 }
985}
986
987impl fmt::Display for SandboxAllow {
988 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
989 match self {
990 Self::DownloadsWithoutUserActivation => {
991 write!(fmt, "allow-downloads-without-user-activation")
992 }
993 Self::Forms => write!(fmt, "allow-forms"),
994 Self::Modals => write!(fmt, "allow-modals"),
995 Self::OrientationLock => write!(fmt, "allow-orientation-lock"),
996 Self::PointerLock => write!(fmt, "allow-pointer-lock"),
997 Self::Popups => write!(fmt, "allow-popups"),
998 Self::PopupsToEscapeSandbox => write!(fmt, "allow-popups-to-escape-sandbox"),
999 Self::Presentation => write!(fmt, "allow-presentation"),
1000 Self::SameOrigin => write!(fmt, "allow-same-origin"),
1001 Self::Scripts => write!(fmt, "allow-scripts"),
1002 Self::StorageAccessByUserActivation => {
1003 write!(fmt, "allow-storage-access-by-user-activation")
1004 }
1005 Self::TopNavigation => write!(fmt, "allow-top-navigation"),
1006 Self::TopNavigationByUserActivation => {
1007 write!(fmt, "allow-top-navigation-by-user-activation")
1008 }
1009 }
1010 }
1011}
1012
1013impl fmt::Display for SriFor {
1014 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1015 match self {
1016 Self::Script => write!(fmt, "script"),
1017 Self::Style => write!(fmt, "style"),
1018 Self::ScriptStyle => write!(fmt, "script style"),
1019 }
1020 }
1021}
1022
1023impl fmt::Display for Directive<'_> {
1024 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1025 match self {
1026 Self::BaseUri(s) => write!(fmt, "base-uri {s}"),
1027 Self::BlockAllMixedContent => write!(fmt, "block-all-mixed-content"),
1028 Self::ChildSrc(s) => write!(fmt, "child-src {s}"),
1029 Self::ConnectSrc(s) => write!(fmt, "connect-src {s}"),
1030 Self::DefaultSrc(s) => write!(fmt, "default-src {s}"),
1031 Self::FontSrc(s) => write!(fmt, "font-src {s}"),
1032 Self::FormAction(s) => write!(fmt, "form-action {s}"),
1033 Self::FrameAncestors(s) => write!(fmt, "frame-ancestors {s}"),
1034 Self::FrameSrc(s) => write!(fmt, "frame-src {s}"),
1035 Self::ImgSrc(s) => write!(fmt, "img-src {s}"),
1036 Self::ManifestSrc(s) => write!(fmt, "manifest-src {s}"),
1037 Self::MediaSrc(s) => write!(fmt, "media-src {s}"),
1038 Self::NavigateTo(s) => write!(fmt, "navigate-to {s}"),
1039 Self::ObjectSrc(s) => write!(fmt, "object-src {s}"),
1040 Self::PluginTypes(s) => write!(fmt, "plugin-types {s}"),
1041 Self::PrefetchSrc(s) => write!(fmt, "prefetch-src {s}"),
1042 Self::ReportTo(s) => write!(fmt, "report-to {s}"),
1043 Self::ReportUri(uris) => {
1044 if uris.0.is_empty() {
1045 return Ok(());
1046 }
1047 write!(fmt, "report-uri ")?;
1048
1049 for uri in &uris.0[0..uris.0.len() - 1] {
1050 write!(fmt, "{uri} ")?;
1051 }
1052
1053 let last = uris.0[uris.0.len() - 1];
1054 write!(fmt, "{last}")
1055 }
1056 Self::RequireSriFor(s) => write!(fmt, "require-sri-for {s}"),
1057 Self::Sandbox(s) => {
1058 if s.0.is_empty() {
1059 write!(fmt, "sandbox")
1060 } else {
1061 write!(fmt, "sandbox {s}")
1062 }
1063 }
1064 Self::ScriptSrc(s) => write!(fmt, "script-src {s}"),
1065 Self::ScriptSrcAttr(s) => write!(fmt, "script-src-attr {s}"),
1066 Self::ScriptSrcElem(s) => write!(fmt, "script-src-elem {s}"),
1067 Self::StyleSrc(s) => write!(fmt, "style-src {s}"),
1068 Self::StyleSrcAttr(s) => write!(fmt, "style-src-attr {s}"),
1069 Self::StyleSrcElem(s) => write!(fmt, "style-src-elem {s}"),
1070 Self::TrustedTypes(trusted_types) => {
1071 if trusted_types.is_empty() {
1072 return Ok(());
1073 }
1074 write!(fmt, "trusted-types ")?;
1075
1076 for trusted_type in &trusted_types[0..trusted_types.len() - 1] {
1077 write!(fmt, "{trusted_type} ")?;
1078 }
1079
1080 let last = trusted_types[trusted_types.len() - 1];
1081 write!(fmt, "{last}")
1082 }
1083 Self::UpgradeInsecureRequests => write!(fmt, "upgrade-insecure-requests"),
1084 Self::WorkerSrc(s) => write!(fmt, "worker-src {s}"),
1085 }
1086 }
1087}
1088
1089impl fmt::Display for Plugins<'_> {
1090 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1091 if self.0.is_empty() {
1092 return Ok(());
1093 }
1094
1095 for plugin in &self.0[0..self.0.len() - 1] {
1096 write!(fmt, "{}/{} ", plugin.0, plugin.1)?;
1097 }
1098
1099 let last = &self.0[self.0.len() - 1];
1100 write!(fmt, "{}/{}", last.0, last.1)
1101 }
1102}
1103
1104impl fmt::Display for Sources<'_> {
1105 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1106 if self.0.is_empty() {
1107 return write!(fmt, "'none'");
1108 }
1109
1110 for source in &self.0[0..self.0.len() - 1] {
1111 write!(fmt, "{source} ")?;
1112 }
1113
1114 let last = &self.0[self.0.len() - 1];
1115 write!(fmt, "{last}")
1116 }
1117}
1118
1119impl fmt::Display for SandboxAllowedList {
1120 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1121 if self.0.is_empty() {
1122 return Ok(());
1123 }
1124
1125 for directive in &self.0[0..self.0.len() - 1] {
1126 write!(fmt, "{directive} ")?;
1127 }
1128
1129 let last = &self.0[self.0.len() - 1];
1130 write!(fmt, "{last}")
1131 }
1132}
1133
1134impl fmt::Display for CSP<'_> {
1135 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1136 if self.0.is_empty() {
1137 return Ok(());
1138 }
1139
1140 for directive in &self.0[0..self.0.len() - 1] {
1141 write!(fmt, "{directive}; ")?;
1142 }
1143
1144 let last = &self.0[self.0.len() - 1];
1145 write!(fmt, "{last}")
1146 }
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151 use super::*;
1152
1153 #[test]
1154 /// Tests combining different Directives and sources, and makes sure that
1155 /// spaces and semicolons are inserted correctly.
1156 fn large_csp() {
1157 let font_src = Source::Host("https://cdn.example.org");
1158
1159 let mut csp = CSP::new()
1160 .push(Directive::ImgSrc(
1161 Sources::new_with(Source::Self_)
1162 .push(Source::Scheme("https"))
1163 .push(Source::Host("http://shields.io")),
1164 ))
1165 .push(Directive::ConnectSrc(
1166 Sources::new().push(Source::Host("https://crates.io")).push(Source::Self_),
1167 ))
1168 .push(Directive::StyleSrc(
1169 Sources::new_with(Source::Self_)
1170 .push(Source::UnsafeInline)
1171 .push(font_src.clone()),
1172 ));
1173
1174 csp.push_borrowed(Directive::FontSrc(Sources::new_with(font_src)));
1175
1176 println!("{csp}");
1177
1178 let csp = csp.to_string();
1179
1180 assert_eq!(
1181 csp,
1182 "img-src 'self' https: http://shields.io; connect-src https://crates.io 'self'; style-src 'self' 'unsafe-inline' https://cdn.example.org; font-src https://cdn.example.org"
1183 );
1184 }
1185
1186 #[test]
1187 /// Tests all the possible source variations.
1188 fn all_sources() {
1189 let csp = CSP::new().push(Directive::ScriptSrc(
1190 Sources::new()
1191 .push(Source::Hash(("sha256", "1234a")))
1192 .push(Source::Nonce("5678b"))
1193 .push(Source::ReportSample)
1194 .push(Source::StrictDynamic)
1195 .push(Source::UnsafeEval)
1196 .push(Source::WasmUnsafeEval)
1197 .push(Source::UnsafeHashes)
1198 .push(Source::UnsafeInline)
1199 .push(Source::Scheme("data"))
1200 .push(Source::Host("https://example.org"))
1201 .push(Source::Self_),
1202 ));
1203
1204 assert_eq!(
1205 csp.to_string(),
1206 "script-src 'sha256-1234a' 'nonce-5678b' 'report-sample' 'strict-dynamic' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-hashes' 'unsafe-inline' data: https://example.org 'self'"
1207 );
1208 }
1209
1210 #[test]
1211 fn empty_values() {
1212 let csp = CSP::new();
1213
1214 assert_eq!(csp.to_string(), "");
1215
1216 let csp = CSP::new().push(Directive::ImgSrc(Sources::new()));
1217
1218 assert_eq!(csp.to_string(), "img-src 'none'");
1219 }
1220
1221 #[test]
1222 fn sandbox() {
1223 let csp = CSP::new().push(Directive::Sandbox(SandboxAllowedList::new()));
1224
1225 assert_eq!(csp.to_string(), "sandbox");
1226
1227 let csp = CSP::new()
1228 .push(Directive::Sandbox(SandboxAllowedList::new().push(SandboxAllow::Scripts)));
1229
1230 assert_eq!(csp.to_string(), "sandbox allow-scripts");
1231 assert_eq!(
1232 csp.to_string(),
1233 "sandbox ".to_owned() + &SandboxAllow::Scripts.to_string()
1234 );
1235 }
1236
1237 #[test]
1238 fn special() {
1239 let mut csp = CSP::new();
1240 let sri_directive = Directive::RequireSriFor(SriFor::Script);
1241
1242 csp.push_borrowed(sri_directive);
1243
1244 assert_eq!(csp.to_string(), "require-sri-for script");
1245
1246 let csp = CSP::new_with(Directive::BlockAllMixedContent);
1247 assert_eq!(csp.to_string(), "block-all-mixed-content");
1248
1249 let csp = CSP::new_with(Directive::PluginTypes(
1250 Plugins::new().push(("application", "x-java-applet")),
1251 ));
1252 assert_eq!(csp.to_string(), "plugin-types application/x-java-applet");
1253
1254 let csp = CSP::new_with(Directive::ReportTo("endpoint-1"));
1255 assert_eq!(csp.to_string(), "report-to endpoint-1");
1256
1257 let csp = CSP::new_with(Directive::ReportUri(
1258 ReportUris::new_with("https://r1.example.org").push("https://r2.example.org"),
1259 ));
1260 assert_eq!(
1261 csp.to_string(),
1262 "report-uri https://r1.example.org https://r2.example.org"
1263 );
1264
1265 let csp = CSP::new_with(Directive::TrustedTypes(vec!["hello", "hello2"]));
1266 assert_eq!(csp.to_string(), "trusted-types hello hello2");
1267
1268 let csp = CSP::new_with(Directive::UpgradeInsecureRequests);
1269 assert_eq!(csp.to_string(), "upgrade-insecure-requests");
1270 }
1271
1272 #[test]
1273 fn empty_trusted_types() {
1274 let csp = CSP::new_with(Directive::TrustedTypes(vec![]));
1275 assert_eq!(csp.to_string(), "");
1276 }
1277
1278 #[test]
1279 fn empty_report_uri() {
1280 let csp = CSP::new_with(Directive::ReportUri(ReportUris::new()));
1281 assert_eq!(csp.to_string(), "");
1282 }
1283
1284 #[test]
1285 fn repeated_directive_normalization() {
1286 let mut csp = CSP::new();
1287
1288 csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1289 csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Host(
1290 "https://example.org",
1291 ))));
1292
1293 csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Nonce("abc"))));
1294 csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Self_)));
1295
1296 assert_eq!(
1297 csp.normalize().to_string(),
1298 "connect-src 'self' https://example.org; script-src 'nonce-abc' 'self'"
1299 );
1300 }
1301
1302 #[test]
1303 fn repeated_duplicate_directives_are_removed() {
1304 let mut csp = CSP::new();
1305
1306 csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1307
1308 csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1309
1310 assert_eq!(csp.normalize().to_string(), "connect-src 'self'");
1311 }
1312
1313 #[test]
1314 fn repeated_non_mergeable_directive_first_wins() {
1315 let mut csp = CSP::new();
1316
1317 csp.push_borrowed(Directive::RequireSriFor(SriFor::Script));
1318 csp.push_borrowed(Directive::RequireSriFor(SriFor::Style));
1319
1320 assert_eq!(csp.normalize().to_string(), "require-sri-for script");
1321 }
1322}