use std::sync::Arc;
use std::time::Instant;
use bytes::Bytes;
use tokio::net::{TcpStream, UnixStream};
use crate::api::common::{Limits, VERSIONS};
use crate::headers::CookieJar;
use crate::helpers::hsts::HstsStore;
use crate::models::{Body, ConnectionID, Headers, Message, Method, Port, Url, Version};
use crate::tls;
use crate::protocol::base::{AnyConnection, Connection, Transport};
use crate::protocol::common::{self, Error};
use crate::protocol::h1::H1Connection;
use crate::protocol::h2::H2Connection;
use crate::protocol::h3::{H3Connection, H3Session};
#[derive(Clone)]
pub struct ClientConfig {
pub versions: Vec<Version>,
pub limits: ClientLimits,
pub secure: bool,
pub roots: Option<Vec<Vec<u8>>>,
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,
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>>,
}
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 }
}
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 async fn connect(&self, host: &str, target: Port) -> Result<AnyConnection, Error> {
let id = self.id(host, &target);
common::within(self.config.limits.connection_timeout, async move {
match target {
Port::QUIC(port) => self.connect_quic(host, port, id).await,
Port::TCP(port) => {
let transport = TcpStream::connect((host, port)).await?;
self.connect_stream(host, Box::new(transport), id).await
}
Port::UDS(ref path) => {
let transport = UnixStream::connect(path).await?;
self.assemble(self.prior_version(), Box::new(transport), id).await
}
}
})
.await?
}
pub async fn connect_stream(&self, host: &str, transport: Box<dyn Transport>, id: ConnectionID) -> Result<AnyConnection, Error> {
if !self.config.secure {
return self.assemble(self.prior_version(), transport, id).await;
}
self.connect_stream_tls(host, transport, id).await
}
pub async fn connect_stream_tls(&self, host: &str, transport: Box<dyn Transport>, id: ConnectionID) -> Result<AnyConnection, Error> {
let versions: Vec<Version> = self.config.versions.iter().copied().filter(|version| version.major() != 3).collect();
if versions.is_empty() {
return Err(Error::Version("HTTP/3 needs a QUIC port".into()));
}
let connector = tls::client_config(self.config.roots.as_deref().unwrap_or(&[]), &versions)?;
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 = tls::negotiated(stream.ssl().selected_alpn_protocol(), &versions)?;
self.assemble(version, Box::new(stream), id).await
}
pub async fn connect_quic(&self, host: &str, port: u16, id: ConnectionID) -> 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 socket: tokio_quiche::socket::Socket<std::sync::Arc<tokio::net::UdpSocket>, std::sync::Arc<tokio::net::UdpSocket>> = udp.try_into().map_err(|err: std::io::Error| Error::Io(err))?;
let mut settings = tokio_quiche::settings::QuicSettings::default();
settings.alpn = vec![Version::V3_0.alpn().as_bytes().to_vec()];
settings.max_idle_timeout = crate::protocol::common::duration(self.config.limits.message.read_timeout);
settings.enable_dgram = false;
let hooks = tokio_quiche::settings::Hooks {
connection_hook: Some(std::sync::Arc::new(tls::QuicClientTls { roots: self.config.roots.clone().unwrap_or_default() })),
};
let params = tokio_quiche::ConnectionParams::new_client(
settings,
Some(tokio_quiche::settings::TlsCertificatePaths { cert: "", private_key: "", kind: tokio_quiche::settings::CertificateKind::X509 }),
hooks,
);
let session = H3Session::new(crate::models::Role::UserAgent, id, self.config.limits.message);
let (mut connection, worker) = H3Connection::pair(session, None);
let quic = tokio_quiche::quic::connect_with_config(socket, Some(host), ¶ms, worker)
.await
.map_err(|err| Error::Tls(err.to_string()))?;
connection.guard = Some(std::sync::Arc::new(quic));
Ok(AnyConnection::H3(connection))
}
pub fn prior_version(&self) -> Version {
match self.config.versions[..] {
[version] if version.major() != 3 => version,
_ => Version::V1_1,
}
}
pub async fn assemble(&self, version: Version, transport: Box<dyn Transport>, id: ConnectionID) -> Result<AnyConnection, Error> {
let role = crate::models::Role::UserAgent;
match version {
Version::V1_0 | Version::V1_1 => {
Ok(AnyConnection::H1(H1Connection::new(transport, role, id, self.config.limits.message)))
}
Version::V2_0 => Ok(AnyConnection::H2(H2Connection::new(transport, role, id, self.config.limits.message))),
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_h3(&self) -> bool {
self.config.versions.contains(&Version::V3_0) && self.config.versions.iter().all(|version| version.major() == 3)
}
pub async fn open(&self, url: &Url) -> Result<AnyConnection, Error> {
let id = self.id(&url.host, &Port::TCP(url.port));
common::within(self.config.limits.connection_timeout, async move {
if !url.secure() {
let transport = TcpStream::connect((url.host.as_str(), url.port)).await?;
return self.assemble(self.prior_version(), Box::new(transport), id).await;
}
if self.only_h3() {
return self.connect_quic(&url.host, url.port, id).await;
}
let transport = TcpStream::connect((url.host.as_str(), url.port)).await?;
self.connect_stream_tls(&url.host, Box::new(transport), id).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 !fields.contains("host") {
fields.append("host", url.authority());
}
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.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).await
}
}