spin_manifest/compat/
allowed_http_hosts.rs1use anyhow::{anyhow, Result};
2use url::Url;
3
4const ALLOW_ALL_HOSTS: &str = "insecure:allow-all";
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum AllowedHttpHosts {
9 AllowAll,
11 AllowSpecific(Vec<AllowedHttpHost>),
13}
14
15impl Default for AllowedHttpHosts {
16 fn default() -> Self {
17 Self::AllowSpecific(vec![])
18 }
19}
20
21#[derive(Clone, Debug, Default, Eq, PartialEq)]
23pub struct AllowedHttpHost {
24 pub(crate) domain: String,
25 pub(crate) port: Option<u16>,
26}
27
28impl AllowedHttpHost {
29 pub fn new(name: impl Into<String>, port: Option<u16>) -> Self {
31 Self {
32 domain: name.into(),
33 port,
34 }
35 }
36}
37
38pub fn parse_allowed_http_hosts(list: &[impl AsRef<str>]) -> Result<AllowedHttpHosts> {
40 if list.iter().any(|domain| domain.as_ref() == ALLOW_ALL_HOSTS) {
41 Ok(AllowedHttpHosts::AllowAll)
42 } else {
43 let parse_results = list
44 .iter()
45 .map(|h| parse_allowed_http_host(h.as_ref()))
46 .collect::<Vec<_>>();
47 let (hosts, errors) = partition_results(parse_results);
48
49 if errors.is_empty() {
50 Ok(AllowedHttpHosts::AllowSpecific(hosts))
51 } else {
52 Err(anyhow!(
53 "One or more allowed_http_hosts entries was invalid:\n{}",
54 errors.join("\n")
55 ))
56 }
57 }
58}
59
60fn parse_allowed_http_host(text: &str) -> Result<AllowedHttpHost, String> {
61 if text.contains("//") {
65 parse_allowed_http_host_from_schemed(text)
66 } else {
67 parse_allowed_http_host_from_unschemed(text)
68 }
69}
70
71fn parse_allowed_http_host_from_unschemed(text: &str) -> Result<AllowedHttpHost, String> {
72 let urlised = format!("http://{text}");
76 let fake_url = Url::parse(&urlised)
77 .map_err(|_| format!("{text} isn't a valid host or host:port string"))?;
78 parse_allowed_http_host_from_http_url(&fake_url, text)
79}
80
81fn parse_allowed_http_host_from_schemed(text: &str) -> Result<AllowedHttpHost, String> {
82 let url = Url::parse(text).map_err(|e| format!("{text} isn't a valid HTTP host URL: {e}"))?;
83
84 if !matches!(url.scheme(), "http" | "https") {
85 return Err(format!("{text} isn't a valid host or host:port string"));
86 }
87
88 parse_allowed_http_host_from_http_url(&url, text)
89}
90
91fn parse_allowed_http_host_from_http_url(url: &Url, text: &str) -> Result<AllowedHttpHost, String> {
92 let host = url
93 .host_str()
94 .ok_or_else(|| format!("{text} doesn't contain a host name"))?;
95
96 let has_path = url.path().len() > 1; if has_path {
98 return Err(format!(
99 "{text} contains a path, should be host and optional port only"
100 ));
101 }
102
103 Ok(AllowedHttpHost::new(host, url.port()))
104}
105
106fn partition_results<T, E>(results: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) {
107 let mut oks = Vec::with_capacity(results.len());
109 let mut errs = vec![];
110
111 for result in results {
112 match result {
113 Ok(t) => oks.push(t),
114 Err(e) => errs.push(e),
115 }
116 }
117
118 (oks, errs)
119}
120
121#[cfg(test)]
122mod test {
123 use super::*;
124
125 impl AllowedHttpHost {
126 fn host(name: impl Into<String>) -> Self {
128 Self {
129 domain: name.into(),
130 port: None,
131 }
132 }
133
134 fn host_and_port(name: impl Into<String>, port: u16) -> Self {
136 Self {
137 domain: name.into(),
138 port: Some(port),
139 }
140 }
141 }
142
143 #[test]
144 fn test_allowed_hosts_accepts_http_url() {
145 assert_eq!(
146 AllowedHttpHost::host("spin.fermyon.dev"),
147 parse_allowed_http_host("http://spin.fermyon.dev").unwrap()
148 );
149 assert_eq!(
150 AllowedHttpHost::host("spin.fermyon.dev"),
151 parse_allowed_http_host("http://spin.fermyon.dev/").unwrap()
152 );
153 assert_eq!(
154 AllowedHttpHost::host("spin.fermyon.dev"),
155 parse_allowed_http_host("https://spin.fermyon.dev").unwrap()
156 );
157 }
158
159 #[test]
160 fn test_allowed_hosts_accepts_http_url_with_port() {
161 assert_eq!(
162 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
163 parse_allowed_http_host("http://spin.fermyon.dev:4444").unwrap()
164 );
165 assert_eq!(
166 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
167 parse_allowed_http_host("http://spin.fermyon.dev:4444/").unwrap()
168 );
169 assert_eq!(
170 AllowedHttpHost::host_and_port("spin.fermyon.dev", 5555),
171 parse_allowed_http_host("https://spin.fermyon.dev:5555").unwrap()
172 );
173 }
174
175 #[test]
176 fn test_allowed_hosts_accepts_plain_host() {
177 assert_eq!(
178 AllowedHttpHost::host("spin.fermyon.dev"),
179 parse_allowed_http_host("spin.fermyon.dev").unwrap()
180 );
181 }
182
183 #[test]
184 fn test_allowed_hosts_accepts_plain_host_with_port() {
185 assert_eq!(
186 AllowedHttpHost::host_and_port("spin.fermyon.dev", 7777),
187 parse_allowed_http_host("spin.fermyon.dev:7777").unwrap()
188 );
189 }
190
191 #[test]
192 fn test_allowed_hosts_accepts_self() {
193 assert_eq!(
194 AllowedHttpHost::host("self"),
195 parse_allowed_http_host("self").unwrap()
196 );
197 }
198
199 #[test]
200 fn test_allowed_hosts_accepts_localhost_addresses() {
201 assert_eq!(
202 AllowedHttpHost::host("localhost"),
203 parse_allowed_http_host("localhost").unwrap()
204 );
205 assert_eq!(
206 AllowedHttpHost::host("localhost"),
207 parse_allowed_http_host("http://localhost").unwrap()
208 );
209 assert_eq!(
210 AllowedHttpHost::host_and_port("localhost", 3001),
211 parse_allowed_http_host("localhost:3001").unwrap()
212 );
213 assert_eq!(
214 AllowedHttpHost::host_and_port("localhost", 3001),
215 parse_allowed_http_host("http://localhost:3001").unwrap()
216 );
217 }
218
219 #[test]
220 fn test_allowed_hosts_accepts_ip_addresses() {
221 assert_eq!(
222 AllowedHttpHost::host("192.168.1.1"),
223 parse_allowed_http_host("192.168.1.1").unwrap()
224 );
225 assert_eq!(
226 AllowedHttpHost::host("192.168.1.1"),
227 parse_allowed_http_host("http://192.168.1.1").unwrap()
228 );
229 assert_eq!(
230 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
231 parse_allowed_http_host("192.168.1.1:3002").unwrap()
232 );
233 assert_eq!(
234 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
235 parse_allowed_http_host("http://192.168.1.1:3002").unwrap()
236 );
237 assert_eq!(
238 AllowedHttpHost::host("[::1]"),
239 parse_allowed_http_host("[::1]").unwrap()
240 );
241 assert_eq!(
242 AllowedHttpHost::host_and_port("[::1]", 8001),
243 parse_allowed_http_host("http://[::1]:8001").unwrap()
244 );
245 }
246
247 #[test]
248 fn test_allowed_hosts_rejects_path() {
249 assert!(parse_allowed_http_host("http://spin.fermyon.dev/a").is_err());
250 assert!(parse_allowed_http_host("http://spin.fermyon.dev:6666/a/b").is_err());
251 }
252
253 #[test]
254 fn test_allowed_hosts_rejects_ftp_url() {
255 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev").is_err());
256 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev:6666").is_err());
257 }
258
259 #[test]
260 fn test_allowed_hosts_respects_allow_all() {
261 assert_eq!(
262 AllowedHttpHosts::AllowAll,
263 parse_allowed_http_hosts(&["insecure:allow-all"]).unwrap()
264 );
265 assert_eq!(
266 AllowedHttpHosts::AllowAll,
267 parse_allowed_http_hosts(&["spin.fermyon.dev", "insecure:allow-all"]).unwrap()
268 );
269 }
270}