use crate::resolver::PinningResolver;
use crate::security::headers::{apply_forwarding_policy, strip_hop_by_hop};
use bytes::Bytes;
use http::{HeaderValue, Request, Response, StatusCode};
use http_body_util::{BodyExt, Full, Limited};
use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::HttpConnector;
use std::net::IpAddr;
use std::time::Duration;
#[derive(Debug)]
pub enum ProxyError {
Connect,
Status(StatusCode),
}
impl From<StatusCode> for ProxyError {
fn from(status: StatusCode) -> Self {
ProxyError::Status(status)
}
}
pub struct ProxyClient {
client: Client<HttpConnector<PinningResolver>, Full<Bytes>>,
resolver: PinningResolver,
request_timeout: Duration,
max_response_size: usize,
trusted_proxies: Vec<ipnetwork::IpNetwork>,
apply_attribution_policy: bool,
}
impl ProxyClient {
pub fn new(
allowed_networks: Vec<ipnetwork::IpNetwork>,
dns_ttl: Duration,
request_timeout: Duration,
max_response_size: usize,
trusted_proxies: Vec<ipnetwork::IpNetwork>,
apply_attribution_policy: bool,
) -> Self {
let resolver = PinningResolver::new(allowed_networks, dns_ttl);
let mut connector = HttpConnector::new_with_resolver(resolver.clone());
connector.set_connect_timeout(Some(Duration::from_secs(10)));
connector.set_nodelay(true);
let client = Client::builder(hyper_util::rt::TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(90))
.build(connector);
Self {
client,
resolver,
request_timeout,
max_response_size,
trusted_proxies,
apply_attribution_policy,
}
}
pub fn resolver(&self) -> &PinningResolver {
&self.resolver
}
pub async fn forward(
&self,
upstream_addr: &str,
parts: &http::request::Parts,
body: Bytes,
client_ip: IpAddr,
scheme: &'static str,
) -> Result<Response<Full<Bytes>>, ProxyError> {
let upstream_uri: http::Uri = format!(
"http://{}{}",
upstream_addr,
parts
.uri
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or("/")
)
.parse()
.map_err(|_| StatusCode::BAD_GATEWAY)?;
let mut headers = parts.headers.clone();
let original_host = headers.get(http::header::HOST).cloned();
strip_hop_by_hop(&mut headers);
headers.remove(http::header::CONTENT_LENGTH);
if self.apply_attribution_policy {
apply_forwarding_policy(
&mut headers,
client_ip,
scheme,
original_host.as_ref(),
&self.trusted_proxies,
);
}
let mut upstream_req = Request::new(Full::new(body));
*upstream_req.method_mut() = parts.method.clone();
*upstream_req.uri_mut() = upstream_uri;
*upstream_req.version_mut() = http::Version::HTTP_11;
*upstream_req.headers_mut() = headers;
let host_value = match original_host {
Some(h) => h,
None => HeaderValue::from_str(upstream_addr).map_err(|_| StatusCode::BAD_GATEWAY)?,
};
upstream_req
.headers_mut()
.insert(http::header::HOST, host_value);
let resp = match tokio::time::timeout(
self.request_timeout,
self.client.request(upstream_req),
)
.await
{
Err(_elapsed) => return Err(ProxyError::Status(StatusCode::GATEWAY_TIMEOUT)),
Ok(Err(e)) if e.is_connect() => return Err(ProxyError::Connect),
Ok(Err(_)) => return Err(ProxyError::Status(StatusCode::BAD_GATEWAY)),
Ok(Ok(resp)) => resp,
};
let (resp_parts, resp_body) = resp.into_parts();
let upstream_has_cl = resp_parts.headers.contains_key(http::header::CONTENT_LENGTH);
let upstream_has_te = resp_parts
.headers
.contains_key(http::header::TRANSFER_ENCODING);
if upstream_has_cl && upstream_has_te {
return Err(ProxyError::Status(StatusCode::BAD_GATEWAY));
}
let resp_body_bytes = Limited::new(resp_body, self.max_response_size)
.collect()
.await
.map_err(|_| ProxyError::Status(StatusCode::BAD_GATEWAY))?
.to_bytes();
let mut resp_headers = resp_parts.headers;
strip_hop_by_hop(&mut resp_headers);
let mut out = Response::new(Full::new(resp_body_bytes));
*out.status_mut() = resp_parts.status;
*out.headers_mut() = resp_headers;
Ok(out)
}
}