use std::str::FromStr;
use std::sync::Arc;
use crate::RxMeta;
use crate::auth::Signer;
use crate::backend::{Backend, Config};
use crate::clock::monotonic_ns;
use crate::driver::Driver;
use crate::endpoint::{ConnId, Handler};
use crate::error::{Error, Result};
use crate::http::{HttpConn, HttpDial, HttpHandler, HttpOptions, Response};
use crate::net::Target;
use crate::outcome::Sent;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpImpl {
Native,
Curl,
}
impl FromStr for HttpImpl {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"native" => Ok(Self::Native),
"curl" => Ok(Self::Curl),
_ => Err(Error::Config(format!("unknown HTTP implementation {s:?}"))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurlDrive {
Perform,
SocketAction,
}
#[derive(Debug, Clone)]
pub struct HttpClientConfig {
pub implementation: HttpImpl,
pub driver: Config,
pub pool: usize,
pub request_timeout_ms: u32,
pub buf_size: usize,
pub background_connect: bool,
pub curl_drive: CurlDrive,
pub curl_ca_file: Option<String>,
pub curl_allow_non_idempotent_methods: bool,
pub connect_gate: bool,
pub reconnect_seed: u64,
}
impl HttpClientConfig {
#[must_use]
pub const fn new(implementation: HttpImpl) -> Self {
Self {
implementation,
driver: Config::new(Backend::RecvBusy),
pool: 2,
request_timeout_ms: 5_000,
buf_size: 1 << 20,
background_connect: true,
curl_drive: CurlDrive::Perform,
curl_ca_file: None,
curl_allow_non_idempotent_methods: false,
connect_gate: false,
reconnect_seed: 0,
}
}
}
pub trait ClientHandler {
fn on_response(&mut self, request: u64, response: &Response<'_>);
fn on_error(&mut self, request: u64, sent: Sent, error: Error) {
let _ = (request, sent, error);
}
fn on_connect_error(&mut self, error: Error) {
crate::warn::connect_failure("HttpClient could not open a connection", &error);
}
fn connect_gate(&mut self) -> std::result::Result<(), u64> {
Ok(())
}
}
enum Imp {
Native(Box<Native>),
#[cfg(feature = "curl")]
Curl(Box<crate::curl_client::CurlClient>),
}
pub struct HttpClient {
imp: Imp,
next_id: u64,
}
impl HttpClient {
pub fn new(
cfg: &HttpClientConfig,
target: Target,
tls: Arc<rustls::ClientConfig>,
) -> Result<Self> {
if cfg.pool == 0 {
return Err(Error::Config("pool must be at least 1".into()));
}
let imp = match cfg.implementation {
HttpImpl::Native => {
Imp::Native(Box::new(Native::new(cfg, target, tls)?))
}
#[cfg(feature = "curl")]
HttpImpl::Curl => {
let _ = tls;
if cfg.connect_gate {
return Err(Error::Config(
"connect_gate: libcurl opens its own connections; use the native client"
.into(),
));
}
if cfg.driver.quickack_check == crate::route::QuickAckCheck::Warn {
crate::warn::prepare();
}
Imp::Curl(Box::new(crate::curl_client::CurlClient::new(cfg, &target)?))
}
#[cfg(not(feature = "curl"))]
HttpImpl::Curl => {
return Err(Error::Unsupported(
"the libcurl client needs the `curl` feature",
));
}
};
Ok(Self { imp, next_id: 0 })
}
#[must_use]
pub const fn implementation(&self) -> HttpImpl {
match self.imp {
Imp::Native(_) => HttpImpl::Native,
#[cfg(feature = "curl")]
Imp::Curl(_) => HttpImpl::Curl,
}
}
#[must_use]
pub fn connect_failures(&self) -> u64 {
match &self.imp {
Imp::Native(n) => n.connect_failures,
#[cfg(feature = "curl")]
Imp::Curl(_) => 0,
}
}
#[must_use]
pub fn route_checks(&self) -> Vec<crate::route::RouteCheck> {
match &self.imp {
Imp::Native(n) => n
.slots
.iter()
.filter(|s| s.open)
.filter_map(|s| s.id.and_then(|id| n.driver.get(id)))
.map(|c| c.route().clone())
.collect(),
#[cfg(feature = "curl")]
Imp::Curl(c) => c.route_checks(),
}
}
#[must_use]
pub fn idle(&self) -> usize {
match &self.imp {
Imp::Native(n) => n.idle(),
#[cfg(feature = "curl")]
Imp::Curl(c) => c.idle(),
}
}
pub fn request(
&mut self,
method: &str,
path: &str,
headers: &[(&str, &str)],
body: &[u8],
) -> Result<u64> {
let id = self.next_id;
match &mut self.imp {
Imp::Native(n) => n.send(id, |c| c.request(method, path, headers, body))?,
#[cfg(feature = "curl")]
Imp::Curl(c) => c.request(id, method, path, None, headers, body)?,
}
self.next_id += 1;
Ok(id)
}
pub fn signed_request(
&mut self,
method: &str,
path: &str,
params: &str,
headers: &[(&str, &str)],
signer: &Signer,
) -> Result<u64> {
let now_ms = u64::try_from(crate::clock::realtime_ns() / 1_000_000).unwrap_or(0);
self.signed_request_at(method, path, params, headers, signer, now_ms)
}
pub fn signed_request_at(
&mut self,
method: &str,
path: &str,
params: &str,
headers: &[(&str, &str)],
signer: &Signer,
timestamp_ms: u64,
) -> Result<u64> {
let id = self.next_id;
match &mut self.imp {
Imp::Native(n) => {
n.send(id, |c| {
c.signed_request_at(method, path, params, headers, signer, timestamp_ms)
})?;
}
#[cfg(feature = "curl")]
Imp::Curl(c) => {
crate::auth::validate_params(params)?;
if path.contains('?') {
return Err(Error::Http("put query parameters in `params`".into()));
}
let mut q = Vec::with_capacity(params.len() + 128);
crate::auth::append_signed_params(&mut q, params, timestamp_ms, signer);
let q = String::from_utf8(q).map_err(|_| Error::Http("non-UTF-8 query".into()))?;
c.request(id, method, path, Some(&q), headers, &[])?;
}
}
self.next_id += 1;
Ok(id)
}
pub fn poll<H: ClientHandler>(&mut self, handler: &mut H) -> Result<usize> {
match &mut self.imp {
Imp::Native(n) => {
if n.gated {
return Err(Error::Config(
"connect_gate is set: poll with poll_gated".into(),
));
}
n.poll(handler, false)
}
#[cfg(feature = "curl")]
Imp::Curl(c) => c.poll(handler),
}
}
pub fn poll_gated<H: ClientHandler>(&mut self, handler: &mut H) -> Result<usize> {
match &mut self.imp {
Imp::Native(n) => n.poll(handler, true),
#[cfg(feature = "curl")]
Imp::Curl(_) => Err(Error::Unsupported(
"the libcurl client opens its own connections: no connect gate",
)),
}
}
}
struct Slot {
id: Option<ConnId>,
open: bool,
request: Option<(u64, u64)>,
retry_at_ns: i64,
failures: u32,
closed: bool,
}
const REOPEN_NS: f64 = 100e6;
const REOPEN_MAX_NS: i64 = 5_000_000_000;
struct Native {
driver: Driver<HttpConn>,
target: Target,
tls: Arc<rustls::ClientConfig>,
opts: HttpOptions,
background: bool,
slots: Vec<Slot>,
next: usize,
connect_failures: u64,
gated: bool,
rng: u64,
}
impl Native {
fn new(cfg: &HttpClientConfig, target: Target, tls: Arc<rustls::ClientConfig>) -> Result<Self> {
let mut n = Self {
driver: Driver::new(cfg.driver.clone())?,
target,
tls,
opts: HttpOptions {
request_timeout_ms: cfg.request_timeout_ms,
max_in_flight: 1,
buf_size: cfg.buf_size,
},
background: cfg.background_connect,
slots: (0..cfg.pool)
.map(|_| Slot {
id: None,
open: false,
request: None,
retry_at_ns: i64::MIN,
failures: 0,
closed: false,
})
.collect(),
next: 0,
connect_failures: 0,
gated: cfg.connect_gate,
rng: crate::reconnect::rng_state(cfg.reconnect_seed)?,
};
HttpDial::new(n.target.clone(), n.tls.clone(), n.opts)?;
if n.background {
n.driver.background_ready()?;
}
if !n.gated {
for i in 0..n.slots.len() {
let _ = n.open(i);
}
}
Ok(n)
}
fn open(&mut self, i: usize) -> Result<()> {
let dial = HttpDial::new(self.target.clone(), self.tls.clone(), self.opts)?;
let id = if self.background {
self.driver.connect_background(dial)?
} else {
self.driver.connect(dial)?
};
self.slots[i].id = Some(id);
Ok(())
}
fn idle(&self) -> usize {
self.slots
.iter()
.filter(|s| s.open && s.request.is_none())
.count()
}
fn send(&mut self, id: u64, f: impl FnOnce(&mut HttpConn) -> Result<u64>) -> Result<()> {
let n = self.slots.len();
let Some(i) = (0..n)
.map(|k| (self.next + k) % n)
.find(|&i| self.slots[i].open && self.slots[i].request.is_none())
else {
return Err(Error::Busy("no idle connection in the pool"));
};
let mut conn = self.slots[i]
.id
.and_then(|c| self.driver.get_mut(c))
.ok_or(Error::Closed)?;
let on_conn = f(conn.inner())?;
self.slots[i].request = Some((id, on_conn));
self.next = (i + 1) % n;
Ok(())
}
fn poll<H: ClientHandler>(&mut self, handler: &mut H, gated: bool) -> Result<usize> {
let mut a = Adapter {
h: handler,
slots: &mut self.slots,
done: 0,
connect_failures: 0,
};
self.driver.poll(&mut a)?;
let done = a.done;
self.connect_failures += a.connect_failures;
if self.slots.iter().any(|s| s.id.is_none()) {
self.reopen(handler, gated);
}
Ok(done)
}
#[cold]
fn reopen<H: ClientHandler>(&mut self, handler: &mut H, gated: bool) {
let now = monotonic_ns();
let n = self.slots.len();
for i in 0..n {
if std::mem::take(&mut self.slots[i].closed) {
let delay = self.reopen_delay_ns(self.slots[i].failures);
self.slots[i].retry_at_ns = now.saturating_add(delay);
}
}
for i in 0..n {
if self.slots[i].id.is_some() || now < self.slots[i].retry_at_ns {
continue;
}
let gate = if gated {
handler.connect_gate()
} else {
Ok(())
};
if let Err(wait_ns) = gate {
let wait = i64::try_from(wait_ns)
.unwrap_or(i64::MAX)
.min(REOPEN_MAX_NS);
self.slots[i].retry_at_ns = now.saturating_add(wait);
continue;
}
if let Err(e) = self.open(i) {
self.connect_failures += 1;
let failures = self.slots[i].failures.saturating_add(1);
self.slots[i].failures = failures;
let delay = self.reopen_delay_ns(failures);
self.slots[i].retry_at_ns = now.saturating_add(delay);
handler.on_connect_error(e);
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn reopen_delay_ns(&mut self, failures: u32) -> i64 {
let limit = (REOPEN_NS * f64::from(1u32 << failures.min(6))).min(REOPEN_MAX_NS as f64);
(crate::reconnect::unit(&mut self.rng) * limit) as i64
}
}
struct Adapter<'a, H> {
h: &'a mut H,
slots: &'a mut [Slot],
done: usize,
connect_failures: u64,
}
impl<H> Adapter<'_, H> {
fn slot(&mut self, id: ConnId) -> Option<&mut Slot> {
self.slots.iter_mut().find(|s| s.id == Some(id))
}
}
impl<H: ClientHandler> Handler for Adapter<'_, H> {
fn on_open(&mut self, id: ConnId) {
if let Some(s) = self.slot(id) {
s.open = true;
s.failures = 0;
}
}
fn on_closed(&mut self, id: ConnId, error: Error) {
let Some(s) = self.slot(id) else {
return;
};
s.id = None;
s.closed = true;
let was_open = std::mem::replace(&mut s.open, false);
if !was_open {
s.failures = s.failures.saturating_add(1);
}
if let Some((r, on_conn)) = s.request.take() {
self.done += 1;
let (sent, error) = match error {
Error::Unanswered(u) => {
let sent = match u.first_unsent {
Some(first) if first <= on_conn => Sent::No,
_ => Sent::Maybe,
};
(sent, u.cause)
}
e => (Sent::Maybe, e),
};
self.h.on_error(r, sent, error);
} else if !was_open {
self.connect_failures += 1;
self.h.on_connect_error(error);
}
}
}
impl<H: ClientHandler> HttpHandler for Adapter<'_, H> {
fn on_response(&mut self, conn: ConnId, _request: u64, response: &Response<'_>, _: &RxMeta) {
if let Some((r, _)) = self.slot(conn).and_then(|s| s.request.take()) {
self.done += 1;
self.h.on_response(r, response);
}
}
}