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
55pub 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
74impl 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 .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 .build()
98 .into(),
99 token: Some(Token::demo()),
100 configuration: crate::environment::demo_configuration(),
101 }
102 }
103}
104
105fn 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}