use std::sync::Arc;
use std::time::Instant;
use boring::ssl::SslConnector;
use bytes::Bytes;
use tokio::net::{TcpStream, UnixStream};
use crate::api::common::VERSIONS;
use crate::cookies::CookieJar;
use crate::hsts::HSTSStore;
use crate::helpers::text::Text;
use crate::models::{ALPN, Body, ConnectionID, Headers, Limits, Message, Method, Port, TransportKind, URL, Version};
use crate::tls::Security;
use crate::protocol::base::{AnyConnection, Connection, Transport};
use crate::protocol::common::Error;
use crate::protocol::h1::H1Connection;
use crate::protocol::h2::H2Connection;
use crate::protocol::h3::{H3Connection, H3Session};
use crate::protocol::handler::QUICApplication;
use crate::protocol::quic;
use crate::helpers::sync;
#[derive(Clone)]
pub struct ClientConfig {
pub versions: Vec<Version>,
pub limits: ClientLimits,
pub secure: bool,
pub roots: Option<Vec<Vec<u8>>>,
pub tls: crate::tls::TLSConfig,
pub ech: std::collections::HashMap<String, Vec<u8>>,
pub cookies: bool,
pub hsts: bool,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
versions: VERSIONS.to_vec(),
limits: ClientLimits::default(),
secure: true,
roots: None,
tls: crate::tls::TLSConfig::default(),
ech: std::collections::HashMap::new(),
cookies: true,
hsts: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ClientLimits {
pub message: Limits,
pub connection_timeout: f64,
}
impl Default for ClientLimits {
fn default() -> Self {
Self { message: Limits::default(), connection_timeout: 10.0 }
}
}
pub struct Client {
pub config: ClientConfig,
pub jar: Option<Arc<CookieJar>>,
pub store: Option<Arc<HSTSStore>>,
pub connector: std::sync::OnceLock<SslConnector>,
}
impl Default for Client {
fn default() -> Self {
Self::new(ClientConfig::default())
}
}
impl Client {
pub fn new(config: ClientConfig) -> Self {
let jar = config.cookies.then(|| Arc::new(CookieJar::new().with_limits(config.limits.message)));
let store = config.hsts.then(|| Arc::new(HSTSStore::new().with_limits(config.limits.message)));
Self { config, jar, store, connector: std::sync::OnceLock::new() }
}
pub fn connector(&self) -> Result<&SslConnector, Error> {
if let Some(connector) = self.connector.get() {
return Ok(connector);
}
let versions: Vec<Version> = self.config.versions.iter().copied().filter(|version| version.transport() == TransportKind::Stream).collect();
if versions.is_empty() {
return Err(Error::Version("no configured version runs over a stream transport".into()));
}
let connector = self.config.tls.client(self.config.roots.as_deref().unwrap_or(&[]), &versions)?;
Ok(self.connector.get_or_init(|| connector))
}
pub fn ech(&self, host: &str) -> Option<&Vec<u8>> {
self.config.ech.get(host).or_else(|| self.config.ech.get("*"))
}
pub fn id(&self, host: &str, target: &Port) -> ConnectionID {
ConnectionID(Bytes::from(format!("{host}/{target:?}")))
}
pub fn authority(&self, host: &str, target: &Port) -> String {
let scheme = if self.config.secure { "https" } else { "http" };
match target {
Port::UDS(_) => host.to_owned(),
Port::TCP(port) | Port::QUIC(port) => URL::authority_of(scheme, host, *port),
}
}
pub fn request_finalizer(&self, authority: impl Into<Text>) -> crate::finalizer::RequestFinalizer {
crate::finalizer::RequestFinalizer::new(Some(authority.into()))
}
pub async fn connect(&self, host: &str, target: Port) -> Result<AnyConnection, Error> {
let id = self.id(host, &target);
let authority = self.authority(host, &target);
sync::Timeout::within(self.config.limits.connection_timeout, async move {
match target {
Port::QUIC(port) => self.connect_quic(host, port, id, &authority).await,
Port::TCP(port) => {
let transport = TcpStream::connect((host, port)).await?;
let _ = transport.set_nodelay(true);
self.connect_stream(host, Box::new(transport), id, &authority).await
}
Port::UDS(ref path) => {
let transport = UnixStream::connect(path).await?;
self.assemble(self.prior_version()?, Box::new(transport), id, &authority).await
}
}
})
.await?
}
pub async fn connect_stream(&self, host: &str, transport: Box<dyn Transport>, id: ConnectionID, authority: &str) -> Result<AnyConnection, Error> {
if !self.config.secure {
return self.assemble(self.prior_version()?, transport, id, authority).await;
}
self.connect_stream_tls(host, transport, id, authority).await
}
pub async fn connect_stream_tls(&self, host: &str, transport: Box<dyn Transport>, id: ConnectionID, authority: &str) -> Result<AnyConnection, Error> {
let versions: Vec<Version> = self.config.versions.iter().copied().filter(|version| version.transport() == TransportKind::Stream).collect();
if versions.is_empty() {
return Err(Error::Version("no configured version runs over a stream transport".into()));
}
let connector = self.connector()?;
let mut config = connector.configure().map_err(|err| Error::TLS(err.to_string()))?;
if let Some(list) = self.ech(host) {
config.set_ech_config_list(list).map_err(|err| Error::TLS(err.to_string()))?;
}
let stream = tokio_boring::connect(config, host, transport).await.map_err(|err| Error::TLS(err.to_string()))?;
let version = ALPN::negotiated(stream.ssl().selected_alpn_protocol(), &versions)?;
let security = Security::of(stream.ssl());
Ok(self.assemble(version, Box::new(stream), id, authority).await?.with_security(security))
}
pub async fn connect_quic(&self, host: &str, port: u16, id: ConnectionID, authority: &str) -> Result<AnyConnection, Error> {
let address = tokio::net::lookup_host((host, port))
.await?
.next()
.ok_or_else(|| Error::IO(std::io::Error::other(format!("{host} resolved to no address"))))?;
let bind = match address {
std::net::SocketAddr::V4(_) => std::net::SocketAddr::from(([0, 0, 0, 0], 0)),
std::net::SocketAddr::V6(_) => std::net::SocketAddr::from(([0u16; 8], 0)),
};
let udp = tokio::net::UdpSocket::bind(bind).await?;
udp.connect(address).await?;
let versions = Port::QUIC(port).offers(&self.config.versions);
let Some(version) = versions.first().copied() else {
return Err(Error::Version("no configured version runs over QUIC".into()));
};
match version {
Version::V3_0 => {
let config = quic::QUICConfig {
versions: versions.clone(),
idle_timeout: self.config.limits.message.read_timeout,
max_streams_bidi: None,
enable_dgram: false,
};
let hook = std::sync::Arc::new(quic::QUICClientTLS {
roots: self.config.roots.clone().unwrap_or_default(),
tls: self.config.tls.clone(),
});
let session = H3Session::new(crate::models::Role::UserAgent, id, self.config.limits.message);
let (connection, worker) = H3Connection::pair(session);
let connection = connection.with_request_finalizer(self.request_finalizer(authority));
let application = QUICApplication::new(versions, version, worker);
let guard = quic::QUICDialer::connect(host, udp, &config, hook, application).await?;
Ok(AnyConnection::H3(connection.with_guard(std::sync::Arc::new(guard))))
}
Version::V1_0 | Version::V1_1 | Version::V2_0 => Err(Error::Version(format!("{version} needs a stream transport"))),
}
}
pub fn prior_version(&self) -> Result<Version, Error> {
let mut stream = self.config.versions.iter().copied().filter(|version| version.transport() == TransportKind::Stream);
let Some(first) = stream.next() else {
return Err(Error::Version("no configured version runs over a stream transport".into()));
};
if stream.next().is_none() {
return Ok(first);
}
self.config
.versions
.iter()
.copied()
.find(|version| version.major() == 1)
.ok_or_else(|| Error::Version("several versions are configured and none of them is HTTP/1.x, which is the only one a peer that cannot negotiate may be assumed to speak".into()))
}
pub async fn assemble(&self, version: Version, transport: Box<dyn Transport>, id: ConnectionID, authority: &str) -> Result<AnyConnection, Error> {
let role = crate::models::Role::UserAgent;
let finalizer = self.request_finalizer(authority);
match version {
Version::V1_0 | Version::V1_1 => {
let connection = H1Connection::new(transport, role, id, self.config.limits.message).with_version(version).with_request_finalizer(finalizer);
Ok(AnyConnection::H1(connection))
}
Version::V2_0 => {
let connection = H2Connection::new(transport, role, id, self.config.limits.message).with_request_finalizer(finalizer);
Ok(AnyConnection::H2(connection))
}
Version::V3_0 => Err(Error::Version("HTTP/3 needs a QUIC port".into())),
}
}
pub async fn request(&self, connection: &mut AnyConnection, request: Message) -> Result<Message, Error> {
connection.send(request).await?;
loop {
let response = connection.receive().await?;
if !response.is_informational() {
return Ok(response);
}
}
}
pub fn only_quic(&self) -> bool {
!self.config.versions.is_empty() && self.config.versions.iter().all(|version| version.transport() == TransportKind::QUIC)
}
pub async fn open(&self, url: &URL) -> Result<AnyConnection, Error> {
let id = self.id(&url.host, &Port::TCP(url.port));
let authority = url.authority();
sync::Timeout::within(self.config.limits.connection_timeout, async move {
if !url.secure() {
let transport = TcpStream::connect((url.host.as_str(), url.port)).await?;
let _ = transport.set_nodelay(true);
return self.assemble(self.prior_version()?, Box::new(transport), id, &authority).await;
}
if self.only_quic() {
return self.connect_quic(&url.host, url.port, id, &authority).await;
}
let transport = TcpStream::connect((url.host.as_str(), url.port)).await?;
let _ = transport.set_nodelay(true);
self.connect_stream_tls(&url.host, Box::new(transport), id, &authority).await
})
.await?
}
pub fn apply_hsts(&self, url: &mut URL, now: Instant) {
if let Some(store) = &self.store
&& matches!(url.scheme.as_str(), "http" | "ws")
&& store.secure(&url.host, now)
{
url.scheme = if url.scheme == "http" { "https".to_owned() } else { "wss".to_owned() };
if url.port == 80 {
url.port = 443;
}
}
}
pub async fn fetch(&self, method: Method, url: &str, headers: Option<Headers>, body: Option<Body>) -> Result<Message, Error> {
let now = Instant::now();
let mut url = URL::parse(url)?;
self.apply_hsts(&mut url, now);
let mut connection = self.open(&url).await?;
let mut fields = headers.unwrap_or_default();
if let Some(jar) = &self.jar
&& !fields.contains("cookie")
&& let Some(cookie) = jar.cookie(&url, now)
{
fields.append("cookie", cookie);
}
let mut request = Message::request(method, url.target.clone(), connection.version());
request.security.secure = url.secure();
request.headers = Some(fields);
request.body = body;
let response = self.request(&mut connection, request).await?;
connection.close().await;
if let (Some(jar), Some(headers)) = (&self.jar, response.headers.as_ref()) {
let set: Vec<&str> = headers.get_all("set-cookie").collect();
if !set.is_empty() {
jar.learn(&url, &set, now);
}
}
if let (Some(store), Some(headers)) = (&self.store, response.headers.as_ref())
&& let Some(policy) = headers.get("strict-transport-security")
{
store.learn(&url.host, policy, url.secure(), now);
}
Ok(response)
}
pub async fn get(&self, url: &str) -> Result<Message, Error> {
self.fetch(Method::GET, url, None, None).await
}
pub async fn head(&self, url: &str) -> Result<Message, Error> {
self.fetch(Method::HEAD, url, None, None).await
}
pub async fn post(&self, url: &str, body: Body) -> Result<Message, Error> {
self.fetch(Method::POST, url, None, Some(body)).await
}
pub async fn put(&self, url: &str, body: Body) -> Result<Message, Error> {
self.fetch(Method::PUT, url, None, Some(body)).await
}
pub async fn delete(&self, url: &str) -> Result<Message, Error> {
self.fetch(Method::DELETE, url, None, None).await
}
pub async fn websocket(&self, url: &str) -> Result<crate::websocket::WebSocketConnection<Box<dyn Transport>>, Error> {
let mut url = URL::parse(url)?;
self.apply_hsts(&mut url, Instant::now());
let connection = self.open(&url).await?;
connection.open_websocket(&url.authority(), &url.target, self.config.limits.message).await
}
}