rama_http_headers/common/content_security_policy/
source_expression.rs1use std::borrow::Cow;
2use std::fmt::{self, Write as _};
3use std::str::FromStr;
4
5use rama_net::Protocol;
6use rama_net::address::Domain;
7
8use crate::Error;
9
10use super::host_source::HostSource;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum HashAlgorithm {
15 Sha256,
17 Sha384,
19 Sha512,
21}
22
23impl HashAlgorithm {
24 pub const fn as_str(&self) -> &'static str {
27 match self {
28 Self::Sha256 => "sha256",
29 Self::Sha384 => "sha384",
30 Self::Sha512 => "sha512",
31 }
32 }
33}
34
35impl fmt::Display for HashAlgorithm {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.write_str(self.as_str())
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum SourceExpression {
48 SelfOrigin,
50 None,
52 UnsafeInline,
55 UnsafeEval,
57 StrictDynamic,
60 UnsafeHashes,
63 WasmUnsafeEval,
66 ReportSample,
68 InlineSpeculationRules,
70 Wildcard,
73 Scheme(Protocol),
76 Host(HostSource),
79 Nonce(Cow<'static, str>),
81 Hash {
83 algorithm: HashAlgorithm,
84 value: Cow<'static, str>,
86 },
87}
88
89impl SourceExpression {
90 pub fn scheme(scheme: impl Into<Protocol>) -> Self {
93 Self::Scheme(scheme.into())
94 }
95
96 pub fn host(host: impl Into<HostSource>) -> Self {
100 Self::Host(host.into())
101 }
102
103 pub fn nonce(nonce: impl Into<Cow<'static, str>>) -> Self {
105 Self::Nonce(nonce.into())
106 }
107
108 pub fn hash(algorithm: HashAlgorithm, value: impl Into<Cow<'static, str>>) -> Self {
110 Self::Hash {
111 algorithm,
112 value: value.into(),
113 }
114 }
115}
116
117impl fmt::Display for SourceExpression {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 Self::SelfOrigin => f.write_str("'self'"),
121 Self::None => f.write_str("'none'"),
122 Self::UnsafeInline => f.write_str("'unsafe-inline'"),
123 Self::UnsafeEval => f.write_str("'unsafe-eval'"),
124 Self::StrictDynamic => f.write_str("'strict-dynamic'"),
125 Self::UnsafeHashes => f.write_str("'unsafe-hashes'"),
126 Self::WasmUnsafeEval => f.write_str("'wasm-unsafe-eval'"),
127 Self::ReportSample => f.write_str("'report-sample'"),
128 Self::InlineSpeculationRules => f.write_str("'inline-speculation-rules'"),
129 Self::Wildcard => f.write_char('*'),
130 Self::Scheme(p) => write!(f, "{}:", p.as_str()),
131 Self::Host(h) => write!(f, "{h}"),
132 Self::Nonce(n) => write!(f, "'nonce-{n}'"),
133 Self::Hash { algorithm, value } => write!(f, "'{algorithm}-{value}'"),
134 }
135 }
136}
137
138impl From<Protocol> for SourceExpression {
139 fn from(p: Protocol) -> Self {
140 Self::Scheme(p)
141 }
142}
143
144impl From<Domain> for SourceExpression {
145 fn from(d: Domain) -> Self {
146 Self::Host(HostSource::new(d))
147 }
148}
149
150impl From<HostSource> for SourceExpression {
151 fn from(h: HostSource) -> Self {
152 Self::Host(h)
153 }
154}
155
156impl FromStr for SourceExpression {
157 type Err = Error;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 match s {
165 "*" => return Ok(Self::Wildcard),
166 "'self'" => return Ok(Self::SelfOrigin),
167 "'none'" => return Ok(Self::None),
168 "'unsafe-inline'" => return Ok(Self::UnsafeInline),
169 "'unsafe-eval'" => return Ok(Self::UnsafeEval),
170 "'strict-dynamic'" => return Ok(Self::StrictDynamic),
171 "'unsafe-hashes'" => return Ok(Self::UnsafeHashes),
172 "'wasm-unsafe-eval'" => return Ok(Self::WasmUnsafeEval),
173 "'report-sample'" => return Ok(Self::ReportSample),
174 "'inline-speculation-rules'" => return Ok(Self::InlineSpeculationRules),
175 _ => {}
176 }
177
178 if let Some(inner) = s.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')) {
180 if let Some(nonce) = inner.strip_prefix("nonce-") {
181 return Ok(Self::Nonce(Cow::Owned(nonce.to_owned())));
182 }
183 for alg in [
184 HashAlgorithm::Sha256,
185 HashAlgorithm::Sha384,
186 HashAlgorithm::Sha512,
187 ] {
188 if let Some(hash) = inner
189 .strip_prefix(alg.as_str())
190 .and_then(|value| value.strip_prefix('-'))
191 {
192 return Ok(Self::Hash {
193 algorithm: alg,
194 value: Cow::Owned(hash.to_owned()),
195 });
196 }
197 }
198 return HostSource::try_parse(s)
203 .map(Self::Host)
204 .map_err(|_err| Error::invalid());
205 }
206
207 if let Some(scheme) = s.strip_suffix(':')
209 && !scheme.is_empty()
210 && !s.contains('/')
211 {
212 let proto = Protocol::try_from(scheme).map_err(|_err| Error::invalid())?;
213 return Ok(Self::Scheme(proto));
214 }
215
216 HostSource::try_parse(s).map(Self::Host)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn keywords_round_trip() {
227 for (s, want) in [
228 ("'self'", SourceExpression::SelfOrigin),
229 ("'none'", SourceExpression::None),
230 ("'unsafe-inline'", SourceExpression::UnsafeInline),
231 ("'unsafe-eval'", SourceExpression::UnsafeEval),
232 ("'strict-dynamic'", SourceExpression::StrictDynamic),
233 ("*", SourceExpression::Wildcard),
234 ] {
235 assert_eq!(SourceExpression::from_str(s).unwrap(), want);
236 assert_eq!(want.to_string(), s);
237 }
238 }
239
240 #[test]
241 fn scheme_uses_typed_protocol() {
242 let parsed = SourceExpression::from_str("data:").unwrap();
243 assert!(matches!(parsed, SourceExpression::Scheme(ref p) if p.as_str() == "data"));
244 assert_eq!(parsed.to_string(), "data:");
245
246 let https = SourceExpression::scheme(Protocol::HTTPS);
247 assert_eq!(https.to_string(), "https:");
248 }
249
250 #[test]
251 fn host_uses_typed_host_source() {
252 let parsed = SourceExpression::from_str("https://raw.githubusercontent.com").unwrap();
253 match parsed {
254 SourceExpression::Host(h) => {
255 assert_eq!(h.scheme().unwrap().as_str(), "https");
256 assert_eq!(h.host().as_ref(), "raw.githubusercontent.com");
257 }
258 other => panic!("expected Host, got {other:?}"),
259 }
260 }
261
262 #[test]
263 fn wildcard_subdomain_host_round_trips() {
264 let parsed = SourceExpression::from_str("*.example.com").unwrap();
265 assert_eq!(parsed.to_string(), "*.example.com");
266 }
267
268 #[test]
269 fn nonce_and_hash_round_trip() {
270 assert_eq!(
271 SourceExpression::from_str("'nonce-abc'")
272 .unwrap()
273 .to_string(),
274 "'nonce-abc'"
275 );
276 let h = SourceExpression::from_str("'sha384-xyz'").unwrap();
277 assert!(matches!(
278 h,
279 SourceExpression::Hash {
280 algorithm: HashAlgorithm::Sha384,
281 ..
282 }
283 ));
284 assert_eq!(h.to_string(), "'sha384-xyz'");
285 }
286
287 #[test]
288 fn from_impls_construct_typed_sources() {
289 let from_proto: SourceExpression = Protocol::HTTPS.into();
290 assert_eq!(from_proto.to_string(), "https:");
291
292 let from_domain: SourceExpression = Domain::from_static("example.com").into();
293 assert_eq!(from_domain.to_string(), "example.com");
294 }
295}