spin_manifest/compat/
allowed_http_hosts.rs1use anyhow::{Result, anyhow};
2use itertools::Itertools;
3use url::Url;
4
5const ALLOW_ALL_HOSTS: &str = "insecure:allow-all";
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum AllowedHttpHosts {
10 AllowAll,
12 AllowSpecific(Vec<AllowedHttpHost>),
14}
15
16impl Default for AllowedHttpHosts {
17 fn default() -> Self {
18 Self::AllowSpecific(vec![])
19 }
20}
21
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
24pub struct AllowedHttpHost {
25 pub(crate) domain: String,
26 pub(crate) port: Option<u16>,
27}
28
29impl AllowedHttpHost {
30 pub fn new(name: impl Into<String>, port: Option<u16>) -> Self {
32 Self {
33 domain: name.into(),
34 port,
35 }
36 }
37}
38
39pub fn parse_allowed_http_hosts(list: &[impl AsRef<str>]) -> Result<AllowedHttpHosts> {
41 if list.iter().any(|domain| domain.as_ref() == ALLOW_ALL_HOSTS) {
42 Ok(AllowedHttpHosts::AllowAll)
43 } else {
44 let (hosts, errors): (Vec<_>, Vec<_>) = list
45 .iter()
46 .map(|h| parse_allowed_http_host(h.as_ref()))
47 .partition_result();
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
106#[cfg(test)]
107mod test {
108 use super::*;
109
110 impl AllowedHttpHost {
111 fn host(name: impl Into<String>) -> Self {
113 Self {
114 domain: name.into(),
115 port: None,
116 }
117 }
118
119 fn host_and_port(name: impl Into<String>, port: u16) -> Self {
121 Self {
122 domain: name.into(),
123 port: Some(port),
124 }
125 }
126 }
127
128 #[test]
129 fn test_allowed_hosts_accepts_http_url() {
130 assert_eq!(
131 AllowedHttpHost::host("spin.fermyon.dev"),
132 parse_allowed_http_host("http://spin.fermyon.dev").unwrap()
133 );
134 assert_eq!(
135 AllowedHttpHost::host("spin.fermyon.dev"),
136 parse_allowed_http_host("http://spin.fermyon.dev/").unwrap()
137 );
138 assert_eq!(
139 AllowedHttpHost::host("spin.fermyon.dev"),
140 parse_allowed_http_host("https://spin.fermyon.dev").unwrap()
141 );
142 }
143
144 #[test]
145 fn test_allowed_hosts_accepts_http_url_with_port() {
146 assert_eq!(
147 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
148 parse_allowed_http_host("http://spin.fermyon.dev:4444").unwrap()
149 );
150 assert_eq!(
151 AllowedHttpHost::host_and_port("spin.fermyon.dev", 4444),
152 parse_allowed_http_host("http://spin.fermyon.dev:4444/").unwrap()
153 );
154 assert_eq!(
155 AllowedHttpHost::host_and_port("spin.fermyon.dev", 5555),
156 parse_allowed_http_host("https://spin.fermyon.dev:5555").unwrap()
157 );
158 }
159
160 #[test]
161 fn test_allowed_hosts_accepts_plain_host() {
162 assert_eq!(
163 AllowedHttpHost::host("spin.fermyon.dev"),
164 parse_allowed_http_host("spin.fermyon.dev").unwrap()
165 );
166 }
167
168 #[test]
169 fn test_allowed_hosts_accepts_plain_host_with_port() {
170 assert_eq!(
171 AllowedHttpHost::host_and_port("spin.fermyon.dev", 7777),
172 parse_allowed_http_host("spin.fermyon.dev:7777").unwrap()
173 );
174 }
175
176 #[test]
177 fn test_allowed_hosts_accepts_self() {
178 assert_eq!(
179 AllowedHttpHost::host("self"),
180 parse_allowed_http_host("self").unwrap()
181 );
182 }
183
184 #[test]
185 fn test_allowed_hosts_accepts_localhost_addresses() {
186 assert_eq!(
187 AllowedHttpHost::host("localhost"),
188 parse_allowed_http_host("localhost").unwrap()
189 );
190 assert_eq!(
191 AllowedHttpHost::host("localhost"),
192 parse_allowed_http_host("http://localhost").unwrap()
193 );
194 assert_eq!(
195 AllowedHttpHost::host_and_port("localhost", 3001),
196 parse_allowed_http_host("localhost:3001").unwrap()
197 );
198 assert_eq!(
199 AllowedHttpHost::host_and_port("localhost", 3001),
200 parse_allowed_http_host("http://localhost:3001").unwrap()
201 );
202 }
203
204 #[test]
205 fn test_allowed_hosts_accepts_ip_addresses() {
206 assert_eq!(
207 AllowedHttpHost::host("192.168.1.1"),
208 parse_allowed_http_host("192.168.1.1").unwrap()
209 );
210 assert_eq!(
211 AllowedHttpHost::host("192.168.1.1"),
212 parse_allowed_http_host("http://192.168.1.1").unwrap()
213 );
214 assert_eq!(
215 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
216 parse_allowed_http_host("192.168.1.1:3002").unwrap()
217 );
218 assert_eq!(
219 AllowedHttpHost::host_and_port("192.168.1.1", 3002),
220 parse_allowed_http_host("http://192.168.1.1:3002").unwrap()
221 );
222 assert_eq!(
223 AllowedHttpHost::host("[::1]"),
224 parse_allowed_http_host("[::1]").unwrap()
225 );
226 assert_eq!(
227 AllowedHttpHost::host_and_port("[::1]", 8001),
228 parse_allowed_http_host("http://[::1]:8001").unwrap()
229 );
230 }
231
232 #[test]
233 fn test_allowed_hosts_rejects_path() {
234 assert!(parse_allowed_http_host("http://spin.fermyon.dev/a").is_err());
235 assert!(parse_allowed_http_host("http://spin.fermyon.dev:6666/a/b").is_err());
236 }
237
238 #[test]
239 fn test_allowed_hosts_rejects_ftp_url() {
240 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev").is_err());
241 assert!(parse_allowed_http_host("ftp://spin.fermyon.dev:6666").is_err());
242 }
243
244 #[test]
245 fn test_allowed_hosts_respects_allow_all() {
246 assert_eq!(
247 AllowedHttpHosts::AllowAll,
248 parse_allowed_http_hosts(&["insecure:allow-all"]).unwrap()
249 );
250 assert_eq!(
251 AllowedHttpHosts::AllowAll,
252 parse_allowed_http_hosts(&["spin.fermyon.dev", "insecure:allow-all"]).unwrap()
253 );
254 }
255}