Browse Source
* Improved HTTP client * Change config compat to use auto, rename blacklist * Fix wrong doc referencespull/4737/head
Daniel García
7 months ago
committed by
GitHub
12 changed files with 326 additions and 217 deletions
@ -0,0 +1,246 @@ |
|||||
|
use std::{ |
||||
|
fmt, |
||||
|
net::{IpAddr, SocketAddr}, |
||||
|
str::FromStr, |
||||
|
sync::{Arc, Mutex}, |
||||
|
time::Duration, |
||||
|
}; |
||||
|
|
||||
|
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver}; |
||||
|
use once_cell::sync::Lazy; |
||||
|
use regex::Regex; |
||||
|
use reqwest::{ |
||||
|
dns::{Name, Resolve, Resolving}, |
||||
|
header, Client, ClientBuilder, |
||||
|
}; |
||||
|
use url::Host; |
||||
|
|
||||
|
use crate::{util::is_global, CONFIG}; |
||||
|
|
||||
|
pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> { |
||||
|
let Ok(url) = url::Url::parse(url) else { |
||||
|
err!("Invalid URL"); |
||||
|
}; |
||||
|
let Some(host) = url.host() else { |
||||
|
err!("Invalid host"); |
||||
|
}; |
||||
|
|
||||
|
should_block_host(host)?; |
||||
|
|
||||
|
static INSTANCE: Lazy<Client> = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client")); |
||||
|
|
||||
|
Ok(INSTANCE.request(method, url)) |
||||
|
} |
||||
|
|
||||
|
pub fn get_reqwest_client_builder() -> ClientBuilder { |
||||
|
let mut headers = header::HeaderMap::new(); |
||||
|
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); |
||||
|
|
||||
|
let redirect_policy = reqwest::redirect::Policy::custom(|attempt| { |
||||
|
if attempt.previous().len() >= 5 { |
||||
|
return attempt.error("Too many redirects"); |
||||
|
} |
||||
|
|
||||
|
let Some(host) = attempt.url().host() else { |
||||
|
return attempt.error("Invalid host"); |
||||
|
}; |
||||
|
|
||||
|
if let Err(e) = should_block_host(host) { |
||||
|
return attempt.error(e); |
||||
|
} |
||||
|
|
||||
|
attempt.follow() |
||||
|
}); |
||||
|
|
||||
|
Client::builder() |
||||
|
.default_headers(headers) |
||||
|
.redirect(redirect_policy) |
||||
|
.dns_resolver(CustomDnsResolver::instance()) |
||||
|
.timeout(Duration::from_secs(10)) |
||||
|
} |
||||
|
|
||||
|
pub fn should_block_address(domain_or_ip: &str) -> bool { |
||||
|
if let Ok(ip) = IpAddr::from_str(domain_or_ip) { |
||||
|
if should_block_ip(ip) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
should_block_address_regex(domain_or_ip) |
||||
|
} |
||||
|
|
||||
|
fn should_block_ip(ip: IpAddr) -> bool { |
||||
|
if !CONFIG.http_request_block_non_global_ips() { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
!is_global(ip) |
||||
|
} |
||||
|
|
||||
|
fn should_block_address_regex(domain_or_ip: &str) -> bool { |
||||
|
let Some(block_regex) = CONFIG.http_request_block_regex() else { |
||||
|
return false; |
||||
|
}; |
||||
|
|
||||
|
static COMPILED_REGEX: Mutex<Option<(String, Regex)>> = Mutex::new(None); |
||||
|
let mut guard = COMPILED_REGEX.lock().unwrap(); |
||||
|
|
||||
|
// If the stored regex is up to date, use it
|
||||
|
if let Some((value, regex)) = &*guard { |
||||
|
if value == &block_regex { |
||||
|
return regex.is_match(domain_or_ip); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If we don't have a regex stored, or it's not up to date, recreate it
|
||||
|
let regex = Regex::new(&block_regex).unwrap(); |
||||
|
let is_match = regex.is_match(domain_or_ip); |
||||
|
*guard = Some((block_regex, regex)); |
||||
|
|
||||
|
is_match |
||||
|
} |
||||
|
|
||||
|
fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> { |
||||
|
let (ip, host_str): (Option<IpAddr>, String) = match host { |
||||
|
url::Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()), |
||||
|
url::Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()), |
||||
|
url::Host::Domain(d) => (None, d.to_string()), |
||||
|
}; |
||||
|
|
||||
|
if let Some(ip) = ip { |
||||
|
if should_block_ip(ip) { |
||||
|
return Err(CustomHttpClientError::NonGlobalIp { |
||||
|
domain: None, |
||||
|
ip, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if should_block_address_regex(&host_str) { |
||||
|
return Err(CustomHttpClientError::Blocked { |
||||
|
domain: host_str, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Ok(()) |
||||
|
} |
||||
|
|
||||
|
#[derive(Debug, Clone)] |
||||
|
pub enum CustomHttpClientError { |
||||
|
Blocked { |
||||
|
domain: String, |
||||
|
}, |
||||
|
NonGlobalIp { |
||||
|
domain: Option<String>, |
||||
|
ip: IpAddr, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
impl CustomHttpClientError { |
||||
|
pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> { |
||||
|
let mut source = e.source(); |
||||
|
|
||||
|
while let Some(err) = source { |
||||
|
source = err.source(); |
||||
|
if let Some(err) = err.downcast_ref::<CustomHttpClientError>() { |
||||
|
return Some(err); |
||||
|
} |
||||
|
} |
||||
|
None |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
impl fmt::Display for CustomHttpClientError { |
||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
|
match self { |
||||
|
Self::Blocked { |
||||
|
domain, |
||||
|
} => write!(f, "Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX"), |
||||
|
Self::NonGlobalIp { |
||||
|
domain: Some(domain), |
||||
|
ip, |
||||
|
} => write!(f, "IP {ip} for domain '{domain}' is not a global IP!"), |
||||
|
Self::NonGlobalIp { |
||||
|
domain: None, |
||||
|
ip, |
||||
|
} => write!(f, "IP {ip} is not a global IP!"), |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
impl std::error::Error for CustomHttpClientError {} |
||||
|
|
||||
|
#[derive(Debug, Clone)] |
||||
|
enum CustomDnsResolver { |
||||
|
Default(), |
||||
|
Hickory(Arc<TokioAsyncResolver>), |
||||
|
} |
||||
|
type BoxError = Box<dyn std::error::Error + Send + Sync>; |
||||
|
|
||||
|
impl CustomDnsResolver { |
||||
|
fn instance() -> Arc<Self> { |
||||
|
static INSTANCE: Lazy<Arc<CustomDnsResolver>> = Lazy::new(CustomDnsResolver::new); |
||||
|
Arc::clone(&*INSTANCE) |
||||
|
} |
||||
|
|
||||
|
fn new() -> Arc<Self> { |
||||
|
match read_system_conf() { |
||||
|
Ok((config, opts)) => { |
||||
|
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone()); |
||||
|
Arc::new(Self::Hickory(Arc::new(resolver))) |
||||
|
} |
||||
|
Err(e) => { |
||||
|
warn!("Error creating Hickory resolver, falling back to default: {e:?}"); |
||||
|
Arc::new(Self::Default()) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
||||
|
async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> { |
||||
|
pre_resolve(name)?; |
||||
|
|
||||
|
let result = match self { |
||||
|
Self::Default() => tokio::net::lookup_host(name).await?.next(), |
||||
|
Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)), |
||||
|
}; |
||||
|
|
||||
|
if let Some(addr) = &result { |
||||
|
post_resolve(name, addr.ip())?; |
||||
|
} |
||||
|
|
||||
|
Ok(result) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> { |
||||
|
if should_block_address(name) { |
||||
|
return Err(CustomHttpClientError::Blocked { |
||||
|
domain: name.to_string(), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Ok(()) |
||||
|
} |
||||
|
|
||||
|
fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomHttpClientError> { |
||||
|
if should_block_ip(ip) { |
||||
|
Err(CustomHttpClientError::NonGlobalIp { |
||||
|
domain: Some(name.to_string()), |
||||
|
ip, |
||||
|
}) |
||||
|
} else { |
||||
|
Ok(()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
impl Resolve for CustomDnsResolver { |
||||
|
fn resolve(&self, name: Name) -> Resolving { |
||||
|
let this = self.clone(); |
||||
|
Box::pin(async move { |
||||
|
let name = name.as_str(); |
||||
|
let result = this.resolve_domain(name).await?; |
||||
|
Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter())) |
||||
|
}) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue