1use std::ops::Range;
2use std::sync::Arc;
3
4use anyhow::{Context as _, bail, ensure};
5use futures_util::future::{BoxFuture, Shared};
6use spin_expressions::SyncResolver;
7use url::Host;
8
9pub const SERVICE_CHAINING_DOMAIN: &str = "spin.internal";
11pub const SERVICE_CHAINING_DOMAIN_SUFFIX: &str = ".spin.internal";
13
14pub type SharedFutureResult<T> = Shared<BoxFuture<'static, Result<Arc<T>, Arc<anyhow::Error>>>>;
16
17#[derive(Clone)]
19pub struct OutboundAllowedHosts {
20 allowed_hosts_future: SharedFutureResult<AllowedHostsConfig>,
21 disallowed_host_handler: Option<Arc<dyn DisallowedHostHandler>>,
22 is_empty: bool, }
24
25impl OutboundAllowedHosts {
26 pub fn new(
28 allowed_hosts_future: SharedFutureResult<AllowedHostsConfig>,
29 disallowed_host_handler: Option<Arc<dyn DisallowedHostHandler>>,
30 is_empty: bool,
31 ) -> Self {
32 Self {
33 allowed_hosts_future,
34 disallowed_host_handler,
35 is_empty,
36 }
37 }
38
39 pub async fn check_url(&self, url: &str, scheme: &str) -> anyhow::Result<bool> {
44 tracing::debug!("Checking outbound networking request to '{url}'");
45 let url = match OutboundUrl::parse(url, scheme) {
46 Ok(url) => url,
47 Err(err) => {
48 tracing::warn!(%err,
49 "A component tried to make a request to a url that could not be parsed: {url}",
50 );
51 return Ok(false);
52 }
53 };
54
55 let allowed_hosts = self.resolve().await?;
56 let is_allowed = allowed_hosts.allows(&url);
57 if !is_allowed {
58 tracing::debug!("Disallowed outbound networking request to '{url}'");
59 self.report_disallowed_host(url.scheme(), &url.authority());
60 }
61 Ok(is_allowed)
62 }
63
64 pub async fn check_relative_url(&self, schemes: &[&str]) -> anyhow::Result<bool> {
69 tracing::debug!("Checking relative outbound networking request with schemes {schemes:?}");
70 let allowed_hosts = self.resolve().await?;
71 let is_allowed = allowed_hosts.allows_relative_url(schemes);
72 if !is_allowed {
73 tracing::debug!(
74 "Disallowed relative outbound networking request with schemes {schemes:?}"
75 );
76 let scheme = schemes.first().unwrap_or(&"");
77 self.report_disallowed_host(scheme, "self");
78 }
79 Ok(is_allowed)
80 }
81
82 async fn resolve(&self) -> anyhow::Result<Arc<AllowedHostsConfig>> {
83 self.allowed_hosts_future
84 .clone()
85 .await
86 .map_err(anyhow::Error::msg)
87 }
88
89 fn report_disallowed_host(&self, scheme: &str, authority: &str) {
90 if let Some(handler) = &self.disallowed_host_handler {
91 handler.handle_disallowed_host(scheme, authority);
92 }
93 }
94
95 pub fn is_empty(&self) -> bool {
99 self.is_empty
100 }
101}
102
103pub trait DisallowedHostHandler: Send + Sync {
105 fn handle_disallowed_host(&self, scheme: &str, authority: &str);
107}
108
109impl<F: Fn(&str, &str) + Send + Sync> DisallowedHostHandler for F {
110 fn handle_disallowed_host(&self, scheme: &str, authority: &str) {
111 self(scheme, authority);
112 }
113}
114
115#[derive(Eq, Debug, Clone)]
117pub struct AllowedHostConfig {
118 original: String,
119 scheme: SchemeConfig,
120 host: HostConfig,
121 port: PortConfig,
122}
123
124impl AllowedHostConfig {
125 pub fn parse(url: impl Into<String>) -> anyhow::Result<Self> {
127 let original = url.into();
128 let url = original.trim();
129 let Some((scheme, rest)) = url.split_once("://") else {
130 match url {
131 "*" | ":" | "" | "?" => bail!(
132 "{url:?} is not an allowed outbound host format.\nHosts must be in the form <scheme>://<host>[:<port>], with '*' wildcards allowed for each.\nIf you intended to allow all outbound networking, you can use '*://*:*' - this will obviate all network sandboxing.\nLearn more: https://spinframework.dev/v3/http-outbound#granting-http-permissions-to-components"
133 ),
134 _ => bail!(
135 "{url:?} does not contain a scheme (e.g., 'http://' or '*://')\nLearn more: https://spinframework.dev/v3/http-outbound#granting-http-permissions-to-components"
136 ),
137 }
138 };
139 let (host, port) = match rest.rsplit_once(':') {
140 None => (rest, ""),
141 Some((h, "")) => (h, ""),
142 Some((h, tail)) => {
143 if HostConfig::parse(rest).is_ok() {
144 (rest, "")
145 } else {
146 let port = match tail.split_once('/') {
147 Some((port, path)) => {
148 if !path.is_empty() {
149 bail!("{url:?} has a path but is not allowed to");
150 }
151 port
152 }
153 None => tail,
154 };
155 (h, port)
156 }
157 }
158 };
159
160 let port = PortConfig::parse(port, scheme)
161 .with_context(|| format!("Invalid allowed host port {port:?}"))?;
162 let scheme = SchemeConfig::parse(scheme)
163 .with_context(|| format!("Invalid allowed host scheme {scheme:?}"))?;
164 let host =
165 HostConfig::parse(host).with_context(|| format!("Invalid allowed host {host:?}"))?;
166
167 Ok(Self {
168 scheme,
169 host,
170 port,
171 original,
172 })
173 }
174
175 pub fn scheme(&self) -> &SchemeConfig {
176 &self.scheme
177 }
178
179 pub fn host(&self) -> &HostConfig {
180 &self.host
181 }
182
183 pub fn port(&self) -> &PortConfig {
184 &self.port
185 }
186
187 pub fn is_for_service_chaining(&self) -> bool {
189 self.host.is_for_service_chaining()
190 }
191
192 fn allows(&self, url: &OutboundUrl) -> bool {
194 self.scheme.allows(&url.scheme)
195 && self.host.allows(&url.host)
196 && self.port.allows(url.port, &url.scheme)
197 }
198
199 fn allows_relative(&self, schemes: &[&str]) -> bool {
202 schemes.iter().any(|s| self.scheme.allows(s)) && self.host.allows_relative()
203 }
204}
205
206impl PartialEq for AllowedHostConfig {
207 fn eq(&self, other: &Self) -> bool {
208 self.scheme == other.scheme && self.host == other.host && self.port == other.port
209 }
210}
211
212impl std::fmt::Display for AllowedHostConfig {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 f.write_str(&self.original)
215 }
216}
217
218#[derive(PartialEq, Eq, Debug, Clone)]
220pub enum SchemeConfig {
221 Any,
223 List(Vec<String>),
225}
226
227impl SchemeConfig {
228 fn parse(scheme: &str) -> anyhow::Result<Self> {
230 if scheme == "*" {
231 return Ok(Self::Any);
232 }
233
234 if scheme.starts_with('{') {
235 anyhow::bail!("scheme lists are not supported")
236 }
237
238 if scheme.chars().any(|c| !c.is_alphabetic()) {
239 anyhow::bail!("only alphabetic characters are allowed");
240 }
241
242 Ok(Self::List(vec![scheme.into()]))
243 }
244
245 pub fn allows_any(&self) -> bool {
247 matches!(self, Self::Any)
248 }
249
250 fn allows(&self, scheme: &str) -> bool {
252 match self {
253 SchemeConfig::Any => true,
254 SchemeConfig::List(l) => l.iter().any(|s| s.as_str() == scheme),
255 }
256 }
257}
258
259#[derive(Debug, PartialEq, Eq, Clone)]
261pub enum HostConfig {
262 Any,
263 AnySubdomain(String),
264 ToSelf,
265 Literal(Host),
266 Cidr(ip_network::IpNetwork),
267}
268
269impl HostConfig {
270 fn parse(mut host: &str) -> anyhow::Result<Self> {
272 host = host.trim();
273 if host == "*" {
274 return Ok(Self::Any);
275 }
276
277 if host == "self" || host == "self.alt" {
278 return Ok(Self::ToSelf);
279 }
280
281 if host.starts_with('{') {
282 ensure!(host.ends_with('}'));
283 bail!("host lists are not yet supported")
284 }
285
286 if let Ok(net) = ip_network::IpNetwork::from_str_truncate(host) {
287 return Ok(Self::Cidr(net));
288 }
289
290 host = host.strip_suffix('/').unwrap_or(host);
291 if host.contains('/') {
292 bail!("must not include a path");
293 }
294
295 if let Some(domain) = host.strip_prefix("*.") {
296 if domain.contains('*') {
297 bail!("wildcards are allowed only as prefixes");
298 }
299 let domain = Host::parse(domain)
300 .with_context(|| format!("invalid wildcard host \"*.{domain}\""))?;
301 let Host::Domain(domain) = domain else {
302 bail!("wildcard suffix must be a domain");
303 };
304 return Ok(Self::AnySubdomain(format!(".{domain}")));
305 }
306
307 if host.contains('*') {
308 bail!("wildcards are allowed only as subdomains");
309 }
310
311 Self::literal(host)
312 }
313
314 fn literal(host: &str) -> anyhow::Result<Self> {
316 Ok(Self::Literal(Host::parse(host)?))
317 }
318
319 fn allows(&self, host: &str) -> bool {
321 let host: Host = match Host::parse(host) {
322 Ok(host) => host,
323 Err(err) => {
324 tracing::warn!(?err, "invalid host in HostConfig::allows");
325 return false;
326 }
327 };
328 match (self, host) {
329 (HostConfig::Any, _) => true,
330 (HostConfig::AnySubdomain(suffix), Host::Domain(domain)) => domain.ends_with(suffix),
331 (HostConfig::Literal(literal), host) => host == *literal,
332 (HostConfig::Cidr(c), Host::Ipv4(ip)) => c.contains(ip),
333 (HostConfig::Cidr(c), Host::Ipv6(ip)) => c.contains(ip),
334 (HostConfig::AnySubdomain(_), _) => false,
336 (HostConfig::Cidr(_), Host::Domain(_)) => false,
338 (HostConfig::ToSelf, _) => false,
340 }
341 }
342
343 fn allows_relative(&self) -> bool {
345 matches!(self, Self::Any | Self::ToSelf)
346 }
347
348 fn is_for_service_chaining(&self) -> bool {
350 match self {
351 Self::Literal(Host::Domain(domain)) => domain.ends_with(SERVICE_CHAINING_DOMAIN_SUFFIX),
352 Self::AnySubdomain(suffix) => suffix == SERVICE_CHAINING_DOMAIN_SUFFIX,
353 _ => false,
354 }
355 }
356}
357
358#[derive(Debug, PartialEq, Eq, Clone)]
360pub enum PortConfig {
361 Any,
362 List(Vec<IndividualPortConfig>),
363}
364
365impl PortConfig {
366 fn parse(port: &str, scheme: &str) -> anyhow::Result<PortConfig> {
368 if port.is_empty() {
369 return well_known_port(scheme)
370 .map(|p| PortConfig::List(vec![IndividualPortConfig::Port(p)]))
371 .with_context(|| format!("no port was provided and the scheme {scheme:?} does not have a known default port number"));
372 }
373 if port == "*" {
374 return Ok(PortConfig::Any);
375 }
376
377 if port.starts_with('{') {
378 bail!("port lists are not yet supported")
380 }
381
382 let port = IndividualPortConfig::parse(port)?;
383
384 Ok(Self::List(vec![port]))
385 }
386
387 fn allows(&self, port: Option<u16>, scheme: &str) -> bool {
389 match self {
390 PortConfig::Any => true,
391 PortConfig::List(l) => {
392 let port = match port.or_else(|| well_known_port(scheme)) {
393 Some(p) => p,
394 None => return false,
395 };
396 l.iter().any(|p| p.allows(port))
397 }
398 }
399 }
400}
401
402#[derive(Debug, PartialEq, Eq, Clone)]
404pub enum IndividualPortConfig {
405 Port(u16),
406 Range(Range<u16>),
407}
408
409impl IndividualPortConfig {
410 fn parse(port: &str) -> anyhow::Result<Self> {
412 if let Some((start, end)) = port.split_once("..") {
413 let start = start
414 .parse()
415 .with_context(|| format!("port range {port:?} contains non-number"))?;
416 let end = end
417 .parse()
418 .with_context(|| format!("port range {port:?} contains non-number"))?;
419 return Ok(Self::Range(start..end));
420 }
421 Ok(Self::Port(port.parse().with_context(|| {
422 format!("port {port:?} is not a number")
423 })?))
424 }
425
426 fn allows(&self, port: u16) -> bool {
428 match self {
429 IndividualPortConfig::Port(p) => p == &port,
430 IndividualPortConfig::Range(r) => r.contains(&port),
431 }
432 }
433}
434
435fn well_known_port(scheme: &str) -> Option<u16> {
437 match scheme {
438 "postgres" => Some(5432),
439 "mysql" => Some(3306),
440 "redis" => Some(6379),
441 "mqtt" => Some(1883),
442 "http" => Some(80),
443 "https" => Some(443),
444 _ => None,
445 }
446}
447
448enum PartialAllowedHostConfig {
451 Exact(AllowedHostConfig),
452 Unresolved(spin_expressions::Template),
453}
454
455impl PartialAllowedHostConfig {
456 fn resolve(self, resolver: &impl SyncResolver) -> anyhow::Result<Option<AllowedHostConfig>> {
458 match self {
459 Self::Exact(h) => Ok(Some(h)),
460 Self::Unresolved(t) => {
461 let resolved = resolver.resolve_template(&t)?;
462 Self::parse_or_skip(&resolved)
463 }
464 }
465 }
466
467 fn validate(&self, resolver: &impl SyncResolver) -> anyhow::Result<()> {
470 if let Self::Unresolved(template) = self {
471 let Ok(resolved) = resolver.resolve_template(template) else {
472 return Ok(());
474 };
475
476 Self::parse_or_skip(&resolved).with_context(|| {
477 let template_str = template.to_string();
478 format!("using default variable value(s) with template {template_str:?} results in invalid config {resolved:?}")
479 })?;
480 }
481 Ok(())
482 }
483
484 fn parse_or_skip(resolved: &str) -> anyhow::Result<Option<AllowedHostConfig>> {
489 if resolved.is_empty() || resolved == "http://" || resolved == "https://" {
494 Ok(None)
495 } else {
496 AllowedHostConfig::parse(resolved).map(Some)
497 }
498 }
499}
500
501#[derive(PartialEq, Eq, Debug, Clone)]
503pub enum AllowedHostsConfig {
504 All,
505 SpecificHosts(Vec<AllowedHostConfig>),
506}
507
508impl AllowedHostsConfig {
509 pub fn parse<S: AsRef<str>>(
512 hosts: &[S],
513 resolver: &impl SyncResolver,
514 component_ids: &[String],
515 ) -> anyhow::Result<AllowedHostsConfig> {
516 let partial = Self::parse_partial(hosts)?;
517 let allowed = partial
518 .into_iter()
519 .flat_map(|p| p.resolve(resolver).transpose())
520 .collect::<anyhow::Result<Vec<_>>>()?;
521 let allowed = Self::expand_wildcard_service_chaining(allowed, component_ids);
522 Ok(Self::SpecificHosts(allowed))
523 }
524
525 pub fn validate<S: AsRef<str>>(
527 hosts: &[S],
528 resolver: &impl SyncResolver,
529 ) -> anyhow::Result<()> {
530 for partial in Self::parse_partial(hosts)? {
531 partial.validate(resolver)?;
532 }
533 Ok(())
534 }
535
536 fn parse_partial<S: AsRef<str>>(hosts: &[S]) -> anyhow::Result<Vec<PartialAllowedHostConfig>> {
539 if hosts.len() == 1 && hosts[0].as_ref() == "insecure:allow-all" {
540 bail!(
541 "'insecure:allow-all' is not allowed - use '*://*:*' instead if you really want to allow all outbound traffic'"
542 )
543 }
544 let mut allowed = Vec::with_capacity(hosts.len());
545 for host in hosts {
546 let template = spin_expressions::Template::new(host.as_ref())?;
547 if template.is_literal() {
548 allowed.push(PartialAllowedHostConfig::Exact(AllowedHostConfig::parse(
549 host.as_ref(),
550 )?));
551 } else {
552 allowed.push(PartialAllowedHostConfig::Unresolved(template));
553 }
554 }
555 Ok(allowed)
556 }
557
558 fn expand_wildcard_service_chaining(
559 hosts: Vec<AllowedHostConfig>,
560 component_ids: &[String],
561 ) -> Vec<AllowedHostConfig> {
562 let expand_one = |host: AllowedHostConfig| match host.host() {
563 HostConfig::AnySubdomain(domain) if domain == SERVICE_CHAINING_DOMAIN_SUFFIX => {
564 let expanded_domains = component_ids
565 .iter()
566 .map(|c| format!("{c}{SERVICE_CHAINING_DOMAIN_SUFFIX}"));
567 let expanded_hosts = expanded_domains.map(|d| {
568 let mut hh = host.clone();
569 hh.host = HostConfig::Literal(url::Host::Domain(d));
570 hh
571 });
572 expanded_hosts.collect()
573 }
574 _ => vec![host],
575 };
576
577 hosts.into_iter().flat_map(expand_one).collect()
578 }
579
580 pub fn allows(&self, url: &OutboundUrl) -> bool {
582 match self {
583 AllowedHostsConfig::All => true,
584 AllowedHostsConfig::SpecificHosts(hosts) => hosts.iter().any(|h| h.allows(url)),
585 }
586 }
587
588 pub fn allows_relative_url(&self, schemes: &[&str]) -> bool {
591 match self {
592 AllowedHostsConfig::All => true,
593 AllowedHostsConfig::SpecificHosts(hosts) => {
594 hosts.iter().any(|h| h.allows_relative(schemes))
595 }
596 }
597 }
598}
599
600impl Default for AllowedHostsConfig {
601 fn default() -> Self {
602 Self::SpecificHosts(Vec::new())
603 }
604}
605
606#[derive(Debug, Clone)]
608pub struct OutboundUrl {
609 scheme: String,
610 host: String,
611 port: Option<u16>,
612 original: String,
613}
614
615impl OutboundUrl {
616 pub fn parse(url: impl Into<String>, scheme: &str) -> anyhow::Result<Self> {
620 let url = url.into();
621
622 let parsed = match url::Url::parse(&url) {
623 Ok(url) if url.has_host() => Ok(url),
624 first_try => {
625 let second_try: anyhow::Result<url::Url> = format!("{scheme}://{url}")
626 .as_str()
627 .try_into()
628 .context("could not convert into a url");
629 match (second_try, first_try.map_err(|e| e.into())) {
630 (Ok(u), _) => Ok(u),
631 (_, Err(e)) | (Err(e), _) => Err(e),
633 }
634 }
635 }?;
636
637 Ok(Self {
638 scheme: parsed.scheme().to_string(),
639 host: parsed
640 .host()
641 .with_context(|| format!("{url:?} does not have a host component"))?
642 .to_string(),
643 port: parsed.port(),
644 original: url,
645 })
646 }
647
648 pub fn scheme(&self) -> &str {
649 &self.scheme
650 }
651
652 pub fn authority(&self) -> String {
653 if let Some(port) = self.port {
654 format!("{}:{port}", self.host)
655 } else {
656 self.host.clone()
657 }
658 }
659}
660
661impl std::fmt::Display for OutboundUrl {
662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
663 f.write_str(&self.original)
664 }
665}
666
667pub fn is_service_chaining_host(host: &str) -> bool {
669 parse_service_chaining_host(host).is_some()
670}
671
672pub fn parse_service_chaining_target(url: &http::Uri) -> Option<String> {
674 let host = url.authority().map(|a| a.host().trim())?;
675 parse_service_chaining_host(host)
676}
677
678fn parse_service_chaining_host(host: &str) -> Option<String> {
679 let (host, _) = host.rsplit_once(':').unwrap_or((host, ""));
680
681 let (first, rest) = host.split_once('.')?;
682
683 if rest == SERVICE_CHAINING_DOMAIN {
684 Some(first.to_owned())
685 } else {
686 None
687 }
688}
689
690#[cfg(test)]
691mod test {
692 impl AllowedHostConfig {
693 fn new(scheme: SchemeConfig, host: HostConfig, port: PortConfig) -> Self {
694 Self {
695 scheme,
696 host,
697 port,
698 original: String::new(),
699 }
700 }
701 }
702
703 impl SchemeConfig {
704 fn new(scheme: &str) -> Self {
705 Self::List(vec![scheme.into()])
706 }
707 }
708
709 impl HostConfig {
710 fn subdomain(domain: &str) -> Self {
711 Self::AnySubdomain(format!(".{domain}"))
712 }
713 }
714
715 impl PortConfig {
716 fn new(port: u16) -> Self {
717 Self::List(vec![IndividualPortConfig::Port(port)])
718 }
719
720 fn range(port: Range<u16>) -> Self {
721 Self::List(vec![IndividualPortConfig::Range(port)])
722 }
723 }
724
725 #[derive(Default)]
726 struct DummyResolver {
727 variables: std::collections::HashMap<String, String>,
728 }
729
730 impl SyncResolver for DummyResolver {
731 fn resolve_variable(&self, key: &str) -> spin_expressions::Result<String> {
732 self.variables
733 .get(key)
734 .cloned()
735 .ok_or(spin_expressions::Error::InvalidName(key.to_string()))
736 }
737 }
738
739 fn dummy_resolver() -> impl SyncResolver {
740 DummyResolver::default()
741 }
742
743 fn populated_resolver(values: &[(&str, &str)]) -> impl SyncResolver {
744 let variables = values
745 .iter()
746 .map(|(k, v)| (k.to_string(), v.to_string()))
747 .collect();
748
749 DummyResolver { variables }
750 }
751
752 fn empty_values_resolver() -> impl SyncResolver {
753 populated_resolver(&[("one", ""), ("two", "")])
754 }
755
756 use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
757
758 use super::*;
759 use std::net::{Ipv4Addr, Ipv6Addr};
760
761 #[test]
762 fn outbound_url_handles_at_in_paths() {
763 let url = "https://example.com/file@0.1.0.json";
764 let url = OutboundUrl::parse(url, "https").expect("should have parsed url");
765 assert_eq!("example.com", url.host);
766
767 let url = "https://user:password@example.com/file@0.1.0.json";
768 let url = OutboundUrl::parse(url, "https").expect("should have parsed url");
769 assert_eq!("example.com", url.host);
770
771 let url = "https://user:pass%23word@example.com/file@0.1.0.json";
772 let url = OutboundUrl::parse(url, "https").expect("should have parsed url");
773 assert_eq!("example.com", url.host);
774
775 let url = "https://user:password@example.com";
776 let url = OutboundUrl::parse(url, "https").expect("should have parsed url");
777 assert_eq!("example.com", url.host);
778 }
779
780 #[test]
781 fn parse_url_handles_disguised_authority() {
782 let disguised = "https://evil.com#@allowed.com";
783 let url = OutboundUrl::parse(disguised, "https").expect("should have parsed disguised URL");
784 assert_eq!("evil.com", url.host);
785
786 let disguised = "https://evil.com?@allowed.com";
787 let url = OutboundUrl::parse(disguised, "https").expect("should have parsed disguised URL");
788 assert_eq!("evil.com", url.host);
789
790 let disguised = "https://evil.com#not@allowed.com";
791 let url = OutboundUrl::parse(disguised, "https").expect("should have parsed disguised URL");
792 assert_eq!("evil.com", url.host);
793
794 let disguised = "https://evil.com?not@allowed.com";
795 let url = OutboundUrl::parse(disguised, "https").expect("should have parsed disguised URL");
796 assert_eq!("evil.com", url.host);
797 }
798
799 #[test]
800 fn test_allowed_hosts_accepts_url_without_port() {
801 assert_eq!(
802 AllowedHostConfig::new(
803 SchemeConfig::new("http"),
804 HostConfig::literal("spin.fermyon.dev").unwrap(),
805 PortConfig::new(80)
806 ),
807 AllowedHostConfig::parse("http://spin.fermyon.dev").unwrap()
808 );
809
810 assert_eq!(
811 AllowedHostConfig::new(
812 SchemeConfig::new("http"),
813 HostConfig::literal("spin.fermyon.dev").unwrap(),
815 PortConfig::new(80)
816 ),
817 AllowedHostConfig::parse("http://spin.fermyon.dev/").unwrap()
818 );
819
820 assert_eq!(
821 AllowedHostConfig::new(
822 SchemeConfig::new("https"),
823 HostConfig::literal("spin.fermyon.dev").unwrap(),
824 PortConfig::new(443)
825 ),
826 AllowedHostConfig::parse("https://spin.fermyon.dev").unwrap()
827 );
828 }
829
830 #[test]
831 fn test_allowed_hosts_accepts_url_with_port() {
832 assert_eq!(
833 AllowedHostConfig::new(
834 SchemeConfig::new("http"),
835 HostConfig::literal("spin.fermyon.dev").unwrap(),
836 PortConfig::new(4444)
837 ),
838 AllowedHostConfig::parse("http://spin.fermyon.dev:4444").unwrap()
839 );
840 assert_eq!(
841 AllowedHostConfig::new(
842 SchemeConfig::new("http"),
843 HostConfig::literal("spin.fermyon.dev").unwrap(),
844 PortConfig::new(4444)
845 ),
846 AllowedHostConfig::parse("http://spin.fermyon.dev:4444/").unwrap()
847 );
848 assert_eq!(
849 AllowedHostConfig::new(
850 SchemeConfig::new("https"),
851 HostConfig::literal("spin.fermyon.dev").unwrap(),
852 PortConfig::new(5555)
853 ),
854 AllowedHostConfig::parse("https://spin.fermyon.dev:5555").unwrap()
855 );
856 }
857
858 #[test]
859 fn test_allowed_hosts_accepts_url_with_port_range() {
860 assert_eq!(
861 AllowedHostConfig::new(
862 SchemeConfig::new("http"),
863 HostConfig::literal("spin.fermyon.dev").unwrap(),
864 PortConfig::range(4444..5555)
865 ),
866 AllowedHostConfig::parse("http://spin.fermyon.dev:4444..5555").unwrap()
867 );
868 }
869
870 #[test]
871 fn test_allowed_hosts_does_not_accept_plain_host_without_port() {
872 assert!(AllowedHostConfig::parse("spin.fermyon.dev").is_err());
873 }
874
875 #[test]
876 fn test_allowed_hosts_does_not_accept_plain_host_without_scheme() {
877 assert!(AllowedHostConfig::parse("spin.fermyon.dev:80").is_err());
878 }
879
880 #[test]
881 fn test_allowed_hosts_accepts_host_with_glob_scheme() {
882 assert_eq!(
883 AllowedHostConfig::new(
884 SchemeConfig::Any,
885 HostConfig::literal("spin.fermyon.dev").unwrap(),
886 PortConfig::new(7777)
887 ),
888 AllowedHostConfig::parse("*://spin.fermyon.dev:7777").unwrap()
889 )
890 }
891
892 #[test]
893 fn test_allowed_hosts_accepts_self() {
894 assert_eq!(
895 AllowedHostConfig::new(
896 SchemeConfig::new("http"),
897 HostConfig::ToSelf,
898 PortConfig::new(80)
899 ),
900 AllowedHostConfig::parse("http://self").unwrap()
901 );
902 }
903
904 #[test]
905 fn test_allowed_hosts_accepts_localhost_addresses() {
906 assert!(AllowedHostConfig::parse("localhost").is_err());
907 assert_eq!(
908 AllowedHostConfig::new(
909 SchemeConfig::new("http"),
910 HostConfig::literal("localhost").unwrap(),
911 PortConfig::new(80)
912 ),
913 AllowedHostConfig::parse("http://localhost").unwrap()
914 );
915 assert!(AllowedHostConfig::parse("localhost:3001").is_err());
916 assert_eq!(
917 AllowedHostConfig::new(
918 SchemeConfig::new("http"),
919 HostConfig::literal("localhost").unwrap(),
920 PortConfig::new(3001)
921 ),
922 AllowedHostConfig::parse("http://localhost:3001").unwrap()
923 );
924 }
925
926 #[test]
927 fn test_allowed_hosts_accepts_subdomain_wildcards() {
928 assert_eq!(
929 AllowedHostConfig::new(
930 SchemeConfig::new("http"),
931 HostConfig::subdomain("example.com"),
932 PortConfig::new(80)
933 ),
934 AllowedHostConfig::parse("http://*.example.com").unwrap()
935 );
936 }
937
938 #[test]
939 fn test_allowed_hosts_accepts_ip_addresses() {
940 assert_eq!(
941 AllowedHostConfig::new(
942 SchemeConfig::new("http"),
943 HostConfig::literal("192.168.1.1").unwrap(),
944 PortConfig::new(80)
945 ),
946 AllowedHostConfig::parse("http://192.168.1.1").unwrap()
947 );
948 assert_eq!(
949 AllowedHostConfig::new(
950 SchemeConfig::new("http"),
951 HostConfig::literal("192.168.1.1").unwrap(),
952 PortConfig::new(3002)
953 ),
954 AllowedHostConfig::parse("http://192.168.1.1:3002").unwrap()
955 );
956 assert_eq!(
957 AllowedHostConfig::new(
958 SchemeConfig::new("http"),
959 HostConfig::literal("[::1]").unwrap(),
960 PortConfig::new(8001)
961 ),
962 AllowedHostConfig::parse("http://[::1]:8001").unwrap()
963 );
964
965 assert_eq!(
966 AllowedHostConfig::new(
967 SchemeConfig::new("http"),
968 HostConfig::literal("[::1]").unwrap(),
969 PortConfig::new(80)
970 ),
971 AllowedHostConfig::parse("http://[::1]").unwrap()
972 );
973 }
974
975 #[test]
976 fn test_allowed_hosts_accepts_ip_cidr() {
977 assert_eq!(
978 AllowedHostConfig::new(
979 SchemeConfig::Any,
980 HostConfig::Cidr(IpNetwork::V4(
981 Ipv4Network::new(Ipv4Addr::new(127, 0, 0, 0), 24).unwrap()
982 )),
983 PortConfig::new(80)
984 ),
985 AllowedHostConfig::parse("*://127.0.0.0/24:80").unwrap()
986 );
987 assert!(AllowedHostConfig::parse("*://127.0.0.0/24").is_err());
988 assert_eq!(
989 AllowedHostConfig::new(
990 SchemeConfig::Any,
991 HostConfig::Cidr(IpNetwork::V6(
992 Ipv6Network::new(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 8).unwrap()
993 )),
994 PortConfig::new(80)
995 ),
996 AllowedHostConfig::parse("*://ff00::/8:80").unwrap()
997 );
998 }
999
1000 #[test]
1001 fn test_allowed_hosts_rejects_path() {
1002 assert!(AllowedHostConfig::parse("http://spin.fermyon.dev/").is_ok());
1004 assert!(AllowedHostConfig::parse("http://spin.fermyon.dev/a").is_err());
1006 assert!(AllowedHostConfig::parse("http://spin.fermyon.dev:6666/a/b").is_err());
1007 assert!(AllowedHostConfig::parse("http://*.fermyon.dev/a").is_err());
1008 }
1009
1010 #[test]
1011 fn test_allowed_hosts_respects_allow_all() {
1012 assert!(
1013 AllowedHostsConfig::parse(&["insecure:allow-all"], &dummy_resolver(), &[]).is_err()
1014 );
1015 assert!(
1016 AllowedHostsConfig::parse(
1017 &["spin.fermyon.dev", "insecure:allow-all"],
1018 &dummy_resolver(),
1019 &[]
1020 )
1021 .is_err()
1022 );
1023 }
1024
1025 #[test]
1026 fn test_allowed_all_globs() {
1027 assert_eq!(
1028 AllowedHostConfig::new(SchemeConfig::Any, HostConfig::Any, PortConfig::Any),
1029 AllowedHostConfig::parse("*://*:*").unwrap()
1030 );
1031 }
1032
1033 #[test]
1034 fn test_missing_scheme() {
1035 assert!(AllowedHostConfig::parse("example.com").is_err());
1036 }
1037
1038 #[test]
1039 fn test_allowed_hosts_can_be_specific() {
1040 let allowed = AllowedHostsConfig::parse(
1041 &["*://spin.fermyon.dev:443", "http://example.com:8383"],
1042 &dummy_resolver(),
1043 &[],
1044 )
1045 .unwrap();
1046 assert!(
1047 allowed.allows(&OutboundUrl::parse("http://example.com:8383/foo/bar", "http").unwrap())
1048 );
1049 assert!(allowed.allows(&OutboundUrl::parse("https://spin.fermyon.dev", "https").unwrap()));
1051 assert!(allowed.allows(&OutboundUrl::parse("https://spin.fermyon.dev/", "https").unwrap()));
1052 assert!(!allowed.allows(&OutboundUrl::parse("http://example.com/", "http").unwrap()));
1053 assert!(!allowed.allows(&OutboundUrl::parse("http://google.com/", "http").unwrap()));
1054 assert!(allowed.allows(&OutboundUrl::parse("spin.fermyon.dev:443", "https").unwrap()));
1055 assert!(allowed.allows(&OutboundUrl::parse("example.com:8383", "http").unwrap()));
1056 }
1057
1058 #[test]
1059 fn test_allowed_hosts_with_trailing_slash() {
1060 let allowed =
1061 AllowedHostsConfig::parse(&["https://my.api.com/"], &dummy_resolver(), &[]).unwrap();
1062 assert!(allowed.allows(&OutboundUrl::parse("https://my.api.com", "https").unwrap()));
1063 assert!(allowed.allows(&OutboundUrl::parse("https://my.api.com/", "https").unwrap()));
1064 }
1065
1066 #[test]
1067 fn test_allowed_hosts_can_be_subdomain_wildcards() {
1068 let allowed = AllowedHostsConfig::parse(
1069 &["http://*.example.com", "http://*.example2.com:8383"],
1070 &dummy_resolver(),
1071 &[],
1072 )
1073 .unwrap();
1074 assert!(
1075 allowed.allows(&OutboundUrl::parse("http://a.example.com/foo/bar", "http").unwrap())
1076 );
1077 assert!(
1078 allowed.allows(&OutboundUrl::parse("http://a.b.example.com/foo/bar", "http").unwrap())
1079 );
1080 assert!(
1081 allowed.allows(
1082 &OutboundUrl::parse("http://a.b.example2.com:8383/foo/bar", "http").unwrap()
1083 )
1084 );
1085 assert!(
1086 !allowed
1087 .allows(&OutboundUrl::parse("http://a.b.example2.com/foo/bar", "http").unwrap())
1088 );
1089 assert!(
1090 !allowed.allows(&OutboundUrl::parse("http://example.com/foo/bar", "http").unwrap())
1091 );
1092 assert!(
1093 !allowed
1094 .allows(&OutboundUrl::parse("http://example.com:8383/foo/bar", "http").unwrap())
1095 );
1096 assert!(
1097 !allowed.allows(&OutboundUrl::parse("http://myexample.com/foo/bar", "http").unwrap())
1098 );
1099 }
1100
1101 #[test]
1102 fn test_encoded_chars_in_db_password() {
1103 let allowed =
1104 AllowedHostsConfig::parse(&["mysql://xyz.com"], &dummy_resolver(), &[]).unwrap();
1105 assert!(
1106 allowed.allows(
1107 &OutboundUrl::parse("mysql://user%3Apass%23word@xyz.com", "mysql").unwrap()
1108 )
1109 );
1110 assert!(
1111 allowed.allows(&OutboundUrl::parse("user%3Apass%23word@xyz.com", "mysql").unwrap())
1112 );
1113 }
1114
1115 #[test]
1116 fn test_cidr() {
1117 let allowed =
1118 AllowedHostsConfig::parse(&["*://127.0.0.1/24:63551"], &dummy_resolver(), &[]).unwrap();
1119 assert!(allowed.allows(&OutboundUrl::parse("tcp://127.0.0.1:63551", "tcp").unwrap()));
1120 }
1121
1122 fn exact_host(ahc: &AllowedHostConfig) -> String {
1123 match ahc.host() {
1124 HostConfig::Literal(host) => host.to_string(),
1125 _ => panic!("expected host {:?} to be a literal", ahc.host()),
1126 }
1127 }
1128
1129 #[test]
1130 fn expand_wildcard_service_chaining_lists_all_components() {
1131 let component_ids = ["first", "second", "third"]
1132 .iter()
1133 .map(|s| s.to_string())
1134 .collect::<Vec<_>>();
1135 let allowed = AllowedHostsConfig::parse(
1136 &["http://*.spin.internal"],
1137 &dummy_resolver(),
1138 &component_ids,
1139 )
1140 .unwrap();
1141 let AllowedHostsConfig::SpecificHosts(allowed) = allowed else {
1142 panic!("expanded AllowedHostsConfig should be specific hosts");
1143 };
1144
1145 assert_eq!(3, allowed.len());
1146
1147 assert_eq!("first.spin.internal", exact_host(&allowed[0]));
1148 assert_eq!("second.spin.internal", exact_host(&allowed[1]));
1149 assert_eq!("third.spin.internal", exact_host(&allowed[2]));
1150 }
1151
1152 #[test]
1153 fn expand_wildcard_service_chaining_leaves_others_untouched() {
1154 let component_ids = ["first", "second", "third"]
1155 .iter()
1156 .map(|s| s.to_string())
1157 .collect::<Vec<_>>();
1158 let allowed = AllowedHostsConfig::parse(
1159 &[
1160 "pg://localhost:5656",
1161 "http://*.spin.internal",
1162 "https://spinframework.dev",
1163 ],
1164 &dummy_resolver(),
1165 &component_ids,
1166 )
1167 .unwrap();
1168 let AllowedHostsConfig::SpecificHosts(allowed) = allowed else {
1169 panic!("expanded AllowedHostsConfig should be specific hosts");
1170 };
1171
1172 assert_eq!(5, allowed.len());
1173
1174 assert_eq!("localhost", exact_host(&allowed[0]));
1175 assert_eq!("first.spin.internal", exact_host(&allowed[1]));
1176 assert_eq!("second.spin.internal", exact_host(&allowed[2]));
1177 assert_eq!("third.spin.internal", exact_host(&allowed[3]));
1178 assert_eq!("spinframework.dev", exact_host(&allowed[4]));
1179 }
1180
1181 #[test]
1182 fn allowed_hosts_ignores_empty_due_to_empty_variables() {
1183 let resolver = empty_values_resolver();
1184 let hosts = &["https://{{ one }}", "{{ two }}", "https://three"];
1185
1186 let AllowedHostsConfig::SpecificHosts(allowed) =
1187 AllowedHostsConfig::parse(hosts, &resolver, &[]).expect("parse should have succeeded")
1188 else {
1189 panic!("expanded AllowedHostsConfig should be specific hosts");
1190 };
1191
1192 assert_eq!(1, allowed.len());
1193 assert_eq!("three", exact_host(&allowed[0]));
1194 }
1195
1196 #[test]
1197 fn valid_hosts_are_valid() {
1198 let resolver = dummy_resolver();
1199 let hosts = &["http://x.y", "*://my.db:*"];
1200 AllowedHostsConfig::validate(hosts, &resolver).expect("valid hosts should be valid");
1201 }
1202
1203 #[test]
1204 fn invalid_hosts_are_invalid() {
1205 let resolver = dummy_resolver();
1206 let hosts = &["http://x.y", "zootle! wurdl!", "}{ !!**"];
1207 AllowedHostsConfig::validate(hosts, &resolver)
1208 .expect_err("invalid hosts should be invalid");
1209 }
1210
1211 #[test]
1212 fn variables_make_hosts_valid() {
1213 let resolver = populated_resolver(&[("dbhost", "example.com"), ("dbport", "1234")]);
1214 let hosts = &["http://{{ dbhost }}", "http://{{ dbhost }}:{{ dbport }}"];
1215 AllowedHostsConfig::validate(hosts, &resolver).expect("happy variables make hosts valid");
1216 }
1217
1218 #[test]
1219 fn bad_variables_make_hosts_invalid() {
1220 let resolver = populated_resolver(&[("dbhost", "zoinks, Scooby!")]);
1221 let hosts = &["http://{{ dbhost }}"];
1222 AllowedHostsConfig::validate(hosts, &resolver)
1223 .expect_err("bad variables make hosts invalid");
1224 }
1225
1226 #[test]
1227 fn missing_variables_ignored_when_checking_validity() {
1228 let resolver = dummy_resolver();
1229 let hosts = &["http://{{ dbhost }}"];
1230 AllowedHostsConfig::validate(hosts, &resolver)
1231 .expect("missing variables errors should be deferred");
1232 }
1233
1234 #[test]
1235 fn empty_resolutions_ignored_when_checking_validity() {
1236 let resolver = empty_values_resolver();
1237 let hosts = &["https://{{ one }}", "{{ two }}", "https://three"];
1238 AllowedHostsConfig::validate(hosts, &resolver)
1239 .expect("empty resolutions should ignored as valid");
1240 }
1241
1242 #[test]
1243 fn ipv6_literal_without_port_uses_scheme_default() {
1244 for (input, host, port) in [
1245 ("http://[::1]", "[::1]", 80),
1246 ("https://[::1]", "[::1]", 443),
1247 ("http://[2001:db8::1]", "[2001:db8::1]", 80),
1248 ] {
1249 let scheme = input.split_once("://").unwrap().0;
1250 assert_eq!(
1251 AllowedHostConfig::new(
1252 SchemeConfig::new(scheme),
1253 HostConfig::literal(host).unwrap(),
1254 PortConfig::new(port),
1255 ),
1256 AllowedHostConfig::parse(input).unwrap_or_else(|e| panic!("{input}: {e:?}")),
1257 "{input}"
1258 );
1259 }
1260 }
1261
1262 #[test]
1263 fn ipv6_literal_with_explicit_port_still_parses() {
1264 assert_eq!(
1265 AllowedHostConfig::new(
1266 SchemeConfig::new("http"),
1267 HostConfig::literal("[2001:db8::1]").unwrap(),
1268 PortConfig::new(443),
1269 ),
1270 AllowedHostConfig::parse("http://[2001:db8::1]:443").unwrap()
1271 );
1272 assert_eq!(
1273 AllowedHostConfig::new(
1274 SchemeConfig::new("http"),
1275 HostConfig::literal("[::1]").unwrap(),
1276 PortConfig::Any,
1277 ),
1278 AllowedHostConfig::parse("http://[::1]:*").unwrap()
1279 );
1280 assert_eq!(
1281 AllowedHostConfig::new(
1282 SchemeConfig::new("http"),
1283 HostConfig::literal("[::1]").unwrap(),
1284 PortConfig::range(8000..9000),
1285 ),
1286 AllowedHostConfig::parse("http://[::1]:8000..9000").unwrap()
1287 );
1288 assert_eq!(
1289 AllowedHostConfig::new(
1290 SchemeConfig::Any,
1291 HostConfig::literal("[::1]").unwrap(),
1292 PortConfig::new(80),
1293 ),
1294 AllowedHostConfig::parse("*://[::1]:80").unwrap()
1295 );
1296 assert_eq!(
1297 AllowedHostConfig::new(
1298 SchemeConfig::new("http"),
1299 HostConfig::literal("[::1]").unwrap(),
1300 PortConfig::new(8080),
1301 ),
1302 AllowedHostConfig::parse("http://[::1]:8080/").unwrap()
1303 );
1304 }
1305
1306 #[test]
1307 fn cidr_without_port_uses_scheme_default() {
1308 for (input, cidr, port) in [
1309 ("http://ff00::/8", "ff00::/8", 80),
1310 ("https://2001:db8::/32", "2001:db8::/32", 443),
1311 ("http://::1/128", "::1/128", 80),
1312 ("http://10.0.0.0/8", "10.0.0.0/8", 80),
1313 ] {
1314 let scheme = input.split_once("://").unwrap().0;
1315 assert_eq!(
1316 AllowedHostConfig::new(
1317 SchemeConfig::new(scheme),
1318 HostConfig::Cidr(IpNetwork::from_str_truncate(cidr).unwrap()),
1319 PortConfig::new(port),
1320 ),
1321 AllowedHostConfig::parse(input).unwrap_or_else(|e| panic!("{input}: {e:?}")),
1322 "{input}"
1323 );
1324 }
1325 }
1326
1327 #[test]
1328 fn ipv6_cidr_with_explicit_port_still_parses() {
1329 assert_eq!(
1330 AllowedHostConfig::new(
1331 SchemeConfig::new("http"),
1332 HostConfig::Cidr(IpNetwork::from_str_truncate("2001:db8::/32").unwrap()),
1333 PortConfig::new(443),
1334 ),
1335 AllowedHostConfig::parse("http://2001:db8::/32:443").unwrap()
1336 );
1337 }
1338
1339 #[test]
1340 fn rejects_malformed_ipv6_allowed_hosts() {
1341 for bad in [
1342 "http://[::1]/foo",
1343 "http://ff00::/8/extra",
1344 "http://[::1",
1345 "http://[::1]junk",
1346 "http://[::1]::80",
1347 "http://[v1.]",
1348 "[::1]",
1349 "[::1]:8080",
1350 ] {
1351 assert!(
1352 AllowedHostConfig::parse(bad).is_err(),
1353 "expected {bad:?} to be rejected"
1354 );
1355 }
1356 }
1357
1358 #[test]
1359 fn ipv6_literal_allows_and_denies_requests() {
1360 let allowed = AllowedHostsConfig::parse(&["http://[::1]"], &dummy_resolver(), &[]).unwrap();
1361 assert!(allowed.allows(&OutboundUrl::parse("http://[::1]", "http").unwrap()));
1362 assert!(allowed.allows(&OutboundUrl::parse("http://[::1]/some/path", "http").unwrap()));
1363 assert!(allowed.allows(&OutboundUrl::parse("http://[::1]:80", "http").unwrap()));
1364 assert!(!allowed.allows(&OutboundUrl::parse("http://[::2]", "http").unwrap()));
1365 assert!(!allowed.allows(&OutboundUrl::parse("http://[::1]:81", "http").unwrap()));
1366 }
1367
1368 #[test]
1369 fn ipv6_cidr_allows_addresses_in_range() {
1370 let allowed =
1371 AllowedHostsConfig::parse(&["http://2001:db8::/32"], &dummy_resolver(), &[]).unwrap();
1372 assert!(allowed.allows(&OutboundUrl::parse("http://[2001:db8::1]", "http").unwrap()));
1373 assert!(
1374 allowed.allows(&OutboundUrl::parse("http://[2001:db8:abcd::1]/x", "http").unwrap())
1375 );
1376 assert!(!allowed.allows(&OutboundUrl::parse("http://[2001:db9::1]", "http").unwrap()));
1377 }
1378
1379 #[test]
1380 fn wildcard_subdomain_is_case_insensitive() {
1381 let allowed =
1382 AllowedHostsConfig::parse(&["http://*.Example.COM"], &dummy_resolver(), &[]).unwrap();
1383 assert!(allowed.allows(&OutboundUrl::parse("http://foo.example.com", "http").unwrap()));
1384 assert!(allowed.allows(&OutboundUrl::parse("http://a.b.example.com/x", "http").unwrap()));
1385 assert!(!allowed.allows(&OutboundUrl::parse("http://example.com", "http").unwrap()));
1386 }
1387
1388 #[test]
1389 fn rejects_invalid_allowed_host_entries() {
1390 for bad in [
1391 "http://example.com:notaport",
1392 "http://example.com:8080..70000",
1393 "http://self:notaport",
1394 "http://self.alt:notaport",
1395 "http://*:notaport",
1396 "http://127.0.0.1:notaport",
1397 "http://[::1]:notaport",
1398 "http://*.example.com:notaport",
1399 "http://*.example.com:8080..",
1400 "http://*.example.com:8O8O",
1401 "http://example.com:8080/p",
1402 "http://*.example.com:8080/p",
1403 "http://*.example.com/p",
1404 "http://127.0.0.0/24/p",
1405 "http://[::1junk",
1406 "http://*.",
1407 "http://*.:8080",
1408 "example.com:8080",
1409 "http://example%.com",
1410 "http://example%2.com",
1411 "http://example%GG.com",
1412 "http://exa[mple.com",
1413 r"http://exa\mple.com",
1414 "http://example.com:80:90",
1415 "http://[vG.example]",
1416 ] {
1417 assert!(
1418 AllowedHostConfig::parse(bad).is_err(),
1419 "expected {bad:?} to be rejected"
1420 );
1421 }
1422 }
1423
1424 #[test]
1425 fn invalid_explicit_port_reports_port_error() {
1426 for bad in [
1427 "http://example.com:99999",
1428 "http://[::1]:99999",
1429 "http://ff00::/8:99999",
1430 "http://*.example.com:99999",
1431 ] {
1432 let err = AllowedHostConfig::parse(bad).unwrap_err();
1433 assert_eq!(err.to_string(), "Invalid allowed host port \"99999\"");
1434 }
1435 }
1436
1437 #[test]
1438 fn rejects_double_slash_paths() {
1439 for bad in [
1440 "http://example.com//",
1441 "http://*.example.com//",
1442 "http://self//",
1443 "http://[::1]//",
1444 ] {
1445 assert!(
1446 AllowedHostConfig::parse(bad).is_err(),
1447 "expected {bad:?} to be rejected"
1448 );
1449 }
1450 }
1451
1452 #[test]
1453 fn rejects_wildcard_non_domain_suffixes() {
1454 for bad in [
1455 "http://*.127.0.0.1",
1456 "http://*.192.168.001.001",
1457 "http://*.[::1]",
1458 "http://*.[2001:db8::1]",
1459 ] {
1460 assert!(
1461 AllowedHostConfig::parse(bad).is_err(),
1462 "expected {bad:?} to be rejected"
1463 );
1464 }
1465 }
1466}