diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5cebd09..1ba533f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,6 +58,7 @@ jobs: - name: Doc tests run: | cargo test --doc + cargo test --doc --no-default-features cargo test --doc --all-features - name: Test with all features enabled run: cargo test --all-features diff --git a/examples/github.rs b/examples/github.rs index 29f9a6f..d22043c 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -14,7 +14,7 @@ //! use oauth2::basic::BasicClient; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, diff --git a/examples/github_async.rs b/examples/github_async.rs index b66863b..1d0f7bd 100644 --- a/examples/github_async.rs +++ b/examples/github_async.rs @@ -14,7 +14,7 @@ //! use oauth2::basic::BasicClient; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, diff --git a/examples/google.rs b/examples/google.rs index 04bb506..f2f91fc 100644 --- a/examples/google.rs +++ b/examples/google.rs @@ -13,7 +13,7 @@ //! ...and follow the instructions. //! -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{basic::BasicClient, StandardRevocableToken, TokenResponse}; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, diff --git a/examples/google_devicecode.rs b/examples/google_devicecode.rs index 0924192..57d7fa4 100644 --- a/examples/google_devicecode.rs +++ b/examples/google_devicecode.rs @@ -14,7 +14,7 @@ //! use oauth2::basic::BasicClient; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{ AuthType, AuthUrl, ClientId, ClientSecret, DeviceAuthorizationResponse, DeviceAuthorizationUrl, ExtraDeviceAuthorizationFields, Scope, TokenUrl, diff --git a/examples/microsoft_devicecode_tenant_user.rs b/examples/microsoft_devicecode_tenant_user.rs index 42e3052..35536e6 100644 --- a/examples/microsoft_devicecode_tenant_user.rs +++ b/examples/microsoft_devicecode_tenant_user.rs @@ -1,5 +1,5 @@ use oauth2::basic::BasicClient; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::StandardDeviceAuthorizationResponse; use oauth2::{AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, TokenUrl}; diff --git a/examples/msgraph.rs b/examples/msgraph.rs index bfba120..ffdeac0 100644 --- a/examples/msgraph.rs +++ b/examples/msgraph.rs @@ -21,7 +21,7 @@ //! use oauth2::basic::BasicClient; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{ AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenUrl, diff --git a/examples/wunderlist.rs b/examples/wunderlist.rs index d676d43..0fbf6f3 100644 --- a/examples/wunderlist.rs +++ b/examples/wunderlist.rs @@ -18,7 +18,7 @@ use oauth2::basic::{ BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenType, }; -use oauth2::reqwest::reqwest; +use oauth2::reqwest; use oauth2::{ AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, EndpointNotSet, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, diff --git a/src/curl.rs b/src/curl.rs deleted file mode 100644 index 2dbf7dd..0000000 --- a/src/curl.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{HttpRequest, HttpResponse, SyncHttpClient}; - -use curl::easy::Easy; -use http::header::{HeaderValue, CONTENT_TYPE}; -use http::method::Method; -use http::status::StatusCode; - -use std::io::Read; - -pub use curl; - -/// Error type returned by failed curl HTTP requests. -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Error returned by curl crate. - #[error("curl request failed")] - Curl(#[from] curl::Error), - /// Non-curl HTTP error. - #[error("HTTP error")] - Http(#[from] http::Error), - /// Other error. - #[error("Other error: {}", _0)] - Other(String), -} - -/// A synchronous HTTP client using [`curl`]. -pub struct CurlHttpClient; -impl SyncHttpClient for CurlHttpClient { - type Error = Error; - - fn call(&self, request: HttpRequest) -> Result { - let mut easy = Easy::new(); - easy.url(&request.uri().to_string()[..])?; - - let mut headers = curl::easy::List::new(); - for (name, value) in request.headers() { - headers.append(&format!( - "{}: {}", - name, - // TODO: Unnecessary fallibility, curl uses a CString under the hood - value.to_str().map_err(|_| Error::Other(format!( - "invalid {} header value {:?}", - name, - value.as_bytes() - )))? - ))? - } - - easy.http_headers(headers)?; - - if let Method::POST = *request.method() { - easy.post(true)?; - easy.post_field_size(request.body().len() as u64)?; - } else { - assert_eq!(*request.method(), Method::GET); - } - - let mut form_slice = &request.body()[..]; - let mut data = Vec::new(); - { - let mut transfer = easy.transfer(); - - transfer.read_function(|buf| Ok(form_slice.read(buf).unwrap_or(0)))?; - - transfer.write_function(|new_data| { - data.extend_from_slice(new_data); - Ok(new_data.len()) - })?; - - transfer.perform()?; - } - - let mut builder = http::Response::builder() - .status(StatusCode::from_u16(easy.response_code()? as u16).map_err(http::Error::from)?); - - if let Some(content_type) = easy - .content_type()? - .map(HeaderValue::from_str) - .transpose() - .map_err(http::Error::from)? - { - builder = builder.header(CONTENT_TYPE, content_type); - } - - builder.body(data).map_err(Error::Http) - } -} diff --git a/src/curl_client.rs b/src/curl_client.rs new file mode 100644 index 0000000..fc17e3c --- /dev/null +++ b/src/curl_client.rs @@ -0,0 +1,80 @@ +use crate::{HttpClientError, HttpRequest, HttpResponse, SyncHttpClient}; + +use curl::easy::Easy; +use http::header::{HeaderValue, CONTENT_TYPE}; +use http::method::Method; +use http::status::StatusCode; + +use std::io::Read; + +/// A synchronous HTTP client using [`curl`]. +pub struct CurlHttpClient; +impl SyncHttpClient for CurlHttpClient { + type Error = HttpClientError; + + fn call(&self, request: HttpRequest) -> Result { + let mut easy = Easy::new(); + easy.url(&request.uri().to_string()[..]).map_err(Box::new)?; + + let mut headers = curl::easy::List::new(); + for (name, value) in request.headers() { + headers + .append(&format!( + "{}: {}", + name, + // TODO: Unnecessary fallibility, curl uses a CString under the hood + value.to_str().map_err(|_| HttpClientError::Other(format!( + "invalid `{name}` header value {:?}", + value.as_bytes() + )))? + )) + .map_err(Box::new)? + } + + easy.http_headers(headers).map_err(Box::new)?; + + if let Method::POST = *request.method() { + easy.post(true).map_err(Box::new)?; + easy.post_field_size(request.body().len() as u64) + .map_err(Box::new)?; + } else { + assert_eq!(*request.method(), Method::GET); + } + + let mut form_slice = &request.body()[..]; + let mut data = Vec::new(); + { + let mut transfer = easy.transfer(); + + transfer + .read_function(|buf| Ok(form_slice.read(buf).unwrap_or(0))) + .map_err(Box::new)?; + + transfer + .write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + }) + .map_err(Box::new)?; + + transfer.perform().map_err(Box::new)?; + } + + let mut builder = http::Response::builder().status( + StatusCode::from_u16(easy.response_code().map_err(Box::new)? as u16) + .map_err(http::Error::from)?, + ); + + if let Some(content_type) = easy + .content_type() + .map_err(Box::new)? + .map(HeaderValue::from_str) + .transpose() + .map_err(http::Error::from)? + { + builder = builder.header(CONTENT_TYPE, content_type); + } + + builder.body(data).map_err(HttpClientError::Http) + } +} diff --git a/src/lib.rs b/src/lib.rs index d85a544..1c3aede 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,39 +32,38 @@ //! To prevent //! [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) //! vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, -//! use [`redirect::Policy::none`](::reqwest::redirect::Policy::none) when using -//! [`reqwest`](::reqwest), or [`redirects(0)`](::ureq::AgentBuilder::redirects) when using -//! [`ureq`](::ureq). +//! use [`redirect::Policy::none`](reqwest::redirect::Policy::none) when using +//! [`reqwest`], or [`redirects(0)`](ureq::AgentBuilder::redirects) when using [`ureq`]. //! //! ## HTTP Clients //! //! For the HTTP client modes described above, the following HTTP client implementations can be //! used: -//! * **[`reqwest`](::reqwest)** +//! * **[`reqwest`](reqwest)** //! //! The `reqwest` HTTP client supports both the synchronous and asynchronous modes and is enabled //! by default. //! -//! Synchronous client: [`reqwest::blocking::Client`](::reqwest::blocking::Client) (requires the +//! Synchronous client: [`reqwest::blocking::Client`] (requires the //! `reqwest-blocking` feature flag) //! -//! Asynchronous client: [`reqwest::Client`](::reqwest::Client) (requires either the +//! Asynchronous client: [`reqwest::Client`] (requires either the //! `reqwest` or `reqwest-blocking` feature flags) //! -//! * **[`curl`](::curl)** +//! * **[`curl`](curl)** //! //! The `curl` HTTP client only supports the synchronous HTTP client mode and can be enabled in //! `Cargo.toml` via the `curl` feature flag. //! -//! Synchronous client: [`oauth2::curl::CurlHttpClient`](crate::curl::CurlHttpClient) +//! Synchronous client: [`oauth2::CurlHttpClient`](CurlHttpClient) //! -//! * **[`ureq`](::ureq)** +//! * **[`ureq`](ureq)** //! //! The `ureq` HTTP client is a simple HTTP client with minimal dependencies. It only supports //! the synchronous HTTP client mode and can be enabled in `Cargo.toml` via the `ureq` feature //! flag. //! -//! Synchronous client: [`ureq::Agent`](::ureq::Agent) +//! Synchronous client: [`ureq::Agent`] //! //! * **Custom** //! @@ -134,7 +133,7 @@ //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] -//! use oauth2::reqwest::reqwest; +//! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] @@ -207,7 +206,7 @@ //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest")] -//! use oauth2::reqwest::reqwest; +//! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest")] @@ -324,7 +323,7 @@ //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] -//! use oauth2::reqwest::reqwest; +//! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] @@ -370,7 +369,7 @@ //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] -//! use oauth2::reqwest::reqwest; +//! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] @@ -417,7 +416,7 @@ //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] -//! use oauth2::reqwest::reqwest; +//! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] @@ -481,7 +480,7 @@ mod code; /// HTTP client backed by the [curl](https://crates.io/crates/curl) crate. /// Requires "curl" feature. #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] -pub mod curl; +mod curl_client; #[cfg(all(feature = "curl", target_arch = "wasm32"))] compile_error!("wasm32 is not supported with the `curl` feature. Use the `reqwest` backend or a custom backend for wasm32 support"); @@ -502,7 +501,7 @@ mod introspection; /// HTTP client backed by the [reqwest](https://crates.io/crates/reqwest) crate. /// Requires "reqwest" feature. #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] -pub mod reqwest; +mod reqwest_client; /// OAuth 2.0 Token Revocation implementation /// ([RFC 7009](https://tools.ietf.org/html/rfc7009)). @@ -518,10 +517,12 @@ mod types; /// HTTP client backed by the [ureq](https://crates.io/crates/ureq) crate. /// Requires "ureq" feature. #[cfg(feature = "ureq")] -pub mod ureq; +mod ureq_client; pub use crate::client::{Client, EndpointMaybeSet, EndpointNotSet, EndpointSet, EndpointState}; pub use crate::code::AuthorizationRequest; +#[cfg(all(feature = "curl", not(target_arch = "wasm32")))] +pub use crate::curl_client::CurlHttpClient; pub use crate::devicecode::{ DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationRequestFuture, DeviceAuthorizationResponse, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, @@ -550,11 +551,21 @@ pub use crate::types::{ ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, RevocationUrl, Scope, TokenUrl, UserCode, VerificationUriComplete, }; +use std::error::Error; /// Public re-exports of types used for HTTP client interfaces. pub use http; pub use url; +#[cfg(all(feature = "curl", not(target_arch = "wasm32")))] +pub use ::curl; + +#[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] +pub use ::reqwest; + +#[cfg(feature = "ureq")] +pub use ::ureq; + const CONTENT_TYPE_JSON: &str = "application/json"; const CONTENT_TYPE_FORMENCODED: &str = "application/x-www-form-urlencoded"; @@ -583,3 +594,24 @@ pub enum AuthType { /// The client_id and client_secret will be included using the basic auth authentication scheme. BasicAuth, } + +/// Error type returned by built-in HTTP clients when requests fail. +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum HttpClientError +where + RE: Error + 'static, +{ + /// Error returned by reqwest crate. + #[error("request failed")] + Reqwest(#[from] Box), + /// Non-reqwest HTTP error. + #[error("HTTP error")] + Http(#[from] http::Error), + /// I/O error. + #[error("I/O error")] + Io(#[from] std::io::Error), + /// Other error. + #[error("{}", _0)] + Other(String), +} diff --git a/src/reqwest.rs b/src/reqwest_client.rs similarity index 61% rename from src/reqwest.rs rename to src/reqwest_client.rs index 8fe8912..594e49d 100644 --- a/src/reqwest.rs +++ b/src/reqwest_client.rs @@ -1,36 +1,20 @@ -use crate::{AsyncHttpClient, HttpRequest, HttpResponse}; - -use thiserror::Error; +use crate::{AsyncHttpClient, HttpClientError, HttpRequest, HttpResponse}; use std::future::Future; use std::pin::Pin; -pub use reqwest; - -/// Error type returned by failed reqwest HTTP requests. -#[non_exhaustive] -#[derive(Debug, Error)] -pub enum Error { - /// Error returned by reqwest crate. - #[error("request failed")] - Reqwest(#[from] reqwest::Error), - /// Non-reqwest HTTP error. - #[error("HTTP error")] - Http(#[from] http::Error), - /// I/O error. - #[error("I/O error")] - Io(#[from] std::io::Error), -} - impl<'c> AsyncHttpClient<'c> for reqwest::Client { - type Error = Error; + type Error = HttpClientError; fn call( &'c self, request: HttpRequest, - ) -> Pin> + 'c>> { + ) -> Pin> + 'c>> { Box::pin(async move { - let response = self.execute(request.try_into()?).await?; + let response = self + .execute(request.try_into().map_err(Box::new)?) + .await + .map_err(Box::new)?; // This should be simpler once https://github.com/seanmonstar/reqwest/pull/2060 is // merged. @@ -46,18 +30,20 @@ impl<'c> AsyncHttpClient<'c> for reqwest::Client { } builder - .body(response.bytes().await?.to_vec()) - .map_err(Error::Http) + .body(response.bytes().await.map_err(Box::new)?.to_vec()) + .map_err(HttpClientError::Http) }) } } #[cfg(all(feature = "reqwest-blocking", not(target_arch = "wasm32")))] impl crate::SyncHttpClient for reqwest::blocking::Client { - type Error = Error; + type Error = HttpClientError; fn call(&self, request: HttpRequest) -> Result { - let mut response = self.execute(request.try_into()?)?; + let mut response = self + .execute(request.try_into().map_err(Box::new)?) + .map_err(Box::new)?; // This should be simpler once https://github.com/seanmonstar/reqwest/pull/2060 is // merged. @@ -72,6 +58,6 @@ impl crate::SyncHttpClient for reqwest::blocking::Client { let mut body = Vec::new(); ::read_to_end(&mut response, &mut body)?; - builder.body(body).map_err(Error::Http) + builder.body(body).map_err(HttpClientError::Http) } } diff --git a/src/tests.rs b/src/tests.rs index 708ead2..1dd0262 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,8 +6,8 @@ use crate::{ ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationUrl, DeviceCode, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, - EmptyExtraTokenFields, EndUserVerificationUrl, EndpointNotSet, EndpointSet, HttpRequest, - HttpResponse, PasswordTokenRequest, PkceCodeChallenge, PkceCodeChallengeMethod, + EmptyExtraTokenFields, EndUserVerificationUrl, EndpointNotSet, EndpointSet, HttpClientError, + HttpRequest, HttpResponse, PasswordTokenRequest, PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, RefreshTokenRequest, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, Scope, StandardDeviceAuthorizationResponse, StandardErrorResponse, StandardRevocableToken, @@ -332,7 +332,9 @@ fn test_send_sync_impl() { is_sync_and_send::(); #[cfg(feature = "curl")] - is_sync_and_send::(); + is_sync_and_send::>(); #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] - is_sync_and_send::(); + is_sync_and_send::>(); + #[cfg(feature = "ureq")] + is_sync_and_send::>(); } diff --git a/src/ureq.rs b/src/ureq_client.rs similarity index 67% rename from src/ureq.rs rename to src/ureq_client.rs index 980d359..5c66d57 100644 --- a/src/ureq.rs +++ b/src/ureq_client.rs @@ -1,4 +1,4 @@ -use crate::{HttpRequest, HttpResponse}; +use crate::{HttpClientError, HttpRequest, HttpResponse}; use http::{ header::{HeaderValue, CONTENT_TYPE}, @@ -8,28 +8,8 @@ use http::{ use std::io::Read; -pub use ureq; - -/// Error type returned by failed ureq HTTP requests. -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Non-ureq HTTP error. - #[error("HTTP error")] - Http(#[from] http::Error), - /// IO error - #[error("IO error")] - IO(#[from] std::io::Error), - /// Other error. - #[error("Other error: {}", _0)] - Other(String), - /// Error returned by ureq crate. - // boxed due to https://github.com/algesten/ureq/issues/296 - #[error("ureq request failed")] - Ureq(#[from] Box), -} - impl crate::SyncHttpClient for ureq::Agent { - type Error = Error; + type Error = HttpClientError; fn call(&self, request: HttpRequest) -> Result { let mut req = if *request.method() == Method::POST { @@ -45,9 +25,8 @@ impl crate::SyncHttpClient for ureq::Agent { // TODO: In newer `ureq` it should be easier to convert arbitrary byte sequences // without unnecessary UTF-8 fallibility here. value.to_str().map_err(|_| { - Error::Other(format!( - "invalid {} header value {:?}", - name, + HttpClientError::Other(format!( + "invalid `{name}` header value {:?}", value.as_bytes() )) })?, @@ -76,6 +55,6 @@ impl crate::SyncHttpClient for ureq::Agent { let mut body = Vec::new(); response.into_reader().read_to_end(&mut body)?; - builder.body(body).map_err(Error::Http) + builder.body(body).map_err(HttpClientError::Http) } }