connect_1password/
client.rs

1//! HTTP Client
2
3use crate::error::{Error, RequestNotSuccessful};
4use async_trait::async_trait;
5use dotenv::dotenv;
6use exponential_backoff::Backoff;
7use hyper::{
8    client::connect::HttpConnector, header::HeaderValue, Body, Client as HyperClient, Method,
9    Response, StatusCode,
10};
11use hyper_rustls::HttpsConnector;
12use log::{debug, error};
13use serde_json::Value;
14use std::{fmt, ops, thread, time::Duration};
15
16/// GET method
17pub const GET: Method = Method::GET;
18/// POST method
19pub const POST: Method = Method::POST;
20/// PUT method
21pub const PUT: Method = Method::PUT;
22/// DELETE method
23pub const DELETE: Method = Method::DELETE;
24
25const RETRY_ATTEMPTS: u32 = 5;
26
27/// Represents a (Hyper) HTTP client.
28#[derive(Debug)]
29pub struct Client {
30    api_key: String,
31    server_url: String,
32    https_client: HyperClient<HttpsConnector<HttpConnector>>,
33}
34
35/// Interface for any compatible HTTP client
36#[async_trait]
37pub trait HTTPClient {
38    /// Send a request using the underlying HTTP client
39    async fn send_request<T>(
40        &self,
41        method: &str,
42        endpoint: &str,
43        params: &[(&str, &str)],
44        body: Option<String>,
45    ) -> Result<(T, Value), Error>
46    where
47        T: serde::de::DeserializeOwned + std::fmt::Debug;
48}
49
50#[async_trait]
51impl HTTPClient for Client {
52    async fn send_request<T>(
53        &self,
54        method: &str,
55        endpoint: &str,
56        params: &[(&str, &str)],
57        body: Option<String>,
58    ) -> Result<(T, Value), Error>
59    where
60        T: serde::de::DeserializeOwned + std::fmt::Debug,
61    {
62        let api_key: &String = &self.api_key;
63
64        let method = match method {
65            "GET" => GET,
66            "POST" => POST,
67            "PUT" => PUT,
68            "DELETE" => DELETE,
69            &_ => GET,
70        };
71
72        let resp = retry_with_backoff(self, &method, &api_key[..], endpoint, params, body).await?;
73        let status = resp.status();
74
75        let data: (Result<T, Error>, Value) = hyper::body::to_bytes(resp.into_body())
76            .await
77            .map_err(Error::new_network_error)
78            .map(|mut bytes| {
79                dbg!(&bytes);
80
81                if &bytes.len() == &0 {
82                    bytes = hyper::body::Bytes::from("{}");
83                }
84                let json = serde_json::from_slice(&bytes).map_err(Error::new_parsing_error);
85
86                (json, bytes)
87            })
88            .and_then(|data| {
89                let json = data.0;
90                let bytes = std::str::from_utf8(&data.1)?;
91                let json_raw: Value = dbg!(serde_json::from_str(bytes)?);
92
93                dbg!(&json);
94                dbg!(&json_raw);
95
96                match status {
97                    StatusCode::OK => {}
98                    StatusCode::NO_CONTENT => {}
99                    _ => {
100                        debug!(
101                            "Client error! Status: {}, JSON: {}",
102                            status,
103                            &bytes.to_string()
104                        );
105
106                        return Err(RequestNotSuccessful::new(status, bytes.to_string()).into());
107                    }
108                };
109
110                Ok((json, json_raw))
111            })?;
112        let decoded = data.0?;
113        let raw_json = data.1;
114
115        dbg!(&decoded);
116
117        Ok((decoded, raw_json))
118    }
119}
120
121impl Client {
122    /// Create a new instance
123    ///
124    /// # Fields
125    ///
126    /// - `token`: provide the 1Password Connect API token.
127    /// - `server_url`: provide full URL to the host server, i.e. `http://localhost:8080`
128    pub fn new(token: &str, server_url: &str) -> Self {
129        let https = hyper_rustls::HttpsConnectorBuilder::new()
130            .with_native_roots()
131            .https_or_http()
132            .enable_http1()
133            .enable_http2()
134            .build();
135
136        Self {
137            api_key: token.to_string(),
138            server_url: server_url.to_string(),
139            https_client: hyper::Client::builder().build::<_, hyper::Body>(https),
140        }
141    }
142
143    /// Create an instance by fetching defaults from the host ENV.
144    ///
145    /// # Fields
146    ///
147    /// - `OP_API_TOKEN`: provide the 1Password Connect API token.
148    /// - `OP_SERVER_URL`: provide full URL to the host server, i.e. `http://localhost:8080`
149    pub fn default() -> Self {
150        let token = std::env::var("OP_API_TOKEN").expect("1Password API token expected!");
151        let host = std::env::var("OP_SERVER_URL").expect("1Password Connect server URL expected!");
152
153        // .env to override settings in ENV
154        dotenv().ok();
155
156        Client::new(&token, &host)
157    }
158
159    /// Returns the 1Password Connect API token.
160    pub fn token(&self) -> String {
161        self.api_key.clone()
162    }
163}
164
165struct RetryErrors<'a>(pub(crate) &'a mut Vec<String>);
166
167impl<'a> fmt::Display for RetryErrors<'a> {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        self.iter().fold(Ok(()), |result, error_msg| {
170            result.and_then(|_| writeln!(f, "{}", error_msg))
171        })
172    }
173}
174
175impl ops::Deref for RetryErrors<'_> {
176    type Target = Vec<String>;
177
178    fn deref(&self) -> &Self::Target {
179        self.0
180    }
181}
182
183/// Attempt exponential backoff when re-attempting requests to the Melissa service.
184async fn retry_with_backoff(
185    client: &Client,
186    method: &hyper::Method,
187    api_key: &str,
188    endpoint: &str,
189    params: &[(&str, &str)],
190    body: Option<String>,
191) -> Result<Response<Body>, Error> {
192    let retries = RETRY_ATTEMPTS;
193    let min = Duration::from_millis(100);
194    let max = Duration::from_secs(20);
195    let backoff = Backoff::new(retries, min, max);
196    let mut retry_error_messages: Vec<String> = vec![];
197    let mut retry_errors = vec![];
198
199    for duration in &backoff {
200        let url = format!("{}/{}?{}", client.server_url, endpoint, url_encode(params));
201
202        let body_data = match body {
203            Some(ref value) => Body::from(value.clone()),
204            None => Body::empty(),
205        };
206        let mut req = hyper::Request::builder()
207            .method(method)
208            .uri(&*url)
209            .body(body_data)?;
210
211        let auth = String::from("Bearer ") + api_key;
212        req.headers_mut()
213            .insert("Accept", HeaderValue::from_str("application/json")?);
214        req.headers_mut()
215            .insert("Authorization", HeaderValue::from_str(&auth)?);
216
217        match client.https_client.request(req).await {
218            Ok(value) => return Ok(value),
219            Err(err) => {
220                let error_message = format!("[ Retrying ]: Client error: {}", err);
221                retry_error_messages.push(error_message);
222                retry_errors.push(err);
223
224                thread::sleep(duration)
225            }
226        }
227    }
228
229    let err = if let Some(val) = retry_errors.pop() {
230        val
231    } else {
232        error!("Unable to unwrap error");
233        return Err(Error::new_internal_error());
234    };
235
236    Err(Error::new_retry_error(err))
237}
238
239fn url_encode(params: &[(&str, &str)]) -> String {
240    params
241        .iter()
242        .map(|&t| {
243            let (k, v) = t;
244            format!("{}={}", k, v)
245        })
246        .fold("".to_string(), |mut acc, item| {
247            acc.push_str(&item);
248            acc.push('&');
249            acc.replace('+', "%2B")
250        })
251}