summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src/account.rs
blob: f5655c9d1fb78d63a853b058054095c5e8baf5d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use chrono::{DateTime, Utc};
use futures::future::{abortable, AbortHandle};
use mullvad_rpc::{
    availability::ApiAvailabilityHandle,
    rest::{self, Error as RestError, MullvadRestHandle},
    AccountsProxy,
};
use mullvad_types::account::{AccountToken, VoucherSubmission};
use std::{future::Future, time::Duration};
use talpid_core::future_retry::{
    constant_interval, retry_future, retry_future_n, ExponentialBackoff, Jittered,
};

const RETRY_ACTION_INTERVAL: Duration = Duration::ZERO;
const RETRY_ACTION_MAX_RETRIES: usize = 2;

const RETRY_EXPIRY_CHECK_INTERVAL_INITIAL: Duration = Duration::from_secs(4);
const RETRY_EXPIRY_CHECK_INTERVAL_FACTOR: u32 = 5;
const RETRY_EXPIRY_CHECK_INTERVAL_MAX: Duration = Duration::from_secs(24 * 60 * 60);

pub struct Account(());

#[derive(Clone)]
pub struct AccountHandle {
    api_availability: ApiAvailabilityHandle,
    initial_check_abort_handle: AbortHandle,
    proxy: AccountsProxy,
}

impl AccountHandle {
    pub fn create_account(&self) -> impl Future<Output = Result<AccountToken, rest::Error>> {
        let mut proxy = self.proxy.clone();
        let api_handle = self.api_availability.clone();
        retry_future_n(
            move || proxy.create_account(),
            move |result| Self::should_retry(result, &api_handle),
            constant_interval(RETRY_ACTION_INTERVAL),
            RETRY_ACTION_MAX_RETRIES,
        )
    }

    pub fn get_www_auth_token(
        &self,
        account: AccountToken,
    ) -> impl Future<Output = Result<String, rest::Error>> {
        let proxy = self.proxy.clone();
        let api_handle = self.api_availability.clone();
        retry_future_n(
            move || proxy.get_www_auth_token(account.clone()),
            move |result| Self::should_retry(result, &api_handle),
            constant_interval(RETRY_ACTION_INTERVAL),
            RETRY_ACTION_MAX_RETRIES,
        )
    }

    pub async fn check_expiry(&self, token: AccountToken) -> Result<DateTime<Utc>, rest::Error> {
        let proxy = self.proxy.clone();
        let api_handle = self.api_availability.clone();
        let result = retry_future_n(
            move || proxy.get_expiry(token.clone()),
            move |result| Self::should_retry(result, &api_handle),
            constant_interval(RETRY_ACTION_INTERVAL),
            RETRY_ACTION_MAX_RETRIES,
        )
        .await;
        if handle_expiry_result_inner(&result, &self.api_availability) {
            self.initial_check_abort_handle.abort();
        }
        result
    }

    pub async fn submit_voucher(
        &mut self,
        account_token: AccountToken,
        voucher: String,
    ) -> Result<VoucherSubmission, rest::Error> {
        let mut proxy = self.proxy.clone();
        let api_handle = self.api_availability.clone();
        let result = retry_future_n(
            move || proxy.submit_voucher(account_token.clone(), voucher.clone()),
            move |result| Self::should_retry(result, &api_handle),
            constant_interval(RETRY_ACTION_INTERVAL),
            RETRY_ACTION_MAX_RETRIES,
        )
        .await;
        if result.is_ok() {
            self.initial_check_abort_handle.abort();
            self.api_availability.resume_background();
        }
        result
    }

    fn should_retry<T>(result: &Result<T, RestError>, api_handle: &ApiAvailabilityHandle) -> bool {
        match result {
            Err(error) if error.is_network_error() => !api_handle.get_state().is_offline(),
            _ => false,
        }
    }
}

impl Account {
    pub fn new(
        runtime: tokio::runtime::Handle,
        rpc_handle: MullvadRestHandle,
        token: Option<String>,
        api_availability: ApiAvailabilityHandle,
    ) -> AccountHandle {
        let accounts_proxy = AccountsProxy::new(rpc_handle);
        api_availability.pause_background();

        let api_availability_copy = api_availability.clone();
        let accounts_proxy_copy = accounts_proxy.clone();

        let (future, initial_check_abort_handle) = abortable(async move {
            let token = if let Some(token) = token {
                token
            } else {
                api_availability.pause_background();
                return;
            };

            let retry_strategy = Jittered::jitter(
                ExponentialBackoff::new(
                    RETRY_EXPIRY_CHECK_INTERVAL_INITIAL,
                    RETRY_EXPIRY_CHECK_INTERVAL_FACTOR,
                )
                .max_delay(RETRY_EXPIRY_CHECK_INTERVAL_MAX),
            );
            let future_generator = move || {
                let wait_online = api_availability.wait_online();
                let expiry_fut = accounts_proxy.get_expiry(token.clone());
                let api_availability_copy = api_availability.clone();
                async move {
                    let _ = wait_online.await;
                    handle_expiry_result_inner(&expiry_fut.await, &api_availability_copy)
                }
            };
            let should_retry = move |state_was_updated: &bool| -> bool { !*state_was_updated };
            retry_future(future_generator, should_retry, retry_strategy).await;
        });
        runtime.spawn(future);

        AccountHandle {
            api_availability: api_availability_copy,
            initial_check_abort_handle,
            proxy: accounts_proxy_copy,
        }
    }
}

fn handle_expiry_result_inner(
    result: &Result<chrono::DateTime<chrono::Utc>, mullvad_rpc::rest::Error>,
    api_availability: &ApiAvailabilityHandle,
) -> bool {
    match result {
        Ok(_expiry) if *_expiry >= chrono::Utc::now() => {
            api_availability.resume_background();
            true
        }
        Ok(_expiry) => {
            api_availability.pause_background();
            true
        }
        Err(mullvad_rpc::rest::Error::ApiError(_status, code)) => {
            if code == mullvad_rpc::INVALID_ACCOUNT || code == mullvad_rpc::INVALID_AUTH {
                api_availability.pause_background();
                return true;
            }
            false
        }
        Err(_) => false,
    }
}