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.strip_prefix(&format!("{}-", alg.as_str())) {
189 return Ok(Self::Hash {
190 algorithm: alg,
191 value: Cow::Owned(hash.to_owned()),
192 });
193 }
194 }
195 return HostSource::try_parse(s)
200 .map(Self::Host)
201 .map_err(|_err| Error::invalid());
202 }
203
204 if let Some(scheme) = s.strip_suffix(':')
206 && !scheme.is_empty()
207 && !s.contains('/')
208 {
209 let proto = Protocol::try_from(scheme).map_err(|_err| Error::invalid())?;
210 return Ok(Self::Scheme(proto));
211 }
212
213 HostSource::try_parse(s).map(Self::Host)
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn keywords_round_trip() {
224 for (s, want) in [
225 ("'self'", SourceExpression::SelfOrigin),
226 ("'none'", SourceExpression::None),
227 ("'unsafe-inline'", SourceExpression::UnsafeInline),
228 ("'unsafe-eval'", SourceExpression::UnsafeEval),
229 ("'strict-dynamic'", SourceExpression::StrictDynamic),
230 ("*", SourceExpression::Wildcard),
231 ] {
232 assert_eq!(SourceExpression::from_str(s).unwrap(), want);
233 assert_eq!(want.to_string(), s);
234 }
235 }
236
237 #[test]
238 fn scheme_uses_typed_protocol() {
239 let parsed = SourceExpression::from_str("data:").unwrap();
240 assert!(matches!(parsed, SourceExpression::Scheme(ref p) if p.as_str() == "data"));
241 assert_eq!(parsed.to_string(), "data:");
242
243 let https = SourceExpression::scheme(Protocol::HTTPS);
244 assert_eq!(https.to_string(), "https:");
245 }
246
247 #[test]
248 fn host_uses_typed_host_source() {
249 let parsed = SourceExpression::from_str("https://raw.githubusercontent.com").unwrap();
250 match parsed {
251 SourceExpression::Host(h) => {
252 assert_eq!(h.scheme().unwrap().as_str(), "https");
253 assert_eq!(h.host().as_ref(), "raw.githubusercontent.com");
254 }
255 other => panic!("expected Host, got {other:?}"),
256 }
257 }
258
259 #[test]
260 fn wildcard_subdomain_host_round_trips() {
261 let parsed = SourceExpression::from_str("*.example.com").unwrap();
262 assert_eq!(parsed.to_string(), "*.example.com");
263 }
264
265 #[test]
266 fn nonce_and_hash_round_trip() {
267 assert_eq!(
268 SourceExpression::from_str("'nonce-abc'")
269 .unwrap()
270 .to_string(),
271 "'nonce-abc'"
272 );
273 let h = SourceExpression::from_str("'sha384-xyz'").unwrap();
274 assert!(matches!(
275 h,
276 SourceExpression::Hash {
277 algorithm: HashAlgorithm::Sha384,
278 ..
279 }
280 ));
281 assert_eq!(h.to_string(), "'sha384-xyz'");
282 }
283
284 #[test]
285 fn from_impls_construct_typed_sources() {
286 let from_proto: SourceExpression = Protocol::HTTPS.into();
287 assert_eq!(from_proto.to_string(), "https:");
288
289 let from_domain: SourceExpression = Domain::from_static("example.com").into();
290 assert_eq!(from_domain.to_string(), "example.com");
291 }
292}