swink_agent_eval/
url_filter.rs1use std::net::IpAddr;
4
5use url::{Host, Url};
6
7pub trait UrlFilter: Send + Sync {
9 fn allows(&self, url: &Url) -> bool;
11}
12
13#[derive(Debug, Clone, Copy, Default)]
19pub struct DefaultUrlFilter;
20
21impl UrlFilter for DefaultUrlFilter {
22 fn allows(&self, url: &Url) -> bool {
23 let Some(host) = url.host() else {
24 return false;
25 };
26
27 match host {
28 Host::Ipv4(address) => is_public_ip(IpAddr::V4(address)),
29 Host::Ipv6(address) => is_public_ip(IpAddr::V6(address)),
30 Host::Domain(host) => !is_blocked_hostname(host),
31 }
32 }
33}
34
35fn is_public_ip(address: IpAddr) -> bool {
36 match address {
37 IpAddr::V4(address) => {
38 !(address.is_loopback()
39 || address.is_private()
40 || address.is_link_local()
41 || address.is_broadcast()
42 || address.is_unspecified()
43 || is_azure_metadata_ipv4(address))
44 }
45 IpAddr::V6(address) => {
46 !(address.is_loopback()
47 || address.is_unspecified()
48 || address.is_unique_local()
49 || address.is_unicast_link_local())
50 }
51 }
52}
53
54fn is_blocked_hostname(host: &str) -> bool {
55 let host = host.trim_end_matches('.').to_ascii_lowercase();
56
57 host == "localhost"
58 || host.ends_with(".localhost")
59 || matches!(
60 host.as_str(),
61 "metadata.google.internal" | "instance-data.ec2.internal"
62 )
63}
64
65fn is_azure_metadata_ipv4(address: std::net::Ipv4Addr) -> bool {
66 address.octets() == [169, 254, 169, 254]
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn allows_public_hostnames() {
75 let filter = DefaultUrlFilter;
76 assert!(filter.allows(&Url::parse("https://example.com/image.png").unwrap()));
77 assert!(filter.allows(&Url::parse("http://example.com/image.png").unwrap()));
78 }
79
80 #[test]
81 fn blocks_loopback_and_private_ip_literals() {
82 let filter = DefaultUrlFilter;
83
84 assert!(!filter.allows(&Url::parse("https://127.0.0.1/test.png").unwrap()));
85 assert!(!filter.allows(&Url::parse("https://10.0.0.5/test.png").unwrap()));
86 assert!(!filter.allows(&Url::parse("https://192.168.1.20/test.png").unwrap()));
87 assert!(!filter.allows(&Url::parse("https://[::1]/test.png").unwrap()));
88 }
89
90 #[test]
91 fn blocks_known_metadata_hosts() {
92 let filter = DefaultUrlFilter;
93
94 assert!(!filter.allows(&Url::parse("https://169.254.169.254/latest/meta-data").unwrap()));
95 assert!(
96 !filter.allows(
97 &Url::parse("https://metadata.google.internal/computeMetadata/v1").unwrap()
98 )
99 );
100 assert!(
101 !filter.allows(
102 &Url::parse("https://instance-data.ec2.internal/latest/meta-data").unwrap()
103 )
104 );
105 assert!(!filter.allows(&Url::parse("https://localhost/test.png").unwrap()));
106 }
107}