inth_oauth2_async/client/
http_client.rs

1//! HTTP client abstraction and implementations.
2//!
3//! We provide out-of-the-box implementations for two crates: Hyper, and Reqwest; both of which are
4//! gated by Cargo features. The [`HttpClient`] trait can alternatively be implemented for any other
5//! client type you need.
6
7use crate::client::error::ClientError;
8
9/// Abstraction of the parts of a HTTP client implementation that this crate needs.
10#[async_trait::async_trait]
11pub trait HttpClient {
12    /// Make a HTTP POST request.
13    ///
14    /// [`client_id`] and [`client_secret`] are to be given as HTTP Basic Auth credentials username
15    /// and password, respectively.
16    ///
17    /// The [`body`] is of content-type `application/x-www-form-urlencoded`, and the response body
18    /// is expected to be `application/json`.
19    ///
20    /// The response body must be deserialized into a json value.
21    async fn post(
22        &self,
23        url: &str,
24        client_id: &str,
25        client_secret: &str,
26        body: String,
27    ) -> Result<serde_json::Value, ClientError>;
28}
29
30/// Implementation for Reqwest.
31#[cfg(feature = "reqwest-client")]
32pub mod reqwest_client {
33    use super::*;
34    use reqwest::header::{ACCEPT, CONTENT_TYPE};
35
36    #[async_trait::async_trait]
37    impl HttpClient for reqwest::Client {
38        async fn post(
39            &self,
40            url: &str,
41            client_id: &str,
42            client_secret: &str,
43            body: String,
44        ) -> Result<serde_json::Value, ClientError> {
45            let response = reqwest::Client::post(self, url)
46                .basic_auth(client_id, Some(client_secret))
47                .header(ACCEPT, "application/json")
48                .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
49                .body(body)
50                .send()
51                .await?;
52
53            let full = response.bytes().await?;
54            let json = serde_json::from_slice(&full)?;
55
56            Ok(json)
57        }
58    }
59}
60
61/// Implementation for Hyper
62#[cfg(feature = "hyper-client")]
63pub mod hyper_client {
64    use super::*;
65    use base64::write::EncoderWriter as Base64Encoder;
66    use http_body_util::BodyExt;
67    use hyper::body::Body;
68    use hyper::header::{AUTHORIZATION, ACCEPT, CONTENT_TYPE, HeaderValue};
69    use hyper::Request;
70    use hyper_util::client::legacy::connect::Connect;
71    use std::io::Write;
72
73    #[async_trait::async_trait]
74    impl<C, B> HttpClient for hyper_util::client::legacy::Client<C, B> where
75        // (°ー°) ...
76        C: Connect + Clone + Send + Sync + 'static,
77        B: Body + From<String> + Send + Unpin + 'static,
78        B::Data: Send,
79        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
80    {
81        async fn post(
82            &self,
83            url: &str,
84            client_id: &str,
85            client_secret: &str,
86            body: String,
87        ) -> Result<serde_json::Value, ClientError> {
88            let mut auth_header = b"Basic ".to_vec();
89            {
90                let mut enc = Base64Encoder::new(&mut auth_header, base64::STANDARD);
91                write!(enc, "{}:{}", client_id, client_secret)?;
92            }
93
94            let mut auth_header_val = HeaderValue::from_bytes(&auth_header)
95                .expect("invalid header value"); // should never happen for base64 data
96            auth_header_val.set_sensitive(true);
97
98            let req = Request::post(url)
99                .header(AUTHORIZATION, auth_header_val)
100                .header(ACCEPT, "application/json")
101                .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
102                .body(body.into())?;
103
104            let response = self.request(req).await?;
105            let full = response.into_body().collect().await?.to_bytes();
106            let json = serde_json::from_slice(&full)?;
107            Ok(json)
108        }
109    }
110}