diff --git a/Cargo.toml b/Cargo.toml index e1ab8ef2..53a7cb61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,13 +90,14 @@ features = ["std", "std-future"] env_logger = "0.9" flate2 = "1.0.3" indicatif = "0.15" +libc = "0.2.177" rayon = "1" +serde_json = "1" static_assertions = "1.1" structopt = "0.3" tempfile = "3.1" test-case = "2.0" tracing-subscriber = ">=0.2.12, <0.4.0" -serde_json = "1" [dev-dependencies.testserver] path = "testserver" diff --git a/build.rs b/build.rs index 9649004c..ef6989d1 100644 --- a/build.rs +++ b/build.rs @@ -3,6 +3,10 @@ use std::{env, error::Error}; fn main() -> Result<(), Box> { println!("cargo:rustc-env=ISAHC_FEATURES={}", get_feature_string()); + // Allow conditional compilation for tarpaulin and debug builds. + println!("cargo::rustc-check-cfg=cfg(tarpaulin)"); + println!("cargo::rustc-check-cfg=cfg(debug)"); + Ok(()) } diff --git a/src/client.rs b/src/client.rs index 835b1bfb..860741ba 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,7 +5,7 @@ use crate::{ body::{AsyncBody, Body}, config::{ client::ClientConfig, - request::{RequestConfig, SetOpt, WithRequestConfig}, + request::{NoCustomOpt, RequestConfig, SetOpt, WithRequestConfig}, *, }, default_headers::DefaultHeadersInterceptor, @@ -71,9 +71,9 @@ static USER_AGENT: Lazy = Lazy::new(|| { /// # Ok::<(), isahc::Error>(()) /// ``` #[must_use = "builders have no effect if unused"] -pub struct HttpClientBuilder { +pub struct HttpClientBuilder { agent_builder: AgentBuilder, - client_config: ClientConfig, + client_config: ClientConfig, request_config: RequestConfig, interceptors: Vec, default_headers: HeaderMap, @@ -83,13 +83,13 @@ pub struct HttpClientBuilder { cookie_jar: Option, } -impl Default for HttpClientBuilder { +impl Default for HttpClientBuilder { fn default() -> Self { Self::new() } } -impl HttpClientBuilder { +impl HttpClientBuilder { /// Create a new builder for building a custom client. All configuration /// will start out with the default values. /// @@ -114,6 +114,75 @@ impl HttpClientBuilder { } } + /// Set custom curl options. + /// + /// # Examples + /// + /// ``` + /// use std::ptr; + /// use std::error; + /// use std::fmt; + /// use std::mem; + /// use std::ffi; + /// + /// use isahc::{HttpClient, SetOpt, prelude::*}; + /// + /// /// Helper functions for raising nice errors taken from the `curl` crate. + /// fn cvt(easy: &mut curl::easy::Easy2, rc: curl_sys::CURLcode) -> Result<(), curl::Error> { + /// if rc == curl_sys::CURLE_OK { + /// return Ok(()); + /// } + /// let mut err = curl::Error::new(rc); + /// if let Some(msg) = easy.take_error_buf() { + /// err.set_extra(msg); + /// } + /// Err(err) + /// } + /// + /// /// Import the base function for the open socket callback from the `curl` crate. + /// extern "C" { + /// pub fn opensocket_cb(data: *mut libc::c_void, purpose: curl_sys::curlsocktype, address: *mut curl_sys::curl_sockaddr) -> curl_sys::curl_socket_t; + /// } + /// + /// /// Wrap the callback to perform our custom logic before opening the socket. + /// extern "C" fn opensocket_cb_custom(data: *mut libc::c_void, purpose: curl_sys::curlsocktype, address: *mut curl_sys::curl_sockaddr) -> curl_sys::curl_socket_t { + /// // TODO: Wrapper the open socket callback to perform some custom logic. + /// unsafe { opensocket_cb(data, purpose, address) } + /// } + /// + /// /// Implement a custom curl option that modifies the open socket function. + /// #[derive(Debug)] + /// struct CustomOpt {} + /// impl SetOpt for CustomOpt { + /// fn set_opt(&self, easy: &mut curl::easy::Easy2) -> Result<(), curl::Error> { + /// let opt = curl_sys::CURLOPT_OPENSOCKETFUNCTION; + /// let cb: curl_sys::curl_opensocket_callback = opensocket_cb_custom; + /// unsafe { cvt(easy, curl_sys::curl_easy_setopt(easy.raw(), opt, cb))?; } + /// Ok(()) + /// } + /// } + /// + /// let client = HttpClient::builder() + /// .custom_curl_options(CustomOpt {}) + /// .build()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn custom_curl_options(self, custom_curl_options: S) -> HttpClientBuilder { + // Similar to the dns_cache option, this operation actually affects all + // requests in a multi handle so we do not expose it per-request to + // avoid confusing behavior. + HttpClientBuilder{ + agent_builder: self.agent_builder, + client_config: ClientConfig { connection_cache_ttl: self.client_config.connection_cache_ttl, close_connections: self.client_config.close_connections, dns_cache: self.client_config.dns_cache, dns_resolve: self.client_config.dns_resolve, custom_curl_options: Some(custom_curl_options), }, + request_config: self.request_config, + interceptors: self.interceptors, + default_headers: self.default_headers, + error: self.error, + } + } +} + +impl HttpClientBuilder { /// Enable persistent cookie handling for all requests using this client /// using a shared cookie jar. /// @@ -436,7 +505,7 @@ impl HttpClientBuilder { /// /// If the client fails to initialize, an error will be returned. #[allow(unused_mut)] - pub fn build(mut self) -> Result { + pub fn build(mut self) -> Result, Error> { if let Some(err) = self.error { return Err(err); } @@ -483,7 +552,7 @@ impl HttpClientBuilder { } } -impl Configurable for HttpClientBuilder { +impl Configurable for HttpClientBuilder { #[cfg(feature = "cookies")] fn cookie_jar(mut self, cookie_jar: crate::cookies::CookieJar) -> Self { self.cookie_jar = Some(cookie_jar); @@ -491,7 +560,7 @@ impl Configurable for HttpClientBuilder { } } -impl WithRequestConfig for HttpClientBuilder { +impl WithRequestConfig for HttpClientBuilder { #[inline] fn with_config(mut self, f: impl FnOnce(&mut RequestConfig)) -> Self { f(&mut self.request_config); @@ -499,7 +568,7 @@ impl WithRequestConfig for HttpClientBuilder { } } -impl fmt::Debug for HttpClientBuilder { +impl fmt::Debug for HttpClientBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpClientBuilder").finish() } @@ -594,17 +663,24 @@ impl<'a, K: Copy, V: Copy> HeaderPair for &'a (K, V) { /// /// See the documentation on [`HttpClientBuilder`] for a comprehensive look at /// what can be configured. -#[derive(Clone)] -pub struct HttpClient { - inner: Arc, +pub struct HttpClient { + inner: Arc>, } -struct Inner { +impl Clone for HttpClient { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +struct Inner { /// This is how we talk to our background agent thread. agent: agent::Handle, /// Client-wide request configuration. - client_config: ClientConfig, + client_config: ClientConfig, /// Default request configuration to use if not specified in a request. request_config: RequestConfig, @@ -617,7 +693,7 @@ struct Inner { cookie_jar: Option, } -impl HttpClient { +impl HttpClient { /// Create a new HTTP client using the default configuration. /// /// If the client fails to initialize, an error will be returned. @@ -629,17 +705,19 @@ impl HttpClient { /// /// TODO: Stabilize. pub(crate) fn shared() -> &'static Self { - static SHARED: Lazy = + static SHARED: Lazy> = Lazy::new(|| HttpClient::new().expect("shared client failed to initialize")); &SHARED } /// Create a new [`HttpClientBuilder`] for building a custom client. - pub fn builder() -> HttpClientBuilder { + pub fn builder() -> HttpClientBuilder { HttpClientBuilder::default() } +} +impl HttpClient { /// Get the configured cookie jar for this HTTP client, if any. /// /// # Availability @@ -1064,23 +1142,20 @@ impl HttpClient { easy.signal(false)?; - let request_config = request - .extensions() - .get::() - .unwrap(); + let request_config = request.extensions().get::().unwrap(); request_config.set_opt(&mut easy)?; self.inner.client_config.set_opt(&mut easy)?; // Check if we need to disable the Expect header. - let disable_expect_header = request_config.expect_continue + let disable_expect_header = request_config + .expect_continue .as_ref() .map(|x| x.is_disabled()) .unwrap_or_default(); // Set the HTTP method to use. Curl ties in behavior with the request // method, so we need to configure this carefully. - #[allow(indirect_structural_match)] match (request.method(), has_body) { // Normal GET request. (&http::Method::GET, false) => { @@ -1161,7 +1236,7 @@ impl HttpClient { } } -impl crate::interceptor::Invoke for &HttpClient { +impl crate::interceptor::Invoke for &HttpClient { fn invoke( &self, mut request: Request, @@ -1236,7 +1311,7 @@ impl crate::interceptor::Invoke for &HttpClient { } } -impl fmt::Debug for HttpClient { +impl fmt::Debug for HttpClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpClient").finish() } @@ -1275,12 +1350,12 @@ impl<'c> fmt::Debug for ResponseFuture<'c> { /// Response body stream. Holds a reference to the agent to ensure it is kept /// alive until at least this transfer is complete. -struct ResponseBody { +struct ResponseBody { inner: ResponseBodyReader, - _client: HttpClient, + _client: HttpClient, } -impl AsyncRead for ResponseBody { +impl AsyncRead for ResponseBody { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -1319,8 +1394,8 @@ fn uri_to_string(uri: &http::Uri) -> String { mod tests { use super::*; - static_assertions::assert_impl_all!(HttpClient: Send, Sync); - static_assertions::assert_impl_all!(HttpClientBuilder: Send); + static_assertions::assert_impl_all!(HttpClient::: Send, Sync); + static_assertions::assert_impl_all!(HttpClientBuilder::: Send); #[test] fn test_default_header() { @@ -1335,7 +1410,8 @@ mod tests { #[test] fn test_default_headers_mut() { - let mut builder = HttpClientBuilder::new().default_header("some-key", "some-value"); + let mut builder = + HttpClientBuilder::new().default_header("some-key", "some-value"); let headers_map = &mut builder.default_headers; assert!(headers_map.len() == 1); diff --git a/src/config/client.rs b/src/config/client.rs index 19fc2b94..7d2c5bd1 100644 --- a/src/config/client.rs +++ b/src/config/client.rs @@ -1,18 +1,33 @@ +use crate::NoCustomOpt; + use super::{ dns::{DnsCache, ResolveMap}, request::SetOpt, }; -use std::time::Duration; +use std::{fmt::Debug, time::Duration}; -#[derive(Debug, Default)] -pub(crate) struct ClientConfig { +#[derive(Debug)] +pub(crate) struct ClientConfig { pub(crate) connection_cache_ttl: Option, pub(crate) close_connections: bool, pub(crate) dns_cache: Option, pub(crate) dns_resolve: Option, + pub(crate) custom_curl_options: Option, } -impl SetOpt for ClientConfig { +impl Default for ClientConfig { + fn default() -> Self { + Self { + connection_cache_ttl: Default::default(), + close_connections: Default::default(), + dns_cache: Default::default(), + dns_resolve: Default::default(), + custom_curl_options: None, + } + } +} + +impl SetOpt for ClientConfig { fn set_opt(&self, easy: &mut curl::easy::Easy2) -> Result<(), curl::Error> { if let Some(ttl) = self.connection_cache_ttl { easy.maxage_conn(ttl)?; @@ -26,6 +41,10 @@ impl SetOpt for ClientConfig { map.set_opt(easy)?; } + if let Some(custom_curl_options) = self.custom_curl_options.as_ref() { + custom_curl_options.set_opt(easy)?; + } + easy.forbid_reuse(self.close_connections) } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 1f8dc0db..bac58bdc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -13,7 +13,7 @@ // to update the client code to apply the option when configuring an easy // handle. -use self::{proxy::Proxy, request::SetOpt}; +use self::{proxy::Proxy}; use crate::{ auth::{Authentication, Credentials}, is_http_version_supported, @@ -32,6 +32,7 @@ pub(crate) mod ssl; pub use dial::{Dialer, DialerParseError}; pub use dns::{DnsCache, ResolveMap}; pub use redirect::RedirectPolicy; +pub use request::SetOpt; pub use ssl::{CaCertificate, ClientCertificate, PrivateKey, SslOption}; /// Provides additional methods when building a request for configuring various diff --git a/src/config/request.rs b/src/config/request.rs index 6e6551f1..08903a7c 100644 --- a/src/config/request.rs +++ b/src/config/request.rs @@ -2,6 +2,7 @@ use super::{proxy::Proxy, *}; use curl::easy::Easy2; +use std::fmt::Debug; /// Base trait for any object that can be configured for requests, such as an /// HTTP request builder or an HTTP client. @@ -12,11 +13,21 @@ pub trait WithRequestConfig: Sized { } /// A helper trait for applying a configuration value to a given curl handle. -pub(crate) trait SetOpt { +pub trait SetOpt: Debug + Send + Sync { /// Apply this configuration property to the given curl handle. fn set_opt(&self, easy: &mut Easy2) -> Result<(), curl::Error>; } +/// An type marker that ensures that we carry no custom `SetOpt` code around. +#[derive(Debug)] +pub enum NoCustomOpt {} + +impl SetOpt for NoCustomOpt { + fn set_opt(&self, _easy: &mut Easy2) -> Result<(), curl::Error> { + Ok(()) + } +} + // Define this struct inside a macro to reduce some boilerplate. macro_rules! define_request_config { ($($field:ident: $t:ty,)*) => { diff --git a/src/lib.rs b/src/lib.rs index 8795881a..dacf9b4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -272,6 +272,7 @@ pub mod interceptor; pub(crate) mod interceptor; pub use crate::{ + config::request::{SetOpt, NoCustomOpt}, body::{AsyncBody, Body}, client::{HttpClient, HttpClientBuilder, ResponseFuture}, error::Error, diff --git a/tests/headers.rs b/tests/headers.rs index 07049d55..d5abae06 100644 --- a/tests/headers.rs +++ b/tests/headers.rs @@ -1,5 +1,5 @@ use futures_lite::future::block_on; -use isahc::{prelude::*, HttpClient, Request}; +use isahc::{HttpClient, Request, prelude::*}; use std::{ io::{self, Write}, net::{Shutdown, TcpListener, TcpStream}, diff --git a/tests/metrics.rs b/tests/metrics.rs index cc6cae56..689b4922 100644 --- a/tests/metrics.rs +++ b/tests/metrics.rs @@ -1,4 +1,4 @@ -use isahc::{prelude::*, HttpClient, Request}; +use isahc::{HttpClient, Request, prelude::*}; use std::{io, time::Duration}; use testserver::mock; @@ -19,7 +19,10 @@ fn enabling_metrics_causes_metrics_to_be_collected() { body: "hello world", }; - let client = HttpClient::builder().metrics(true).build().unwrap(); + let client = HttpClient::builder() + .metrics(true) + .build() + .unwrap(); let mut response = client .send(Request::post(m.url()).body("hello server").unwrap()) diff --git a/tests/redirects.rs b/tests/redirects.rs index 814db254..b786e791 100644 --- a/tests/redirects.rs +++ b/tests/redirects.rs @@ -1,4 +1,4 @@ -use isahc::{config::RedirectPolicy, prelude::*, Body, HttpClient, Request}; +use isahc::{Body, HttpClient, Request, config::RedirectPolicy, prelude::*}; use test_case::test_case; use testserver::mock; diff --git a/testserver/src/responder.rs b/testserver/src/responder.rs index 9f8bfcd3..489eef54 100644 --- a/testserver/src/responder.rs +++ b/testserver/src/responder.rs @@ -51,12 +51,3 @@ pub trait Responder: Send + Sync + 'static { /// Respond to a request. fn respond(&self, ctx: &mut RequestContext<'_>); } - -/// Simple responder that returns a general response. -pub struct DefaultResponder; - -impl Responder for DefaultResponder { - fn respond(&self, ctx: &mut RequestContext<'_>) { - ctx.send(Response::default()); - } -}