use crate::{
agent::{self, AgentBuilder},
config::*,
handler::{RequestHandler, RequestHandlerFuture, ResponseBodyReader},
middleware::Middleware,
Body, Error,
};
use futures_io::AsyncRead;
use futures_util::pin_mut;
use http::{Request, Response};
use lazy_static::lazy_static;
use std::{
fmt,
future::Future,
io,
iter::FromIterator,
net::SocketAddr,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
lazy_static! {
static ref USER_AGENT: String = format!(
"curl/{} isahc/{}",
curl::Version::get().version(),
env!("CARGO_PKG_VERSION")
);
}
#[derive(Default)]
pub struct HttpClientBuilder {
agent_builder: AgentBuilder,
defaults: http::Extensions,
middleware: Vec<Box<dyn Middleware>>,
}
impl HttpClientBuilder {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "cookies")]
pub fn cookies(self) -> Self {
self.middleware_impl(crate::cookies::CookieJar::default())
}
#[cfg(feature = "middleware-api")]
pub fn middleware(self, middleware: impl Middleware) -> Self {
self.middleware_impl(middleware)
}
#[allow(unused)]
fn middleware_impl(mut self, middleware: impl Middleware) -> Self {
self.middleware.push(Box::new(middleware));
self
}
pub fn max_connections(mut self, max: usize) -> Self {
self.agent_builder = self.agent_builder.max_connections(max);
self
}
pub fn max_connections_per_host(mut self, max: usize) -> Self {
self.agent_builder = self.agent_builder.max_connections_per_host(max);
self
}
pub fn connection_cache_size(mut self, size: usize) -> Self {
self.agent_builder = self.agent_builder.connection_cache_size(size);
self.defaults.insert(CloseConnection(size == 0));
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.defaults.insert(Timeout(timeout));
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.defaults.insert(ConnectTimeout(timeout));
self
}
pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
self.defaults.insert(policy);
self
}
pub fn auto_referer(mut self) -> Self {
self.defaults.insert(AutoReferer);
self
}
pub fn preferred_http_version(mut self, version: http::Version) -> Self {
self.defaults.insert(PreferredHttpVersion(version));
self
}
pub fn tcp_keepalive(mut self, interval: Duration) -> Self {
self.defaults.insert(TcpKeepAlive(interval));
self
}
pub fn tcp_nodelay(mut self) -> Self {
self.defaults.insert(TcpNoDelay);
self
}
pub fn proxy(mut self, proxy: http::Uri) -> Self {
self.defaults.insert(Proxy(proxy));
self
}
pub fn max_upload_speed(mut self, max: u64) -> Self {
self.defaults.insert(MaxUploadSpeed(max));
self
}
pub fn max_download_speed(mut self, max: u64) -> Self {
self.defaults.insert(MaxDownloadSpeed(max));
self
}
pub fn dns_cache(mut self, cache: impl Into<DnsCache>) -> Self {
self.defaults.insert(cache.into());
self
}
pub fn dns_servers(mut self, servers: impl IntoIterator<Item = SocketAddr>) -> Self {
self.defaults.insert(DnsServers::from_iter(servers));
self
}
pub fn ssl_ciphers(mut self, servers: impl IntoIterator<Item = String>) -> Self {
self.defaults.insert(SslCiphers::from_iter(servers));
self
}
pub fn ssl_client_certificate(mut self, certificate: ClientCertificate) -> Self {
self.defaults.insert(certificate);
self
}
pub fn danger_allow_unsafe_ssl(mut self, allow_unsafe: bool) -> Self {
self.defaults.insert(AllowUnsafeSsl(allow_unsafe));
self
}
pub fn metrics(mut self, enable: bool) -> Self {
self.defaults.insert(EnableMetrics(enable));
self
}
pub fn build(self) -> Result<HttpClient, Error> {
Ok(HttpClient {
agent: Arc::new(self.agent_builder.spawn()?),
defaults: self.defaults,
middleware: self.middleware,
})
}
}
impl fmt::Debug for HttpClientBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClientBuilder").finish()
}
}
pub struct HttpClient {
agent: Arc<agent::Handle>,
defaults: http::Extensions,
middleware: Vec<Box<dyn Middleware>>,
}
impl HttpClient {
pub fn new() -> Result<Self, Error> {
HttpClientBuilder::default().build()
}
pub(crate) fn shared() -> &'static Self {
lazy_static! {
static ref SHARED: HttpClient =
HttpClient::new().expect("shared client failed to initialize");
}
&SHARED
}
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder::default()
}
#[inline]
pub fn get<U>(&self, uri: U) -> Result<Response<Body>, Error>
where
http::Uri: http::HttpTryFrom<U>,
{
self.get_async(uri).join()
}
pub fn get_async<U>(&self, uri: U) -> ResponseFuture<'_>
where
http::Uri: http::HttpTryFrom<U>,
{
self.send_builder_async(http::Request::get(uri), Body::empty())
}
#[inline]
pub fn head<U>(&self, uri: U) -> Result<Response<Body>, Error>
where
http::Uri: http::HttpTryFrom<U>,
{
self.head_async(uri).join()
}
pub fn head_async<U>(&self, uri: U) -> ResponseFuture<'_>
where
http::Uri: http::HttpTryFrom<U>,
{
self.send_builder_async(http::Request::head(uri), Body::empty())
}
#[inline]
pub fn post<U>(&self, uri: U, body: impl Into<Body>) -> Result<Response<Body>, Error>
where
http::Uri: http::HttpTryFrom<U>,
{
self.post_async(uri, body).join()
}
pub fn post_async<U>(&self, uri: U, body: impl Into<Body>) -> ResponseFuture<'_>
where
http::Uri: http::HttpTryFrom<U>,
{
self.send_builder_async(http::Request::post(uri), body)
}
#[inline]
pub fn put<U>(&self, uri: U, body: impl Into<Body>) -> Result<Response<Body>, Error>
where
http::Uri: http::HttpTryFrom<U>,
{
self.put_async(uri, body).join()
}
pub fn put_async<U>(&self, uri: U, body: impl Into<Body>) -> ResponseFuture<'_>
where
http::Uri: http::HttpTryFrom<U>,
{
self.send_builder_async(http::Request::put(uri), body)
}
#[inline]
pub fn delete<U>(&self, uri: U) -> Result<Response<Body>, Error>
where
http::Uri: http::HttpTryFrom<U>,
{
self.delete_async(uri).join()
}
pub fn delete_async<U>(&self, uri: U) -> ResponseFuture<'_>
where
http::Uri: http::HttpTryFrom<U>,
{
self.send_builder_async(http::Request::delete(uri), Body::empty())
}
#[inline]
pub fn send<B: Into<Body>>(&self, request: Request<B>) -> Result<Response<Body>, Error> {
self.send_async(request).join()
}
pub fn send_async<B: Into<Body>>(&self, request: Request<B>) -> ResponseFuture<'_> {
let mut request = request.map(Into::into);
request
.headers_mut()
.entry(http::header::USER_AGENT)
.unwrap()
.or_insert(USER_AGENT.parse().unwrap());
for middleware in self.middleware.iter().rev() {
request = middleware.filter_request(request);
}
ResponseFuture {
client: self,
error: None,
request: Some(request),
inner: None,
}
}
fn send_builder_async(
&self,
mut builder: http::request::Builder,
body: impl Into<Body>,
) -> ResponseFuture<'_> {
match builder.body(body.into()) {
Ok(request) => self.send_async(request),
Err(e) => ResponseFuture {
client: self,
error: Some(e.into()),
request: None,
inner: None,
},
}
}
fn create_easy_handle(
&self,
request: Request<Body>,
) -> Result<(curl::easy::Easy2<RequestHandler>, RequestHandlerFuture), Error> {
let (mut parts, body) = request.into_parts();
let has_body = !body.is_empty();
let body_length = body.len();
let (handler, future) = RequestHandler::new(body);
let mut easy = curl::easy::Easy2::new(handler);
easy.verbose(log::log_enabled!(log::Level::Debug))?;
easy.signal(false)?;
macro_rules! set_opts {
($easy:expr, $extensions:expr, $defaults:expr, [$($option:ty,)*]) => {{
$(
if let Some(extension) = $extensions.get::<$option>().or_else(|| $defaults.get()) {
extension.set_opt($easy)?;
}
)*
}};
}
set_opts!(
&mut easy,
parts.extensions,
self.defaults,
[
Timeout,
ConnectTimeout,
TcpKeepAlive,
TcpNoDelay,
RedirectPolicy,
AutoReferer,
MaxUploadSpeed,
MaxDownloadSpeed,
PreferredHttpVersion,
Proxy,
DnsCache,
DnsServers,
SslCiphers,
ClientCertificate,
AllowUnsafeSsl,
CloseConnection,
EnableMetrics,
]
);
easy.accept_encoding(
parts
.headers
.get("Accept-Encoding")
.and_then(|value| value.to_str().ok())
.unwrap_or(""),
)?;
#[allow(indirect_structural_match)]
match (&parts.method, has_body) {
(&http::Method::GET, false) => {
easy.get(true)?;
}
(&http::Method::HEAD, has_body) => {
easy.upload(has_body)?;
easy.nobody(true)?;
easy.custom_request("HEAD")?;
}
(&http::Method::POST, _) => {
easy.post(true)?;
}
(&http::Method::PUT, _) => {
easy.upload(true)?;
}
(method, has_body) => {
easy.upload(has_body)?;
easy.custom_request(method.as_str())?;
}
}
easy.url(&parts.uri.to_string())?;
if has_body {
let body_length = parts
.headers
.get("Content-Length")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse().ok())
.or(body_length);
if let Some(len) = body_length {
if parts.method == http::Method::POST {
easy.post_field_size(len)?;
} else {
easy.in_filesize(len)?;
}
} else {
parts.headers.insert(
"Transfer-Encoding",
http::header::HeaderValue::from_static("chunked"),
);
}
}
let mut headers = curl::easy::List::new();
for (name, value) in parts.headers.iter() {
let header = format!("{}: {}", name.as_str(), value.to_str().unwrap());
headers.append(&header)?;
}
easy.http_headers(headers)?;
Ok((easy, future))
}
}
impl fmt::Debug for HttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClient").finish()
}
}
#[derive(Debug)]
pub struct ResponseFuture<'c> {
client: &'c HttpClient,
error: Option<Error>,
request: Option<Request<Body>>,
inner: Option<RequestHandlerFuture>,
}
impl<'c> ResponseFuture<'c> {
fn maybe_initialize(&mut self) -> Result<(), Error> {
if let Some(e) = self.error.take() {
return Err(e);
}
if let Some(request) = self.request.take() {
let (easy, future) = self.client.create_easy_handle(request)?;
self.client.agent.submit_request(easy)?;
self.inner = Some(future);
}
Ok(())
}
fn complete(
&self,
result: Result<Response<ResponseBodyReader>, Error>,
) -> Result<Response<Body>, Error> {
result.map(|response| {
let mut response = response.map(|reader| {
let body = ResponseBody {
inner: reader,
agent: self.client.agent.clone(),
};
match body.inner.len() {
Some(len) => Body::reader_sized(body, len),
None => Body::reader(body),
}
});
for middleware in self.client.middleware.iter() {
response = middleware.filter_response(response);
}
response
})
}
fn join(mut self) -> Result<Response<Body>, Error> {
self.maybe_initialize()?;
if let Some(inner) = self.inner.take() {
self.complete(inner.join())
} else {
panic!("join called after poll");
}
}
}
impl Future for ResponseFuture<'_> {
type Output = Result<Response<Body>, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.maybe_initialize()?;
if let Some(inner) = self.inner.as_mut() {
Pin::new(inner).poll(cx).map(|result| self.complete(result))
} else {
Poll::Pending
}
}
}
#[derive(Debug)]
struct ResponseBody {
inner: ResponseBodyReader,
agent: Arc<agent::Handle>,
}
impl AsyncRead for ResponseBody {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let inner = &mut self.inner;
pin_mut!(inner);
inner.poll_read(cx, buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
#[test]
fn traits() {
is_send::<HttpClient>();
is_sync::<HttpClient>();
is_send::<HttpClientBuilder>();
}
}