pub mod proxy;
pub mod reload;
pub mod tls;
use ferryman_core::SharedTable;
use hyper::body::Incoming;
use hyper::service::service_fn;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use hyper_util::server::conn::auto::Builder as HttpAutoBuilder;
use hyper_util::server::graceful::GracefulShutdown;
use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tls::MaybeTlsStream;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
pub type ProxyClient = Client<HttpConnector, Incoming>;
const GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(25);
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
const H2_KEEP_ALIVE: Duration = Duration::from_secs(30);
pub async fn serve(
listener: TcpListener,
table: SharedTable,
tls: Option<TlsAcceptor>,
shutdown: impl Future<Output = ()>,
) -> anyhow::Result<()> {
let mut connector = HttpConnector::new();
connector.set_nodelay(true);
let client: ProxyClient = Client::builder(TokioExecutor::new()).build(connector);
let mut builder = HttpAutoBuilder::new(TokioExecutor::new());
builder
.http1()
.timer(TokioTimer::new())
.header_read_timeout(HANDSHAKE_TIMEOUT);
builder
.http2()
.timer(TokioTimer::new())
.keep_alive_interval(H2_KEEP_ALIVE);
let graceful = GracefulShutdown::new();
let proto = if tls.is_some() { "https" } else { "http" };
tokio::pin!(shutdown);
loop {
tokio::select! {
_ = &mut shutdown => break,
accepted = listener.accept() => {
let (stream, peer) = match accepted {
Ok(x) => x,
Err(e) => {
tracing::warn!(?e, "accept failed");
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
};
let _ = stream.set_nodelay(true);
let table = table.clone();
let client = client.clone();
let tls = tls.clone();
let builder = builder.clone();
let watcher = graceful.watcher();
tokio::spawn(async move {
let stream = match tls {
Some(acceptor) => {
match tokio::time::timeout(HANDSHAKE_TIMEOUT, acceptor.accept(stream)).await {
Ok(Ok(s)) => MaybeTlsStream::Tls(Box::new(s)),
Ok(Err(e)) => {
tracing::debug!(?peer, ?e, "tls handshake failed");
return;
}
Err(_) => {
tracing::debug!(?peer, "tls handshake timed out");
return;
}
}
}
None => MaybeTlsStream::Plain(stream),
};
let io = TokioIo::new(stream);
let seen_request = Arc::new(AtomicBool::new(false));
let seen = seen_request.clone();
let svc = service_fn(move |req| {
seen.store(true, Ordering::Relaxed);
proxy::handle(table.clone(), client.clone(), peer, proto, req)
});
let conn = watcher.watch(builder.serve_connection(io, svc));
tokio::pin!(conn);
let first_request_deadline = async {
tokio::time::sleep(HANDSHAKE_TIMEOUT).await;
if seen_request.load(Ordering::Relaxed) {
std::future::pending::<()>().await;
}
};
tokio::select! {
res = &mut conn => {
if let Err(e) = res {
tracing::debug!(?peer, ?e, "connection closed with error");
}
}
_ = first_request_deadline => {
tracing::debug!(?peer, "no request before deadline; closing");
}
}
});
}
}
}
drop(listener);
tokio::select! {
_ = graceful.shutdown() => {}
_ = tokio::time::sleep(GRACEFUL_SHUTDOWN_TIMEOUT) => {
tracing::warn!("graceful shutdown timed out waiting for in-flight connections");
}
}
Ok(())
}