use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::BoxFuture;
use crate::http::{FetchError, FetchInit, Fetcher, Headers, Response, Url};
use crate::re::{js_re, test};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrivateAddressError(pub String);
impl std::fmt::Display for PrivateAddressError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} is not a public address", self.0)
}
}
impl std::error::Error for PrivateAddressError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PublicFetchError {
Private(PrivateAddressError),
InvalidUrl(String),
Fetch(FetchError),
}
impl std::fmt::Display for PublicFetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PublicFetchError::Private(e) => e.fmt(f),
PublicFetchError::InvalidUrl(_) => f.write_str("Invalid URL"),
PublicFetchError::Fetch(e) => e.fmt(f),
}
}
}
impl std::error::Error for PublicFetchError {}
impl From<PrivateAddressError> for PublicFetchError {
fn from(e: PrivateAddressError) -> Self {
PublicFetchError::Private(e)
}
}
impl From<FetchError> for PublicFetchError {
fn from(e: FetchError) -> Self {
PublicFetchError::Fetch(e)
}
}
pub type Lookup = Arc<dyn Fn(&str) -> BoxFuture<'static, Vec<String>> + Send + Sync>;
fn v4(text: &str) -> Option<[u32; 4]> {
let parts: Vec<&str> = text.split('.').collect();
if parts.len() != 4 {
return None;
}
let mut out = [0u32; 4];
for (i, p) in parts.iter().enumerate() {
if p.is_empty() || p.len() > 3 || !p.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let n: u32 = p.parse().ok()?;
if n > 255 {
return None;
}
out[i] = n;
}
Some(out)
}
fn public_v4([a, b, c, _]: [u32; 4]) -> bool {
if a == 0 || a == 10 || a == 127 || a >= 224 {
return false;
}
if a == 100 && (64..128).contains(&b) {
return false;
}
if a == 169 && b == 254 {
return false;
}
if a == 172 && (16..32).contains(&b) {
return false;
}
if a == 192 && b == 168 {
return false;
}
if a == 192 && b == 0 && (c == 0 || c == 2) {
return false;
}
if a == 198 && (b == 18 || b == 19) {
return false;
}
if a == 198 && b == 51 && c == 100 {
return false;
}
if a == 203 && b == 0 && c == 113 {
return false;
}
true
}
fn unbracket(text: &str) -> &str {
let text = text.strip_prefix('[').unwrap_or(text);
text.strip_suffix(']').unwrap_or(text)
}
fn v6(text: &str) -> Option<[u32; 8]> {
let mut address = unbracket(text).split('%').next().unwrap_or("").to_lowercase();
if let Some(tail) = trailing_v4(&address) {
let four = v4(&tail)?;
address = format!(
"{}{:x}:{:x}",
&address[..address.len() - tail.len()],
(four[0] << 8) | four[1],
(four[2] << 8) | four[3]
);
}
let halves: Vec<&str> = address.split("::").collect();
if halves.len() > 2 {
return None;
}
let head: Vec<&str> = if halves[0].is_empty() { vec![] } else { halves[0].split(':').collect() };
let rest: Vec<&str> =
if halves.len() == 2 && !halves[1].is_empty() { halves[1].split(':').collect() } else { vec![] };
let missing = 8 - head.len() as i64 - rest.len() as i64;
if if halves.len() == 1 { missing != 0 } else { missing < 1 } {
return None;
}
let fill = if halves.len() == 2 { missing as usize } else { 0 };
let groups: Vec<&str> =
head.iter().copied().chain(std::iter::repeat_n("0", fill)).chain(rest.iter().copied()).collect();
let mut out = [0u32; 8];
for (i, g) in groups.iter().enumerate() {
if g.is_empty() || g.len() > 4 || !g.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
out[i] = u32::from_str_radix(g, 16).ok()?;
}
Some(out)
}
fn trailing_v4(text: &str) -> Option<String> {
let b = text.as_bytes();
for start in 0..b.len() {
if let Some(end) = v4_run(b, start)
&& end == b.len()
{
return Some(text[start..].to_string());
}
}
None
}
fn v4_run(b: &[u8], start: usize) -> Option<usize> {
fn digits_then(b: &[u8], at: usize, parts_left: usize) -> Option<usize> {
for n in (1..=3).rev() {
if at + n <= b.len() && b[at..at + n].iter().all(u8::is_ascii_digit) {
let end = at + n;
if parts_left == 0 {
if end == b.len() {
return Some(end);
}
} else if b.get(end) == Some(&b'.')
&& let Some(e) = digits_then(b, end + 1, parts_left - 1)
{
return Some(e);
}
}
}
None
}
digits_then(b, start, 3)
}
pub fn public_address(ip: &str) -> bool {
if let Some(four) = v4(ip) {
return public_v4(four);
}
let Some(g) = v6(ip) else {
return false;
};
let embedded = |hi: u32, lo: u32| [hi >> 8, hi & 255, lo >> 8, lo & 255];
if g[..5].iter().all(|x| *x == 0) && (g[5] == 0xffff || g[5] == 0) {
return if g[5] == 0 && g[6] == 0 && g[7] <= 1 { false } else { public_v4(embedded(g[6], g[7])) };
}
if g[0] == 0x64 && g[1] == 0xff9b && g[2..6].iter().all(|x| *x == 0) {
return public_v4(embedded(g[6], g[7]));
}
if g[0] == 0x2002 {
return public_v4(embedded(g[1], g[2]));
}
if (g[0] & 0xfe00) == 0xfc00 || (g[0] & 0xffc0) == 0xfe80 || (g[0] & 0xff00) == 0xff00 {
return false;
}
if g[0] == 0x2001 && (g[1] == 0 || g[1] == 0xdb8) {
return false;
}
if g[0] == 0x100 && g[1..4].iter().all(|x| *x == 0) {
return false;
}
true
}
const METADATA_NAMES: [&str; 5] =
["metadata", "metadata.google.internal", "metadata.goog", "instance-data", "instance-data.ec2.internal"];
pub fn metadata_address(ip: &str) -> bool {
let four = v4(ip);
let g = if four.is_some() { None } else { v6(ip) };
let inner = four.or_else(|| {
let g = g?;
let mapped = g[..5].iter().all(|x| *x == 0) && (g[5] == 0xffff || g[5] == 0);
let nat64 = g[0] == 0x64 && g[1] == 0xff9b && g[2..6].iter().all(|x| *x == 0);
(mapped || nat64).then(|| [g[6] >> 8, g[6] & 255, g[7] >> 8, g[7] & 255])
});
if let Some(inner) = inner {
return (inner[0] == 169 && inner[1] == 254) || inner == [100, 100, 100, 200];
}
let Some(g) = g else { return false };
if (g[0] & 0xffc0) == 0xfe80 {
return true;
}
g[0] == 0xfd00 && g[1] == 0xec2 && g[2..7].iter().all(|x| *x == 0) && g[7] == 0x254
}
pub async fn metadata_host(host: &str, with: Option<&Lookup>) -> bool {
let lower = host.trim().to_lowercase();
let name = unbracket(&lower).trim_end_matches('.');
if METADATA_NAMES.contains(&name) || metadata_address(name) {
return true;
}
if is_literal(name) {
return false;
}
resolve(name, with).await.iter().any(|a| metadata_address(a))
}
fn is_literal(host: &str) -> bool {
v4(host).is_some() || v6(host).is_some()
}
pub async fn lookup(name: &str) -> Vec<String> {
let bare = unbracket(name);
if is_literal(bare) {
return vec![bare.to_string()];
}
let mut found: Vec<String> = Vec::new();
if let Ok(addresses) = tokio::net::lookup_host((bare, 0)).await {
for a in addresses {
let ip = a.ip().to_string();
if !found.contains(&ip) {
found.push(ip);
}
}
}
found
}
async fn resolve(name: &str, with: Option<&Lookup>) -> Vec<String> {
match with {
Some(f) => f(name).await,
None => lookup(name).await,
}
}
pub async fn public_addresses(name: &str, with: Option<&Lookup>) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for a in resolve(name, with).await {
if public_address(&a) && !out.contains(&a) {
out.push(a);
}
}
out
}
pub async fn resolves_privately(name: &str, with: Option<&Lookup>) -> bool {
resolve(name, with).await.iter().any(|a| !public_address(a))
}
#[derive(Clone)]
pub struct PublicFetchInit {
pub method: String,
pub body: Option<Vec<u8>>,
pub timeout_ms: u64,
pub headers: Headers,
pub redirects: u32,
pub max_bytes: Option<usize>,
pub truncate: bool,
pub lookup: Option<Lookup>,
}
impl PublicFetchInit {
pub fn new(timeout_ms: u64) -> PublicFetchInit {
PublicFetchInit {
method: "GET".into(),
body: None,
timeout_ms,
headers: Headers::new(),
redirects: 0,
max_bytes: None,
truncate: false,
lookup: None,
}
}
}
pub async fn public_fetch(
fetcher: &dyn Fetcher,
target: &str,
init: &PublicFetchInit,
) -> Result<Response, PublicFetchError> {
let until = Instant::now() + Duration::from_millis(init.timeout_ms);
let left = || until.saturating_duration_since(Instant::now());
let mut url = Url::parse(target).ok_or_else(|| PublicFetchError::InvalidUrl(target.to_string()))?;
let mut hop: u32 = 0;
let method = init.method.to_ascii_uppercase();
let redirects = if method == "GET" { init.redirects } else { 0 };
loop {
if url.protocol() != "https:" {
return Err(PrivateAddressError(url.href()).into());
}
let host = unbracket(&url.hostname()).to_lowercase();
let literal = is_literal(&host);
if literal && !public_address(&host) {
return Err(PrivateAddressError(host).into());
}
if host == "localhost" || host.ends_with(".localhost") {
return Err(PrivateAddressError(host).into());
}
let mut options = FetchInit {
method: method.clone(),
headers: init.headers.clone(),
body: init.body.clone(),
manual_redirect: true,
max_bytes: init.max_bytes,
truncate: init.truncate,
..FetchInit::default()
};
let port = url.port().parse::<u16>().unwrap_or(443);
if literal {
options.resolve = host.parse::<IpAddr>().map(|ip| vec![(host.clone(), port, vec![ip])]).unwrap_or_default();
} else {
let addresses = match tokio::time::timeout(left(), resolve(&host, init.lookup.as_ref())).await {
Ok(found) => found,
Err(_) => return Err(FetchError::TimedOut.into()),
};
if addresses.is_empty() {
return Err(FetchError::Failed(format!("getaddrinfo ENOTFOUND {host}")).into());
}
if addresses.iter().any(|a| !public_address(a)) {
return Err(PrivateAddressError(host).into());
}
let ips: Vec<IpAddr> = addresses.iter().filter_map(|a| unbracket(a).parse().ok()).collect();
options.resolve = vec![(host.clone(), port, ips)];
}
let remaining = left();
let ms = remaining.as_millis() as u64;
if ms == 0 {
return Err(FetchError::TimedOut.into());
}
options.timeout_ms = ms;
let href = url.href();
let answer = match tokio::time::timeout(remaining, fetcher.fetch(&href, options)).await {
Err(_) => return Err(FetchError::TimedOut.into()),
Ok(Err(e)) if e == FetchError::TimedOut || Instant::now() >= until => {
return Err(FetchError::TimedOut.into());
}
Ok(Err(e)) => return Err(e.into()),
Ok(Ok(answer)) => answer,
};
let location = answer.headers.get("location").unwrap_or_default();
if answer.status < 300 || answer.status >= 400 || location.is_empty() || hop >= redirects {
return Ok(answer);
}
url = Url::parse_with_base(&location, &href).ok_or(PublicFetchError::InvalidUrl(location))?;
hop += 1;
}
}
fn local_install(url: &str) -> bool {
test(js_re!(r"^http://(localhost|127\.0\.0\.1)(:\d+)?(/|$)"), url)
}
pub fn install_address(url: &str, local: bool) -> bool {
test(js_re!(r"^https://[^/]+"), url) || (local && local_install(url))
}
pub(crate) async fn install_fetch(
fetcher: &dyn Fetcher,
url: &str,
mut init: FetchInit,
local: bool,
lookup: Option<&Lookup>,
) -> Result<Response, FetchError> {
if local && local_install(url) {
init.manual_redirect = true;
return fetcher.fetch(url, init).await;
}
let options = PublicFetchInit {
method: init.method,
body: init.body,
timeout_ms: init.timeout_ms,
headers: init.headers,
redirects: 0,
max_bytes: init.max_bytes,
truncate: init.truncate,
lookup: lookup.cloned(),
};
public_fetch(fetcher, url, &options).await.map_err(|e| match e {
PublicFetchError::Fetch(e) => e,
e => FetchError::Failed(e.to_string()),
})
}