obscuravpn_api/
client.rs

1use crate::cmd::{parse_response, ApiError, ApiErrorKind, Cmd, ProtocolError};
2use crate::token::AcquireToken;
3use crate::types::AuthToken;
4use anyhow::{anyhow, Context};
5use std::sync::Arc;
6use std::sync::Mutex;
7use std::time::Duration;
8use thiserror::Error;
9
10#[derive(Debug)]
11pub struct Client {
12    account_id: String,
13    base_url: String,
14    http: reqwest::Client,
15    cached_auth_token: Arc<Mutex<Option<AuthToken>>>,
16    acquiring_auth_token: tokio::sync::Mutex<()>,
17}
18
19#[derive(Error, Debug)]
20pub enum ClientError {
21    #[error("API Error: {0}")]
22    ApiError(#[from] ApiError),
23    /// We got a response but it wasn't the expected format.
24    ///
25    /// Most likely a response from a proxy or similar.
26    #[error("Protocol Error: {0}")]
27    ProtocolError(#[from] ProtocolError),
28    #[error("request processing error: {:?}", .0)]
29    Other(#[from] anyhow::Error),
30}
31
32impl Client {
33    pub fn new(base_url: impl ToString, account_id: String, user_agent: &str) -> anyhow::Result<Self> {
34        let mut base_url = base_url.to_string();
35        if !base_url.ends_with('/') {
36            base_url += "/"
37        }
38        Ok(Self {
39            account_id,
40            base_url,
41            cached_auth_token: Arc::new(Mutex::new(None)),
42            http: reqwest::Client::builder()
43                .timeout(Duration::from_secs(60))
44                .read_timeout(Duration::from_secs(10))
45                .user_agent(user_agent)
46                .build()
47                .context("failed to initialize HTTP client")?,
48            acquiring_auth_token: tokio::sync::Mutex::new(()),
49        })
50    }
51
52    fn clear_auth_token(&self, token: AuthToken) {
53        let mut guard = self.cached_auth_token.lock().unwrap();
54        if guard.as_ref() == Some(&token) {
55            guard.take();
56        }
57    }
58
59    pub async fn acquire_auth_token(&self) -> Result<AuthToken, ClientError> {
60        if let Some(auth_token) = self.get_auth_token() {
61            return Ok(auth_token);
62        }
63
64        let acquiring_auth_token = self.acquiring_auth_token.lock().await;
65
66        if let Some(auth_token) = self.get_auth_token() {
67            return Ok(auth_token);
68        }
69        let account_id = self.account_id.clone();
70        let request = AcquireToken { account_id }.to_request(&self.base_url)?;
71        let res = self.send_http(request).await?;
72        let auth_token: String = parse_response(res).await?;
73        let auth_token: AuthToken = auth_token.into();
74        self.set_auth_token(Some(auth_token.clone()));
75
76        drop(acquiring_auth_token);
77        Ok(auth_token)
78    }
79
80    pub fn get_auth_token(&self) -> Option<AuthToken> {
81        self.cached_auth_token.lock().unwrap().clone()
82    }
83
84    pub fn set_auth_token(&self, token: Option<AuthToken>) {
85        *self.cached_auth_token.lock().unwrap() = token
86    }
87
88    async fn send_http(&self, request: http::Request<String>) -> anyhow::Result<reqwest::Response> {
89        let request = request.try_into().context("could not construct reqwest::Request")?;
90        self.http.execute(request).await.context("error executing request")
91    }
92
93    pub async fn run<C: Cmd>(&self, cmd: C) -> Result<C::Output, ClientError> {
94        for _ in 0..3 {
95            let auth_token = self.acquire_auth_token().await?;
96            if let Some(output) = self.try_run::<C>(&cmd, &auth_token).await? {
97                return Ok(output);
98            }
99            self.clear_auth_token(auth_token);
100        }
101
102        Err(anyhow!("repeatedly acquired invalid auth token").into())
103    }
104
105    // Sends the http request and maps expected error codes to client errors.
106    // Returns `Ok(None)` if the auth token is invalid, because this error shouldn't bubble up.
107    async fn try_run<C: Cmd>(&self, body: &C, auth_token: &AuthToken) -> Result<Option<C::Output>, ClientError> {
108        let request = body.to_request(&self.base_url, auth_token)?;
109        let res = self.send_http(request).await?;
110        match parse_response(res).await {
111            Ok(output) => Ok(Some(output)),
112            Err(ClientError::ApiError(error)) => match error.body.error {
113                ApiErrorKind::MissingOrInvalidAuthToken {} => Ok(None),
114                _ => Err(ClientError::ApiError(error)),
115            },
116            Err(err) => Err(err),
117        }
118    }
119}