transip/
client.rs

1use core::time::Duration;
2use std::fmt::Debug;
3
4use serde::{de::DeserializeOwned, Serialize};
5use tracing::instrument;
6use ureq::config::{ConfigBuilder, IpFamily};
7use ureq::typestate::AgentScope;
8use ureq::Agent;
9
10use crate::authentication::{
11    AuthRequest, KeyPair, Token, TokenExpired, TokenResponse, UrlAuthentication,
12};
13use crate::{Configuration, Error, Result};
14
15const TRANSIP_API_PREFIX: &str = "https://api.transip.nl/v6/";
16const AGENT_TIMEOUT_SECONDS: u64 = 30;
17const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"));
18
19macro_rules! timeit {
20    ($url:expr, $method:expr, $code:block) => {{
21        let start = std::time::Instant::now();
22        let t = $code;
23        if t.is_err() {
24            tracing::error!(
25                "error {} {} after {} milliseconds",
26                $method,
27                $url,
28                start.elapsed().as_millis()
29            );
30        } else {
31            tracing::info!(
32                "result {} {} after {} milliseconds",
33                $method,
34                $url,
35                start.elapsed().as_millis()
36            );
37        };
38        t
39    }};
40}
41
42#[derive(Debug)]
43pub struct Url {
44    pub prefix: String,
45}
46
47impl From<&str> for Url {
48    fn from(prefix: &str) -> Self {
49        Self {
50            prefix: prefix.to_owned(),
51        }
52    }
53}
54
55/// Client is the main entry for this library. Creation is done by using Client::try_from(configuration).
56/// After creation of a client, this client can be used to call a Transip API call.
57/// Each call starts with a check to see if we have a valid JWT token
58/// If the token is expired or non existant then the Transip API call for requesting a new token is called
59/// Tokens are persisted to disk on exit and reused if not expired on application startup
60pub struct Client {
61    pub(crate) url: Url,
62    configuration: Box<dyn Configuration>,
63    key: Option<KeyPair>,
64    agent: Agent,
65    token: Option<Token>,
66}
67
68impl Debug for Client {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "{}", self.url.prefix)
71    }
72}
73
74// #[cfg(test)]
75impl Client {
76    pub fn demo() -> Self {
77        Self {
78            url: TRANSIP_API_PREFIX.into(),
79            key: None,
80            agent: Agent::config_builder()
81                .timeout_global(Some(Duration::from_secs(AGENT_TIMEOUT_SECONDS)))
82                //                .user_agent(USER_AGENT)
83                .build()
84                .into(),
85            token: Some(Token::demo()),
86            configuration: crate::environment::demo_configuration(),
87        }
88    }
89
90    pub fn test(prefix: String) -> Self {
91        Self {
92            url: format!("{prefix}/").as_str().into(),
93            key: None,
94            agent: Agent::config_builder()
95                .timeout_global(Some(Duration::from_secs(AGENT_TIMEOUT_SECONDS)))
96                //                .user_agent(USER_AGENT)
97                .build()
98                .into(),
99            token: Some(Token::demo()),
100            configuration: crate::environment::demo_configuration(),
101        }
102    }
103}
104
105//#[derive(Debug)]
106//pub struct Ipv6Resolver;
107//
108//impl Resolver for Ipv6Resolver {
109//    fn resolve(&self, netloc: &str) -> std::io::Result<Vec<std::net::SocketAddr>> {
110//        ToSocketAddrs::to_socket_addrs(netloc)
111//            .map(|iter| {
112//                iter
113//                    // only keep ipv6 addresses
114//                    .filter(|s| s.is_ipv6())
115//                    .collect::<Vec<SocketAddr>>()
116//            })
117//            .inspect(|v| {
118//                if v.is_empty() {
119//                    info!(
120//                        "Failed to find any ipv6 addresses. This probably means \
121//                    the DNS server didn't return any."
122//                    )
123//                }
124//            })
125//    }
126//}
127
128fn build_agent(ipv6only: bool) -> ConfigBuilder<AgentScope> {
129    if ipv6only {
130        Agent::config_builder().ip_family(IpFamily::Ipv6Only)
131    } else {
132        Agent::config_builder().ip_family(IpFamily::Any)
133    }
134}
135
136impl TryFrom<Box<dyn Configuration>> for Client {
137    type Error = Error;
138    fn try_from(configuration: Box<dyn Configuration>) -> Result<Self> {
139        KeyPair::try_from_file(configuration.private_key_pem_file()).map(|key| Self {
140            url: TRANSIP_API_PREFIX.into(),
141            key: Some(key),
142            agent: build_agent(configuration.ipv6_only())
143                .user_agent(USER_AGENT)
144                .build()
145                .into(),
146            token: Token::try_from_file(configuration.token_path()).ok(),
147            configuration,
148        })
149    }
150}
151
152impl Drop for Client {
153    fn drop(&mut self) {
154        if self.key.is_some() {
155            if let Some(token) = self.token.take() {
156                if let Err(error) = token.try_to_write_file(self.configuration.token_path()) {
157                    tracing::error!(
158                        "Error {} writing token to {}",
159                        error,
160                        self.configuration.token_path()
161                    );
162                }
163            }
164        }
165    }
166}
167
168impl Client {
169    fn refresh_token_if_needed(&mut self) -> Result<()> {
170        if self.token.token_expired() {
171            let span = tracing::span!(tracing::Level::INFO, "token_refresh");
172            let _span_enter = span.enter();
173            let token_result = timeit!(&self.url.auth(), "POST", {
174                let auth_request = AuthRequest::new(
175                    self.configuration.user_name(),
176                    self.configuration.token_expiration(),
177                    self.configuration.read_only(),
178                    self.configuration.whitelisted_only(),
179                );
180                let json = auth_request.json();
181                let signature = self.key.as_ref().unwrap().sign(&json)?;
182                let token_response = self
183                    .agent
184                    .post(&self.url.auth())
185                    .header("Content-Type", "application/json")
186                    .header("Signature", &signature)
187                    .send_json(&auth_request)?
188                    .into_body()
189                    .read_json::<TokenResponse>()?;
190                Token::try_from(token_response.token)
191            });
192            self.token = token_result.ok();
193        }
194        Ok(())
195    }
196
197    #[instrument(skip(self))]
198    pub(crate) fn get<T>(&mut self, url: &str) -> Result<T>
199    where
200        T: DeserializeOwned,
201    {
202        timeit!(url, "GET", {
203            self.refresh_token_if_needed()?;
204            let token = self.token.as_ref().ok_or(Error::Token)?;
205            let response = self
206                .agent
207                .get(url)
208                .header("Authorization", &format!("Bearer {}", token.raw()))
209                .call()?;
210            let result = response.into_body().read_json::<T>()?;
211            Ok(result)
212        })
213    }
214
215    #[instrument(skip(self))]
216    pub(crate) fn delete<T>(&mut self, url: &str, object: T) -> Result<()>
217    where
218        T: Serialize + Debug,
219    {
220        timeit!(url, "DELETE", {
221            self.refresh_token_if_needed()?;
222            let token = self.token.as_ref().ok_or(Error::Token)?;
223            self.agent
224                .delete(url)
225                .header("Authorization", &format!("Bearer {}", token.raw()))
226                .call()?;
227            Ok(())
228        })
229    }
230
231    #[instrument(skip(self))]
232    pub(crate) fn delete_no_object(&mut self, url: &str) -> Result<()> {
233        timeit!(url, "DELETE", {
234            self.refresh_token_if_needed()?;
235            let token = self.token.as_ref().ok_or(Error::Token)?;
236            self.agent
237                .delete(url)
238                .header("Authorization", &format!("Bearer {}", token.raw()))
239                .call()?;
240            Ok(())
241        })
242    }
243
244    #[instrument(skip(self))]
245    pub(crate) fn patch<T>(&mut self, url: &str, object: T) -> Result<()>
246    where
247        T: Serialize + Debug,
248    {
249        timeit!(url, "PATCH", {
250            self.refresh_token_if_needed()?;
251            let token = self.token.as_ref().ok_or(Error::Token)?;
252            self.agent
253                .patch(url)
254                .header("Authorization", &format!("Bearer {}", token.raw()))
255                .send_json(object)?;
256            Ok(())
257        })
258    }
259
260    #[instrument(skip(self))]
261    pub(crate) fn post<T>(&mut self, url: &str, body: T) -> Result<()>
262    where
263        T: Serialize + Debug,
264    {
265        timeit!(url, "POST", {
266            self.refresh_token_if_needed()?;
267            let token = self.token.as_ref().ok_or(Error::Token)?;
268            self.agent
269                .post(url)
270                .header("Authorization", &format!("Bearer {}", token.raw()))
271                .send_json(body)?;
272            Ok(())
273        })
274    }
275
276    #[instrument(skip(self))]
277    pub(crate) fn put<T>(&mut self, url: &str, body: T) -> Result<()>
278    where
279        T: Serialize + Debug,
280    {
281        timeit!(url, "PUT", {
282            self.refresh_token_if_needed()?;
283            let token = self.token.as_ref().ok_or(Error::Token)?;
284            self.agent
285                .put(url)
286                .header("Authorization", &format!("Bearer {}", token.raw()))
287                .send_json(body)?;
288            Ok(())
289        })
290    }
291}