use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
const INTERNAL_IPV4_RANGES: &[(Ipv4Addr, u32)] = &[
(Ipv4Addr::new(10, 0, 0, 0), 8),
(Ipv4Addr::new(172, 16, 0, 0), 12),
(Ipv4Addr::new(192, 168, 0, 0), 16),
(Ipv4Addr::new(127, 0, 0, 0), 8),
];
pub fn is_insecure_uri(uri: &str) -> bool {
uri.starts_with("git://") || uri.starts_with("http://")
}
pub fn is_internal_source(uri: &str) -> bool {
match extract_host(uri) {
Some(h) => is_internal_host(&h),
None => false,
}
}
fn ipv4_in_cidr(addr: Ipv4Addr, network: Ipv4Addr, prefix_len: u32) -> bool {
let addr_bits = u32::from(addr);
let net_bits = u32::from(network);
let mask = if prefix_len == 0 {
0
} else {
!0u32 << (32 - prefix_len)
};
(addr_bits & mask) == (net_bits & mask)
}
fn is_internal_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => INTERNAL_IPV4_RANGES
.iter()
.any(|(net, prefix)| ipv4_in_cidr(v4, *net, *prefix)),
IpAddr::V6(v6) => {
v6 == Ipv6Addr::LOCALHOST
|| (v6.octets()[0] & 0xfe) == 0xfc
}
}
}
fn extract_host(uri: &str) -> Option<String> {
let after_scheme = uri.split("://").nth(1)?;
let host_port = after_scheme.split('/').next()?;
let host = host_port.split(':').next()?;
let host = if let Some(at_pos) = host.rfind('@') {
&host[at_pos + 1..]
} else {
host
};
if host.is_empty() {
None
} else {
Some(host.to_string())
}
}
fn is_internal_host(host: &str) -> bool {
if let Ok(ip) = host.parse::<IpAddr>() {
return is_internal_ip(ip);
}
let sock_addr = format!("{}:0", host);
match sock_addr.to_socket_addrs() {
Ok(addrs) => {
let addrs: Vec<_> = addrs.collect();
!addrs.is_empty() && addrs.iter().all(|a| is_internal_ip(a.ip()))
}
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn git_protocol_is_insecure() {
assert!(is_insecure_uri("git://github.com/foo/bar.git"));
}
#[test]
fn http_is_insecure() {
assert!(is_insecure_uri("http://rubygems.org/"));
}
#[test]
fn https_is_secure() {
assert!(!is_insecure_uri("https://rubygems.org/"));
}
#[test]
fn ssh_is_secure() {
assert!(!is_insecure_uri("git@github.com:foo/bar.git"));
}
#[test]
fn extract_host_from_git_uri() {
assert_eq!(
extract_host("git://github.com/rails/jquery-rails.git"),
Some("github.com".to_string())
);
}
#[test]
fn extract_host_from_http_uri() {
assert_eq!(
extract_host("http://rubygems.org/"),
Some("rubygems.org".to_string())
);
}
#[test]
fn extract_host_with_port() {
assert_eq!(
extract_host("http://gems.example.com:8080/"),
Some("gems.example.com".to_string())
);
}
#[test]
fn extract_host_with_user() {
assert_eq!(
extract_host("http://user@gems.example.com/"),
Some("gems.example.com".to_string())
);
}
#[test]
fn localhost_is_internal() {
assert!(is_internal_ip("127.0.0.1".parse().unwrap()));
assert!(is_internal_ip("127.0.0.42".parse().unwrap()));
}
#[test]
fn rfc1918_10_is_internal() {
assert!(is_internal_ip("10.0.0.1".parse().unwrap()));
assert!(is_internal_ip("10.255.255.255".parse().unwrap()));
}
#[test]
fn rfc1918_172_is_internal() {
assert!(is_internal_ip("172.16.0.1".parse().unwrap()));
assert!(is_internal_ip("172.31.255.255".parse().unwrap()));
}
#[test]
fn rfc1918_192_is_internal() {
assert!(is_internal_ip("192.168.0.1".parse().unwrap()));
assert!(is_internal_ip("192.168.255.255".parse().unwrap()));
}
#[test]
fn public_ip_is_not_internal() {
assert!(!is_internal_ip("8.8.8.8".parse().unwrap()));
assert!(!is_internal_ip("1.1.1.1".parse().unwrap()));
}
#[test]
fn ipv6_loopback_is_internal() {
assert!(is_internal_ip("::1".parse().unwrap()));
}
#[test]
fn ipv6_unique_local_is_internal() {
assert!(is_internal_ip("fc00::1".parse().unwrap()));
assert!(is_internal_ip("fd12:3456:789a::1".parse().unwrap()));
}
#[test]
fn internal_http_source() {
assert!(is_internal_source("http://192.168.1.1/gems/"));
assert!(is_internal_source("http://10.0.0.1:8080/"));
assert!(is_internal_source("http://127.0.0.1/"));
}
#[test]
fn external_http_source() {
assert!(!is_internal_source("http://rubygems.org/"));
}
#[test]
fn localhost_name_is_internal() {
assert!(is_internal_source("http://localhost/"));
}
#[test]
fn extract_host_no_scheme() {
assert_eq!(extract_host("not-a-url"), None);
}
#[test]
fn extract_host_empty_host() {
assert_eq!(extract_host("http:///path"), None);
}
#[test]
fn ipv4_in_cidr_prefix_zero_matches_any() {
assert!(ipv4_in_cidr(
Ipv4Addr::new(8, 8, 8, 8),
Ipv4Addr::new(0, 0, 0, 0),
0
));
assert!(ipv4_in_cidr(
Ipv4Addr::new(192, 168, 1, 1),
Ipv4Addr::new(0, 0, 0, 0),
0
));
}
#[test]
fn ipv4_in_cidr_prefix_32_exact_match() {
assert!(ipv4_in_cidr(
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(10, 0, 0, 1),
32
));
assert!(!ipv4_in_cidr(
Ipv4Addr::new(10, 0, 0, 2),
Ipv4Addr::new(10, 0, 0, 1),
32
));
}
}