rama_http_headers/common/content_security_policy/
host_source.rs1use std::borrow::Cow;
2use std::fmt;
3use std::str::FromStr;
4
5use rama_net::Protocol;
6use rama_net::address::Domain;
7use rama_utils::macros::generate_set_and_with;
8
9use crate::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum HostSourcePort {
17 Number(u16),
19 Any,
21}
22
23impl fmt::Display for HostSourcePort {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Number(n) => write!(f, "{n}"),
27 Self::Any => f.write_str("*"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct HostSource {
40 scheme: Option<Protocol>,
41 host: Domain,
42 port: Option<HostSourcePort>,
43 path: Option<Cow<'static, str>>,
44}
45
46impl HostSource {
49 #[must_use]
51 pub fn new(host: Domain) -> Self {
52 Self {
53 scheme: None,
54 host,
55 port: None,
56 path: None,
57 }
58 }
59
60 pub fn try_parse(s: &str) -> Result<Self, Error> {
64 let (scheme, rest) = match s.find("://") {
65 Some(idx) => {
66 let proto = Protocol::try_from(&s[..idx]).map_err(|_err| Error::invalid())?;
67 (Some(proto), &s[idx + 3..])
68 }
69 None => (None, s),
70 };
71 let (host_port, path) = match rest.find('/') {
72 Some(idx) => (&rest[..idx], Some(Cow::Owned(rest[idx..].to_owned()))),
73 None => (rest, None),
74 };
75 let (host_str, port) = match host_port.rfind(':') {
76 Some(idx) => {
77 let port_str = &host_port[idx + 1..];
78 let parsed = if port_str == "*" {
79 Some(HostSourcePort::Any)
80 } else {
81 let n: u16 = port_str.parse().map_err(|_err| Error::invalid())?;
82 Some(HostSourcePort::Number(n))
83 };
84 (&host_port[..idx], parsed)
85 }
86 None => (host_port, None),
87 };
88 let host = Domain::try_from(host_str).map_err(|_err| Error::invalid())?;
89 Ok(Self {
90 scheme,
91 host,
92 port,
93 path,
94 })
95 }
96
97 pub fn scheme(&self) -> Option<&Protocol> {
99 self.scheme.as_ref()
100 }
101
102 pub fn host(&self) -> &Domain {
104 &self.host
105 }
106
107 pub fn port(&self) -> Option<HostSourcePort> {
109 self.port
110 }
111
112 pub fn path(&self) -> Option<&str> {
115 self.path.as_deref()
116 }
117
118 generate_set_and_with! {
119 pub fn scheme(mut self, scheme: Protocol) -> Self {
121 self.scheme = Some(scheme);
122 self
123 }
124 }
125
126 generate_set_and_with! {
127 pub fn port(mut self, port: u16) -> Self {
129 self.port = Some(HostSourcePort::Number(port));
130 self
131 }
132 }
133
134 generate_set_and_with! {
135 pub fn any_port(mut self) -> Self {
137 self.port = Some(HostSourcePort::Any);
138 self
139 }
140 }
141
142 generate_set_and_with! {
143 pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
146 self.path = Some(path.into());
147 self
148 }
149 }
150}
151
152impl From<Domain> for HostSource {
153 fn from(host: Domain) -> Self {
154 Self::new(host)
155 }
156}
157
158impl<'a> TryFrom<&'a str> for HostSource {
159 type Error = Error;
160 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
161 Self::try_parse(s)
162 }
163}
164
165impl TryFrom<String> for HostSource {
166 type Error = Error;
167 fn try_from(s: String) -> Result<Self, Self::Error> {
168 Self::try_parse(&s)
169 }
170}
171
172impl FromStr for HostSource {
173 type Err = Error;
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 Self::try_parse(s)
176 }
177}
178
179impl fmt::Display for HostSource {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 if let Some(scheme) = &self.scheme {
182 write!(f, "{}://", scheme.as_str())?;
183 }
184 write!(f, "{}", self.host)?;
185 if let Some(port) = &self.port {
186 write!(f, ":{port}")?;
187 }
188 if let Some(path) = &self.path {
189 f.write_str(path)?;
190 }
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn bare_domain_round_trips() {
201 let h = HostSource::try_parse("example.com").unwrap();
202 assert_eq!(h.to_string(), "example.com");
203 assert_eq!(h.scheme(), None);
204 assert_eq!(h.port(), None);
205 assert_eq!(h.path(), None);
206 }
207
208 #[test]
209 fn scheme_plus_host_round_trips() {
210 let h = HostSource::try_parse("https://raw.githubusercontent.com").unwrap();
211 assert_eq!(h.to_string(), "https://raw.githubusercontent.com");
212 assert_eq!(h.scheme().unwrap().as_str(), "https");
213 }
214
215 #[test]
216 fn full_form_round_trips() {
217 let h = HostSource::try_parse("https://*.example.com:8443/api/*").unwrap();
218 assert_eq!(h.to_string(), "https://*.example.com:8443/api/*");
219 assert!(h.host().is_wildcard());
220 assert_eq!(h.port(), Some(HostSourcePort::Number(8443)));
221 assert_eq!(h.path(), Some("/api/*"));
222 }
223
224 #[test]
225 fn any_port_round_trips() {
226 let h = HostSource::try_parse("https://example.com:*").unwrap();
227 assert_eq!(h.to_string(), "https://example.com:*");
228 assert_eq!(h.port(), Some(HostSourcePort::Any));
229 }
230
231 #[test]
232 fn builder_assembles_components() {
233 let h = HostSource::new(Domain::from_static("example.com"))
234 .with_scheme(Protocol::HTTPS)
235 .with_port(8443)
236 .with_path("/api/");
237 assert_eq!(h.to_string(), "https://example.com:8443/api/");
238 }
239
240 #[test]
241 fn rejects_malformed_scheme() {
242 HostSource::try_parse("ht tp://example.com").unwrap_err();
243 }
244
245 #[test]
246 fn rejects_malformed_port() {
247 HostSource::try_parse("example.com:abc").unwrap_err();
248 HostSource::try_parse("example.com:99999").unwrap_err();
249 }
250}