use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use hyper::Request;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::{Connect, HttpConnector};
use hyper_util::rt::{TokioExecutor, TokioTimer};
use plecto_control::{TlsClientConfig, UpstreamGroup};
use crate::body::stream;
use crate::{BoxError, ReqBody, ResponseBody, upstream_connector};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SendErrorKind {
Connect,
Other,
}
#[derive(Debug, thiserror::Error)]
#[error("upstream send failed: {source}")]
pub(crate) struct UpstreamSendError {
pub(crate) kind: SendErrorKind,
#[source]
pub(crate) source: BoxError,
}
impl UpstreamSendError {
pub(crate) fn is_connect(&self) -> bool {
self.kind == SendErrorKind::Connect
}
}
pub(crate) trait UpstreamClient: Send + Sync {
fn request(
&self,
req: Request<ReqBody>,
) -> impl std::future::Future<Output = Result<hyper::Response<ResponseBody>, UpstreamSendError>> + Send;
}
#[derive(Clone)]
pub(crate) enum HyperUpstreamClient {
Plain(Client<HttpConnector, ReqBody>),
Tls(Client<HttpsConnector<HttpConnector>, ReqBody>),
}
async fn send_pooled<C>(
client: &Client<C, ReqBody>,
req: Request<ReqBody>,
) -> Result<hyper::Response<ResponseBody>, UpstreamSendError>
where
C: Connect + Clone + Send + Sync + 'static,
{
match client.request(req).await {
Ok(resp) => Ok(resp.map(stream)),
Err(e) => Err(UpstreamSendError {
kind: if e.is_connect() {
SendErrorKind::Connect
} else {
SendErrorKind::Other
},
source: Box::new(e),
}),
}
}
impl UpstreamClient for HyperUpstreamClient {
async fn request(
&self,
req: Request<ReqBody>,
) -> Result<hyper::Response<ResponseBody>, UpstreamSendError> {
match self {
HyperUpstreamClient::Plain(client) => send_pooled(client, req).await,
HyperUpstreamClient::Tls(client) => send_pooled(client, req).await,
}
}
}
fn pooled_builder() -> hyper_util::client::legacy::Builder {
let mut builder = Client::builder(TokioExecutor::new());
builder
.pool_timer(TokioTimer::new())
.pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(32);
builder
}
fn build_tls_client(
config: &TlsClientConfig,
sni: Option<&rustls::pki_types::ServerName<'static>>,
) -> HyperUpstreamClient {
let builder = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(config.clone())
.https_only();
let builder = match sni {
Some(name) => builder
.with_server_name_resolver(hyper_rustls::FixedServerNameResolver::new(name.clone())),
None => builder,
};
let connector = builder
.enable_all_versions()
.wrap_connector(upstream_connector());
HyperUpstreamClient::Tls(pooled_builder().build(connector))
}
pub(crate) struct UpstreamClients {
plain: HyperUpstreamClient,
tls: parking_lot::RwLock<HashMap<usize, (Arc<TlsClientConfig>, HyperUpstreamClient)>>,
}
const MAX_TLS_CLIENTS: usize = 64;
impl UpstreamClients {
pub(crate) fn new() -> Self {
Self {
plain: HyperUpstreamClient::Plain(pooled_builder().build(upstream_connector())),
tls: parking_lot::RwLock::new(HashMap::new()),
}
}
pub(crate) fn for_group(&self, group: &UpstreamGroup) -> HyperUpstreamClient {
let Some(config) = group.tls_client_config() else {
return self.plain.clone();
};
let key = Arc::as_ptr(config) as usize;
if let Some((_config, client)) = self.tls.read().get(&key) {
return client.clone();
}
let built = build_tls_client(config, group.tls_sni());
let mut map = self.tls.write();
if map.len() >= MAX_TLS_CLIENTS {
map.clear();
}
map.entry(key).or_insert((config.clone(), built)).1.clone()
}
}
#[cfg(test)]
pub(crate) mod fake {
use std::sync::Mutex;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use super::{Request, ResponseBody, SendErrorKind, UpstreamClient, UpstreamSendError};
use crate::{BoxError, ReqBody};
#[derive(Clone, Copy)]
pub(crate) enum Scripted {
Status(u16),
SendError(SendErrorKind),
Hang(std::time::Duration),
}
pub(crate) struct FakeUpstreamClient {
script: Mutex<Vec<Scripted>>,
calls: Mutex<u32>,
bodies: Mutex<Vec<Bytes>>,
headers: Mutex<Vec<hyper::HeaderMap>>,
}
impl FakeUpstreamClient {
pub(crate) fn new(script: Vec<Scripted>) -> Self {
Self {
script: Mutex::new(script),
calls: Mutex::new(0),
bodies: Mutex::new(Vec::new()),
headers: Mutex::new(Vec::new()),
}
}
pub(crate) fn calls(&self) -> u32 {
*self.calls.lock().unwrap()
}
pub(crate) fn bodies(&self) -> Vec<Bytes> {
self.bodies.lock().unwrap().clone()
}
pub(crate) fn headers(&self) -> Vec<hyper::HeaderMap> {
self.headers.lock().unwrap().clone()
}
}
impl UpstreamClient for FakeUpstreamClient {
async fn request(
&self,
req: Request<ReqBody>,
) -> Result<hyper::Response<ResponseBody>, UpstreamSendError> {
*self.calls.lock().unwrap() += 1;
self.headers.lock().unwrap().push(req.headers().clone());
let body = req
.into_body()
.collect()
.await
.map(|c| c.to_bytes())
.unwrap_or_default();
self.bodies.lock().unwrap().push(body);
let next = {
let mut script = self.script.lock().unwrap();
if script.len() > 1 {
script.remove(0)
} else {
script.first().copied().unwrap_or(Scripted::Status(200))
}
};
match next {
Scripted::Status(status) => Ok(hyper::Response::builder()
.status(status)
.body(
Full::new(Bytes::new())
.map_err(|e: std::convert::Infallible| -> BoxError { match e {} })
.boxed(),
)
.unwrap()),
Scripted::SendError(kind) => Err(UpstreamSendError {
kind,
source: "simulated send failure".into(),
}),
Scripted::Hang(dur) => {
tokio::time::sleep(dur).await;
Ok(hyper::Response::builder()
.status(200)
.body(
Full::new(Bytes::new())
.map_err(|e: std::convert::Infallible| -> BoxError { match e {} })
.boxed(),
)
.unwrap())
}
}
}
}
}