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!("{} isn't a valid host or host:port string", text))?;
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 =
83 Url::parse(text).map_err(|e| format!("{} isn't a valid HTTP host URL: {}", text, e))?;
84
85 if !matches!(url.scheme(), "http" | "https") {
86 return Err(format!("{} isn't a valid host or host:port string", text));
87 }
88
89 parse_allowed_http_host_from_http_url(&url, text)
90}
91
92fn parse_allowed_http_host_from_http_url(url: &Url, text: &str) -> Result<AllowedHttpHost, String> {
93 let host = url
94 .host_str()
95 .ok_or_else(|| format!("{} doesn't contain a host name", text))?;
96
97 let has_path = url.path().len() > 1; if has_path {
99 return Err(format!(
100 "{} contains a path, should be host and optional port only",
101 text
102 ));
103 }
104
105 Ok(AllowedHttpHost::new(host, url.port()))
106}
107
108fn partition_results<T, E>(results: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) {
109 let mut oks = Vec::with_capacity(results.len());
111 let mut errs = vec![];
112
113 for result in results {
114 match result {
115 Ok(t) => oks.push(t),
116 Err(e) => errs.push(e),
117 }
118 }
119
120 (oks, errs)
121}
122
123#[cfg(test)]
124mod test {
125 use super::*;
126
127 impl AllowedHttpHost {
128 fn host(name: impl Into<String>) -> Self {
130 Self {
131 domain: name.into(),
132 port: None,
133 }
134 }
135
136 fn host_and_port(name: impl Into<String>, port: u16) -> Self {
138 Self {
139 domain: name.into(),
140 port: Some(port),
141 }
142 }
143 }
144
145 #[test]
146 fn test_allowed_hosts_accepts_http_url() {
147 assert_eq!(
148 AllowedHttpHost::host("spin.fermyon.dev"),
149 parse_allowed_http_host("http://spin.fermyon.dev").unwrap()
150 );
151 assert_eq!(
152 AllowedHttpHost::host("spin.fermyon.dev"),
153 parse_allowed_http_host("http://spin.fermyon.dev/").unwrap()
154 );
155 assert_eq!(
156 AllowedHttpHost::host("spin.fermyon.dev"),
157 parse_allowed_http_host("https://spin.fermyon.dev").unwrap()
158 );
159 }
160
161 #[test]
162 fn test_allowed_hosts_accepts_http_url_with_port() {
163 assert_eq!(
164 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
165 parse_allowed_http_host("http://spin.fermyon.dev:4444").unwrap()
166 );
167 assert_eq!(
168 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
169 parse_allowed_http_host("http://spin.fermyon.dev:4444/").unwrap()
170 );
171 assert_eq!(
172 AllowedHttpHost::host_and_port("spin.fermyon.dev", 5555),
173 parse_allowed_http_host("https://spin.fermyon.dev:5555").unwrap()
174 );
175 }
176
177 #[test]
178 fn test_allowed_hosts_accepts_plain_host() {
179 assert_eq!(
180 AllowedHttpHost::host("spin.fermyon.dev"),
181 parse_allowed_http_host("spin.fermyon.dev").unwrap()
182 );
183 }
184
185 #[test]
186 fn test_allowed_hosts_accepts_plain_host_with_port() {
187 assert_eq!(
188 AllowedHttpHost::host_and_port("spin.fermyon.dev", 7777),
189 parse_allowed_http_host("spin.fermyon.dev:7777").unwrap()
190 );
191 }
192
193 #[test]
194 fn test_allowed_hosts_accepts_self() {
195 assert_eq!(
196 AllowedHttpHost::host("self"),
197 parse_allowed_http_host("self").unwrap()
198 );
199 }
200
201 #[test]
202 fn test_allowed_hosts_accepts_localhost_addresses() {
203 assert_eq!(
204 AllowedHttpHost::host("localhost"),
205 parse_allowed_http_host("localhost").unwrap()
206 );
207 assert_eq!(
208 AllowedHttpHost::host("localhost"),
209 parse_allowed_http_host("http://localhost").unwrap()
210 );
211 assert_eq!(
212 AllowedHttpHost::host_and_port("localhost", 3001),
213 parse_allowed_http_host("localhost:3001").unwrap()
214 );
215 assert_eq!(
216 AllowedHttpHost::host_and_port("localhost", 3001),
217 parse_allowed_http_host("http://localhost:3001").unwrap()
218 );
219 }
220
221 #[test]
222 fn test_allowed_hosts_accepts_ip_addresses() {
223 assert_eq!(
224 AllowedHttpHost::host("192.168.1.1"),
225 parse_allowed_http_host("192.168.1.1").unwrap()
226 );
227 assert_eq!(
228 AllowedHttpHost::host("192.168.1.1"),
229 parse_allowed_http_host("http://192.168.1.1").unwrap()
230 );
231 assert_eq!(
232 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
233 parse_allowed_http_host("192.168.1.1:3002").unwrap()
234 );
235 assert_eq!(
236 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
237 parse_allowed_http_host("http://192.168.1.1:3002").unwrap()
238 );
239 assert_eq!(
240 AllowedHttpHost::host("[::1]"),
241 parse_allowed_http_host("[::1]").unwrap()
242 );
243 assert_eq!(
244 AllowedHttpHost::host_and_port("[::1]", 8001),
245 parse_allowed_http_host("http://[::1]:8001").unwrap()
246 );
247 }
248
249 #[test]
250 fn test_allowed_hosts_rejects_path() {
251 assert!(parse_allowed_http_host("http://spin.fermyon.dev/a").is_err());
252 assert!(parse_allowed_http_host("http://spin.fermyon.dev:6666/a/b").is_err());
253 }
254
255 #[test]
256 fn test_allowed_hosts_rejects_ftp_url() {
257 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev").is_err());
258 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev:6666").is_err());
259 }
260
261 #[test]
262 fn test_allowed_hosts_respects_allow_all() {
263 assert_eq!(
264 AllowedHttpHosts::AllowAll,
265 parse_allowed_http_hosts(&["insecure:allow-all"]).unwrap()
266 );
267 assert_eq!(
268 AllowedHttpHosts::AllowAll,
269 parse_allowed_http_hosts(&["spin.fermyon.dev", "insecure:allow-all"]).unwrap()
270 );
271 }
272}