#![deny(clippy::all)]
#![deny(unsafe_code)]
#![deny(clippy::cargo)]
#![warn(missing_docs)]
#![deny(rustdoc::invalid_html_tags)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
use std::fmt;
#[derive(Debug, Default, Clone)]
pub struct CSP<'a>(Vec<Directive<'a>>);
#[derive(Debug, Default, Clone)]
pub struct Sources<'a>(Vec<Source<'a>>);
#[derive(Debug, Default, Clone)]
pub struct Plugins<'a>(Vec<(&'a str, &'a str)>);
#[derive(Debug, Default, Clone)]
pub struct ReportUris<'a>(Vec<&'a str>);
#[derive(Debug, Default, Clone)]
pub struct SandboxAllowedList(Vec<SandboxAllow>);
#[derive(Debug, Clone)]
pub enum SriFor {
Script,
Style,
ScriptStyle,
}
#[derive(Debug, Clone)]
pub enum Source<'a> {
Host(&'a str),
Scheme(&'a str),
Self_,
UnsafeEval,
UnsafeHashes,
UnsafeInline,
Nonce(&'a str),
Hash((&'a str, &'a str)),
StrictDynamic,
ReportSample,
}
#[derive(Debug, Clone)]
pub enum SandboxAllow {
DownloadsWithoutUserActivation,
Forms,
Modals,
OrientationLock,
PointerLock,
Popups,
PopupsToEscapeSandbox,
Presentation,
SameOrigin,
Scripts,
StorageAccessByUserActivation,
TopNavigation,
TopNavigationByUserActivation,
}
#[derive(Debug, Clone)]
pub enum Directive<'a> {
BaseUri(Sources<'a>),
BlockAllMixedContent,
ChildSrc(Sources<'a>),
ConnectSrc(Sources<'a>),
DefaultSrc(Sources<'a>),
FontSrc(Sources<'a>),
FormAction(Sources<'a>),
FrameAncestors(Sources<'a>),
FrameSrc(Sources<'a>),
ImgSrc(Sources<'a>),
ManifestSrc(Sources<'a>),
MediaSrc(Sources<'a>),
NavigateTo(Sources<'a>),
ObjectSrc(Sources<'a>),
PluginTypes(Plugins<'a>),
PrefetchSrc(Sources<'a>),
ReportTo(&'a str),
ReportUri(ReportUris<'a>),
RequireSriFor(SriFor),
Sandbox(SandboxAllowedList),
ScriptSrc(Sources<'a>),
ScriptSrcAttr(Sources<'a>),
ScriptSrcElem(Sources<'a>),
StyleSrc(Sources<'a>),
StyleSrcAttr(Sources<'a>),
StyleSrcElem(Sources<'a>),
TrustedTypes(Vec<&'a str>),
UpgradeInsecureRequests,
WorkerSrc(Sources<'a>),
}
impl<'a> CSP<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn new_with(directive: Directive<'a>) -> Self {
Self(vec![directive])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
self.push_borrowed(directive);
self
}
pub fn push_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
self.0.push(directive);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, directive: Directive<'a>) -> Self {
self.push(directive)
}
#[must_use]
pub fn push(mut self, directive: Directive<'a>) -> Self {
self.0.push(directive);
self
}
}
impl<'a> Sources<'a> {
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[must_use]
pub fn new_with(source: Source<'a>) -> Self {
Self(vec![source])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
self.push_borrowed(source);
self
}
pub fn push_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
self.0.push(source);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, source: Source<'a>) -> Self {
self.push(source)
}
#[must_use]
pub fn push(mut self, source: Source<'a>) -> Self {
self.0.push(source);
self
}
}
impl<'a> Plugins<'a> {
#[must_use]
pub fn new_with(plugin: (&'a str, &'a str)) -> Self {
Self(vec![plugin])
}
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
self.push_borrowed(plugin);
self
}
pub fn push_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
self.0.push(plugin);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, plugin: (&'a str, &'a str)) -> Self {
self.push(plugin)
}
#[must_use]
pub fn push(mut self, plugin: (&'a str, &'a str)) -> Self {
self.0.push(plugin);
self
}
}
impl SandboxAllowedList {
#[must_use]
pub fn new_with(sandbox_allow: SandboxAllow) -> Self {
Self(vec![sandbox_allow])
}
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
self.push_borrowed(sandbox_allow);
self
}
pub fn push_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
self.0.push(sandbox_allow);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, sandbox_allow: SandboxAllow) -> Self {
self.push(sandbox_allow)
}
#[must_use]
pub fn push(mut self, sandbox_allow: SandboxAllow) -> Self {
self.0.push(sandbox_allow);
self
}
}
impl<'a> ReportUris<'a> {
#[must_use]
pub fn new_with(report_uri: &'a str) -> Self {
ReportUris(vec![report_uri])
}
#[must_use]
pub const fn new() -> Self {
ReportUris(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
self.push_borrowed(report_uri);
self
}
pub fn push_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
self.0.push(report_uri);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, report_uri: &'a str) -> Self {
self.push(report_uri)
}
#[must_use]
pub fn push(mut self, report_uri: &'a str) -> Self {
self.0.push(report_uri);
self
}
}
impl<'a> fmt::Display for Source<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Host(s) => write!(fmt, "{}", s),
Self::Scheme(s) => write!(fmt, "{}:", s),
Self::Self_ => write!(fmt, "'self'"),
Self::UnsafeEval => write!(fmt, "'unsafe-eval'"),
Self::UnsafeHashes => write!(fmt, "'unsafe-hashes'"),
Self::UnsafeInline => write!(fmt, "'unsafe-inline'"),
Self::Nonce(s) => write!(fmt, "'nonce-{}'", s),
Self::Hash((algo, hash)) => write!(fmt, "'{}-{}'", algo, hash),
Self::StrictDynamic => write!(fmt, "'strict-dynamic'"),
Self::ReportSample => write!(fmt, "'report-sample'"),
}
}
}
impl fmt::Display for SandboxAllow {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DownloadsWithoutUserActivation => {
write!(fmt, "allow-downloads-without-user-activation")
}
Self::Forms => write!(fmt, "allow-forms"),
Self::Modals => write!(fmt, "allow-modals"),
Self::OrientationLock => write!(fmt, "allow-orientation-lock"),
Self::PointerLock => write!(fmt, "allow-pointer-lock"),
Self::Popups => write!(fmt, "allow-popups"),
Self::PopupsToEscapeSandbox => write!(fmt, "allow-popups-to-escape-sandbox"),
Self::Presentation => write!(fmt, "allow-presentation"),
Self::SameOrigin => write!(fmt, "allow-same-origin"),
Self::Scripts => write!(fmt, "allow-scripts"),
Self::StorageAccessByUserActivation => {
write!(fmt, "allow-storage-access-by-user-activation")
}
Self::TopNavigation => write!(fmt, "allow-top-navigation"),
Self::TopNavigationByUserActivation => {
write!(fmt, "allow-top-navigation-by-user-activation")
}
}
}
}
impl fmt::Display for SriFor {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Script => write!(fmt, "script"),
Self::Style => write!(fmt, "style"),
Self::ScriptStyle => write!(fmt, "script style"),
}
}
}
impl<'a> fmt::Display for Directive<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::BaseUri(s) => write!(fmt, "base-uri {}", s),
Self::BlockAllMixedContent => write!(fmt, "block-all-mixed-content"),
Self::ChildSrc(s) => write!(fmt, "child-src {}", s),
Self::ConnectSrc(s) => write!(fmt, "connect-src {}", s),
Self::DefaultSrc(s) => write!(fmt, "default-src {}", s),
Self::FontSrc(s) => write!(fmt, "font-src {}", s),
Self::FormAction(s) => write!(fmt, "form-action {}", s),
Self::FrameAncestors(s) => write!(fmt, "frame-ancestors {}", s),
Self::FrameSrc(s) => write!(fmt, "frame-src {}", s),
Self::ImgSrc(s) => write!(fmt, "img-src {}", s),
Self::ManifestSrc(s) => write!(fmt, "manifest-src {}", s),
Self::MediaSrc(s) => write!(fmt, "media-src {}", s),
Self::NavigateTo(s) => write!(fmt, "navigate-to {}", s),
Self::ObjectSrc(s) => write!(fmt, "object-src {}", s),
Self::PluginTypes(s) => write!(fmt, "plugin-types {}", s),
Self::PrefetchSrc(s) => write!(fmt, "prefetch-src {}", s),
Self::ReportTo(s) => write!(fmt, "report-to {}", s),
Self::ReportUri(uris) => {
write!(fmt, "report-uri ")?;
for uri in &uris.0[0..uris.0.len() - 1] {
write!(fmt, "{} ", uri)?;
}
let last = uris.0[uris.0.len() - 1];
write!(fmt, "{}", last)
}
Self::RequireSriFor(s) => write!(fmt, "require-sri-for {}", s),
Self::Sandbox(s) => {
if s.0.is_empty() {
write!(fmt, "sandbox")
} else {
write!(fmt, "sandbox {}", s)
}
}
Self::ScriptSrc(s) => write!(fmt, "script-src {}", s),
Self::ScriptSrcAttr(s) => write!(fmt, "script-src-attr {}", s),
Self::ScriptSrcElem(s) => write!(fmt, "script-src-elem {}", s),
Self::StyleSrc(s) => write!(fmt, "style-src {}", s),
Self::StyleSrcAttr(s) => write!(fmt, "style-src-attr {}", s),
Self::StyleSrcElem(s) => write!(fmt, "style-src-elem {}", s),
Self::TrustedTypes(trusted_types) => {
write!(fmt, "trusted-types ")?;
for trusted_type in &trusted_types[0..trusted_types.len() - 1] {
write!(fmt, "{} ", trusted_type)?;
}
let last = trusted_types[trusted_types.len() - 1];
write!(fmt, "{}", last)
}
Self::UpgradeInsecureRequests => write!(fmt, "upgrade-insecure-requests"),
Self::WorkerSrc(s) => write!(fmt, "worker-src {}", s),
}
}
}
impl<'a> fmt::Display for Plugins<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return write!(fmt, "");
}
for plugin in &self.0[0..self.0.len() - 1] {
write!(fmt, "{}/{} ", plugin.0, plugin.1)?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{}/{}", last.0, last.1)
}
}
impl<'a> fmt::Display for Sources<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return write!(fmt, "'none'");
}
for source in &self.0[0..self.0.len() - 1] {
write!(fmt, "{} ", source)?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{}", last)
}
}
impl fmt::Display for SandboxAllowedList {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return write!(fmt, "");
}
for directive in &self.0[0..self.0.len() - 1] {
write!(fmt, "{} ", directive)?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{}", last)
}
}
impl<'a> fmt::Display for CSP<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return write!(fmt, "");
}
for directive in &self.0[0..self.0.len() - 1] {
write!(fmt, "{}; ", directive)?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{}", last)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn large_csp() {
let font_src = Source::Host("https://cdn.example.org");
let mut csp = CSP::new()
.push(Directive::ImgSrc(
Sources::new_with(Source::Self_)
.push(Source::Scheme("https"))
.push(Source::Host("http://shields.io")),
))
.push(Directive::ConnectSrc(
Sources::new().push(Source::Host("https://crates.io")).push(Source::Self_),
))
.push(Directive::StyleSrc(
Sources::new_with(Source::Self_)
.push(Source::UnsafeInline)
.push(font_src.clone()),
));
csp.push_borrowed(Directive::FontSrc(Sources::new_with(font_src)));
println!("{}", csp);
let csp = csp.to_string();
assert_eq!(
csp,
"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"
);
}
#[test]
fn all_sources() {
let csp = CSP::new().push(Directive::ScriptSrc(
Sources::new()
.push(Source::Hash(("sha256", "1234a")))
.push(Source::Nonce("5678b"))
.push(Source::ReportSample)
.push(Source::StrictDynamic)
.push(Source::UnsafeEval)
.push(Source::UnsafeHashes)
.push(Source::UnsafeInline)
.push(Source::Scheme("data"))
.push(Source::Host("https://example.org"))
.push(Source::Self_),
));
assert_eq!(
csp.to_string(),
"script-src 'sha256-1234a' 'nonce-5678b' 'report-sample' 'strict-dynamic' 'unsafe-eval' 'unsafe-hashes' 'unsafe-inline' data: https://example.org 'self'"
);
}
#[test]
fn empty_values() {
let csp = CSP::new();
assert_eq!(csp.to_string(), "");
let csp = CSP::new().push(Directive::ImgSrc(Sources::new()));
assert_eq!(csp.to_string(), "img-src 'none'");
}
#[test]
fn sandbox() {
let csp = CSP::new().push(Directive::Sandbox(SandboxAllowedList::new()));
assert_eq!(csp.to_string(), "sandbox");
let csp = CSP::new()
.push(Directive::Sandbox(SandboxAllowedList::new().push(SandboxAllow::Scripts)));
assert_eq!(csp.to_string(), "sandbox allow-scripts");
assert_eq!(
csp.to_string(),
"sandbox ".to_owned() + &SandboxAllow::Scripts.to_string()
);
}
#[test]
fn special() {
let mut csp = CSP::new();
let sri_directive = Directive::RequireSriFor(SriFor::Script);
csp.push_borrowed(sri_directive);
assert_eq!(csp.to_string(), "require-sri-for script");
let csp = CSP::new_with(Directive::BlockAllMixedContent);
assert_eq!(csp.to_string(), "block-all-mixed-content");
let csp = CSP::new_with(Directive::PluginTypes(
Plugins::new().push(("application", "x-java-applet")),
));
assert_eq!(csp.to_string(), "plugin-types application/x-java-applet");
let csp = CSP::new_with(Directive::ReportTo("endpoint-1"));
assert_eq!(csp.to_string(), "report-to endpoint-1");
let csp = CSP::new_with(Directive::ReportUri(
ReportUris::new_with("https://r1.example.org").push("https://r2.example.org"),
));
assert_eq!(
csp.to_string(),
"report-uri https://r1.example.org https://r2.example.org"
);
let csp = CSP::new_with(Directive::TrustedTypes(vec!["hello", "hello2"]));
assert_eq!(csp.to_string(), "trusted-types hello hello2");
let csp = CSP::new_with(Directive::UpgradeInsecureRequests);
assert_eq!(csp.to_string(), "upgrade-insecure-requests");
}
}