From 133d6f1307743cd55431e16b29121a18beeed86c Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 16 Oct 2023 15:20:47 +0200 Subject: [PATCH] Add android conditional compilation for google pay Add conditional compilation for google pay API access for only android. Also allow new error type to be parsed. Additionally fix review comments, formatting and warnings. --- mullvad-api/src/lib.rs | 7 +- mullvad-api/src/rest.rs | 32 +++++- mullvad-daemon/src/device/api.rs | 16 ++- mullvad-daemon/src/device/mod.rs | 155 +++++++++++++++------------ mullvad-daemon/src/device/service.rs | 7 +- mullvad-daemon/src/lib.rs | 72 +++++++------ mullvad-jni/src/lib.rs | 4 +- mullvad-types/src/account.rs | 6 +- 8 files changed, 181 insertions(+), 118 deletions(-) diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index 647b5cf0712b..63f5c2ad5b46 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -5,8 +5,10 @@ use chrono::{offset::Utc, DateTime}; use futures::channel::mpsc; use futures::Stream; use hyper::Method; +#[cfg(target_os = "android")] +use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; use mullvad_types::{ - account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, + account::{AccountToken, VoucherSubmission}, version::AppVersion, }; use proxy::ApiConnectionMode; @@ -63,6 +65,7 @@ pub const API_IP_CACHE_FILENAME: &str = "api-ip-address.txt"; const ACCOUNTS_URL_PREFIX: &str = "accounts/v1"; const APP_URL_PREFIX: &str = "app/v1"; +#[cfg(target_os = "android")] const GOOGLE_PAYMENTS_URL_PREFIX: &str = "payments/google-play/v1"; pub static API: LazyManual = LazyManual::new(ApiEndpoint::from_env_vars); @@ -458,6 +461,7 @@ impl AccountsProxy { } } + #[cfg(target_os = "android")] pub fn init_play_purchase( &mut self, account_token: AccountToken, @@ -490,6 +494,7 @@ impl AccountsProxy { } } + #[cfg(target_os = "android")] pub fn verify_play_purchase( &mut self, account_token: AccountToken, diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs index 674bcf8c4eb0..c3687a1eee9d 100644 --- a/mullvad-api/src/rest.rs +++ b/mullvad-api/src/rest.rs @@ -394,10 +394,17 @@ impl From for RestRequest { } #[derive(serde::Deserialize)] -pub struct ErrorResponse { +struct OldErrorResponse { pub code: String, } +/// If `NewErrorResponse::type` is not defined it should default to "about:blank" +const DEFAULT_ERROR_TYPE: &str = "about:blank"; +#[derive(serde::Deserialize)] +struct NewErrorResponse { + pub r#type: Option, +} + #[derive(Clone)] pub struct RequestFactory { hostname: String, @@ -600,8 +607,27 @@ pub async fn handle_error_response(response: Response) -> Result { status => match get_body_length(&response) { 0 => status.canonical_reason().unwrap_or("Unexpected error"), body_length => { - let err: ErrorResponse = deserialize_body_inner(response, body_length).await?; - return Err(Error::ApiError(status, err.code)); + return match response.headers().get("content-type") { + Some(content_type) if content_type == "application/problem+json" => { + // TODO: We should make sure we unify the new error format and the old + // error format so that they both produce the same Errors for the same + // problems after being processed. + let err: NewErrorResponse = + deserialize_body_inner(response, body_length).await?; + // The new error type replaces the `code` field with the `type` field. + // This is what is used to programmatically check the error. + Err(Error::ApiError( + status, + err.r#type + .unwrap_or_else(|| String::from(DEFAULT_ERROR_TYPE)), + )) + } + _ => { + let err: OldErrorResponse = + deserialize_body_inner(response, body_length).await?; + Err(Error::ApiError(status, err.code)) + } + }; } }, }; diff --git a/mullvad-daemon/src/device/api.rs b/mullvad-daemon/src/device/api.rs index 07e7b35311c4..d7b73d6e7beb 100644 --- a/mullvad-daemon/src/device/api.rs +++ b/mullvad-daemon/src/device/api.rs @@ -2,11 +2,9 @@ use std::pin::Pin; use chrono::{DateTime, Utc}; use futures::{future::FusedFuture, Future}; -use mullvad_types::{ - account::{PlayPurchasePaymentToken, VoucherSubmission}, - device::Device, - wireguard::WireguardData, -}; +#[cfg(target_os = "android")] +use mullvad_types::account::PlayPurchasePaymentToken; +use mullvad_types::{account::VoucherSubmission, device::Device, wireguard::WireguardData}; use super::{Error, PrivateAccountAndDevice, ResponseTx}; @@ -51,6 +49,7 @@ impl CurrentApiCall { self.current_call = Some(Call::VoucherSubmission(voucher_call, Some(tx))); } + #[cfg(target_os = "android")] pub fn set_init_play_purchase( &mut self, init_play_purchase_call: ApiCall, @@ -59,6 +58,7 @@ impl CurrentApiCall { self.current_call = Some(Call::InitPlayPurchase(init_play_purchase_call, Some(tx))); } + #[cfg(target_os = "android")] pub fn set_verify_play_purchase( &mut self, verify_play_purchase_call: ApiCall<()>, @@ -132,10 +132,12 @@ enum Call { ApiCall, Option>, ), + #[cfg(target_os = "android")] InitPlayPurchase( ApiCall, Option>, ), + #[cfg(target_os = "android")] VerifyPlayPurchase(ApiCall<()>, Option>), ExpiryCheck(ApiCall>), } @@ -170,6 +172,7 @@ impl futures::Future for Call { std::task::Poll::Pending } } + #[cfg(target_os = "android")] InitPlayPurchase(call, tx) => { if let std::task::Poll::Ready(response) = Pin::new(call).poll(cx) { std::task::Poll::Ready(ApiResult::InitPlayPurchase( @@ -180,6 +183,7 @@ impl futures::Future for Call { std::task::Poll::Pending } } + #[cfg(target_os = "android")] VerifyPlayPurchase(call, tx) => { if let std::task::Poll::Ready(response) = Pin::new(call).poll(cx) { std::task::Poll::Ready(ApiResult::VerifyPlayPurchase( @@ -203,10 +207,12 @@ pub(crate) enum ApiResult { Result, ResponseTx, ), + #[cfg(target_os = "android")] InitPlayPurchase( Result, ResponseTx, ), + #[cfg(target_os = "android")] VerifyPlayPurchase(Result<(), Error>, ResponseTx<()>), ExpiryCheck(Result, Error>), } diff --git a/mullvad-daemon/src/device/mod.rs b/mullvad-daemon/src/device/mod.rs index 528c25ce9a05..df5606a14375 100644 --- a/mullvad-daemon/src/device/mod.rs +++ b/mullvad-daemon/src/device/mod.rs @@ -5,13 +5,16 @@ use futures::{ }; use mullvad_api::rest; +#[cfg(target_os = "android")] +use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; use mullvad_types::{ - account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, + account::{AccountToken, VoucherSubmission}, device::{ AccountAndDevice, Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceName, DeviceState, }, wireguard::{self, RotationInterval, WireguardData}, }; + use std::{ future::Future, path::Path, @@ -305,7 +308,9 @@ enum AccountManagerCommand { SetRotationInterval(RotationInterval, ResponseTx<()>), ValidateDevice(ResponseTx<()>), SubmitVoucher(String, ResponseTx), + #[cfg(target_os = "android")] InitPlayPurchase(ResponseTx), + #[cfg(target_os = "android")] VerifyPlayPurchase(ResponseTx<()>, PlayPurchase), CheckExpiry(ResponseTx>), Shutdown(oneshot::Sender<()>), @@ -361,20 +366,22 @@ impl AccountManagerHandle { .await } + pub async fn check_expiry(&self) -> Result, Error> { + self.send_command(AccountManagerCommand::CheckExpiry).await + } + + #[cfg(target_os = "android")] pub async fn init_play_purchase(&self) -> Result { self.send_command(AccountManagerCommand::InitPlayPurchase) .await } + #[cfg(target_os = "android")] pub async fn verify_play_purchase(&self, play_purchase: PlayPurchase) -> Result<(), Error> { self.send_command(move |tx| AccountManagerCommand::VerifyPlayPurchase(tx, play_purchase)) .await } - pub async fn check_expiry(&self) -> Result, Error> { - self.send_command(AccountManagerCommand::CheckExpiry).await - } - pub async fn shutdown(self) { let (tx, rx) = oneshot::channel(); let _ = self @@ -526,15 +533,17 @@ impl AccountManager { Some(AccountManagerCommand::SubmitVoucher(voucher, tx)) => { self.handle_voucher_submission(tx, voucher, &mut current_api_call); }, + Some(AccountManagerCommand::CheckExpiry(tx)) => { + self.handle_expiry_request(tx, &mut current_api_call); + }, + #[cfg(target_os = "android")] Some(AccountManagerCommand::InitPlayPurchase(tx)) => { self.handle_init_play_purchase(tx, &mut current_api_call); }, + #[cfg(target_os = "android")] Some(AccountManagerCommand::VerifyPlayPurchase(tx, play_purchase)) => { self.handle_verify_play_purchase(tx, play_purchase, &mut current_api_call); }, - Some(AccountManagerCommand::CheckExpiry(tx)) => { - self.handle_expiry_request(tx, &mut current_api_call); - }, None => { break; @@ -607,6 +616,7 @@ impl AccountManager { } } + #[cfg(target_os = "android")] fn handle_init_play_purchase( &mut self, tx: ResponseTx, @@ -634,31 +644,24 @@ impl AccountManager { } } - fn handle_verify_play_purchase( + fn handle_expiry_request( &mut self, - tx: ResponseTx<()>, - play_purchase: PlayPurchase, + tx: ResponseTx>, current_api_call: &mut api::CurrentApiCall, ) { if current_api_call.is_logging_in() { let _ = tx.send(Err(Error::AccountChange)); return; } + if current_api_call.is_checking_expiry() { + self.expiry_requests.push(tx); + return; + } - let play_purchase_verify_api_call = move || { - let old_config = self.data.device().ok_or(Error::NoDevice)?; - let account_token = old_config.account_token.clone(); - let account_service = self.account_service.clone(); - Ok(async move { - account_service - .verify_play_purchase(account_token, play_purchase) - .await - }) - }; - - match play_purchase_verify_api_call() { + match self.expiry_call() { Ok(call) => { - current_api_call.set_verify_play_purchase(Box::pin(call), tx); + current_api_call.set_expiry_check(Box::pin(call)); + self.expiry_requests.push(tx); } Err(err) => { let _ = tx.send(Err(err)); @@ -666,24 +669,32 @@ impl AccountManager { } } - fn handle_expiry_request( + #[cfg(target_os = "android")] + fn handle_verify_play_purchase( &mut self, - tx: ResponseTx>, + tx: ResponseTx<()>, + play_purchase: PlayPurchase, current_api_call: &mut api::CurrentApiCall, ) { if current_api_call.is_logging_in() { let _ = tx.send(Err(Error::AccountChange)); return; } - if current_api_call.is_checking_expiry() { - self.expiry_requests.push(tx); - return; - } - match self.expiry_call() { + let play_purchase_verify_api_call = move || { + let old_config = self.data.device().ok_or(Error::NoDevice)?; + let account_token = old_config.account_token.clone(); + let account_service = self.account_service.clone(); + Ok(async move { + account_service + .verify_play_purchase(account_token, play_purchase) + .await + }) + }; + + match play_purchase_verify_api_call() { Ok(call) => { - current_api_call.set_expiry_check(Box::pin(call)); - self.expiry_requests.push(tx); + current_api_call.set_verify_play_purchase(Box::pin(call), tx); } Err(err) => { let _ = tx.send(Err(err)); @@ -704,15 +715,17 @@ impl AccountManager { VoucherSubmission(data_response, tx) => { self.consume_voucher_result(data_response, tx).await } + ExpiryCheck(data_response) => self.consume_expiry_result(data_response).await, + #[cfg(target_os = "android")] InitPlayPurchase(data_response, tx) => { self.consume_init_play_purchase_result(data_response, tx) .await } + #[cfg(target_os = "android")] VerifyPlayPurchase(data_response, tx) => { self.consume_verify_play_purchase_result(data_response, tx) .await } - ExpiryCheck(data_response) => self.consume_expiry_result(data_response).await, } } @@ -750,42 +763,6 @@ impl AccountManager { let _ = tx.send(response); } - async fn consume_init_play_purchase_result( - &mut self, - response: Result, - tx: ResponseTx, - ) { - match &response { - Ok(_) => (), - Err(Error::InvalidAccount) => { - self.revoke_device(|| Error::InvalidAccount).await; - } - Err(Error::InvalidDevice) => { - self.revoke_device(|| Error::InvalidDevice).await; - } - Err(err) => log::error!("Failed to initialize play purchase: {}", err), - } - let _ = tx.send(response); - } - - async fn consume_verify_play_purchase_result( - &mut self, - response: Result<(), Error>, - tx: ResponseTx<()>, - ) { - match &response { - Ok(_) => (), - Err(Error::InvalidAccount) => { - self.revoke_device(|| Error::InvalidAccount).await; - } - Err(Error::InvalidDevice) => { - self.revoke_device(|| Error::InvalidDevice).await; - } - Err(err) => log::error!("Failed to verify play purchase: {}", err), - } - let _ = tx.send(response); - } - async fn consume_expiry_result(&mut self, response: Result, Error>) { match response { Ok(expiry) => { @@ -920,6 +897,44 @@ impl AccountManager { } } + #[cfg(target_os = "android")] + async fn consume_init_play_purchase_result( + &mut self, + response: Result, + tx: ResponseTx, + ) { + match &response { + Ok(_) => (), + Err(Error::InvalidAccount) => { + self.revoke_device(|| Error::InvalidAccount).await; + } + Err(Error::InvalidDevice) => { + self.revoke_device(|| Error::InvalidDevice).await; + } + Err(err) => log::error!("Failed to initialize play purchase: {}", err), + } + let _ = tx.send(response); + } + + #[cfg(target_os = "android")] + async fn consume_verify_play_purchase_result( + &mut self, + response: Result<(), Error>, + tx: ResponseTx<()>, + ) { + match &response { + Ok(_) => (), + Err(Error::InvalidAccount) => { + self.revoke_device(|| Error::InvalidAccount).await; + } + Err(Error::InvalidDevice) => { + self.revoke_device(|| Error::InvalidDevice).await; + } + Err(err) => log::error!("Failed to verify play purchase: {}", err), + } + let _ = tx.send(response); + } + fn drain_device_requests_with_err(&mut self, err: Error) { let cloneable_err = Arc::new(err); Self::drain_requests(&mut self.rotation_requests, || { diff --git a/mullvad-daemon/src/device/service.rs b/mullvad-daemon/src/device/service.rs index 85d9d56d8431..fdda61297fd0 100644 --- a/mullvad-daemon/src/device/service.rs +++ b/mullvad-daemon/src/device/service.rs @@ -2,8 +2,10 @@ use std::{future::Future, time::Duration}; use chrono::{DateTime, Utc}; use futures::future::{abortable, AbortHandle}; +#[cfg(target_os = "android")] +use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; use mullvad_types::{ - account::{AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, + account::{AccountToken, VoucherSubmission}, device::{Device, DeviceId}, wireguard::WireguardData, }; @@ -321,6 +323,7 @@ impl AccountService { result.map_err(map_rest_error) } + #[cfg(target_os = "android")] pub async fn init_play_purchase( &self, account_token: AccountToken, @@ -340,6 +343,7 @@ impl AccountService { result.map_err(map_rest_error) } + #[cfg(target_os = "android")] pub async fn verify_play_purchase( &self, account_token: AccountToken, @@ -448,6 +452,7 @@ fn should_retry_backoff(result: &Result) -> bool { fn map_rest_error(error: rest::Error) -> Error { match error { RestError::ApiError(_status, ref code) => match code.as_str() { + // TODO: Implement invalid payment mullvad_api::DEVICE_NOT_FOUND => Error::InvalidDevice, mullvad_api::INVALID_ACCOUNT => Error::InvalidAccount, mullvad_api::MAX_DEVICES_REACHED => Error::MaxDevicesReached, diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 8f661d981a89..1077185ca38d 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -38,9 +38,11 @@ use mullvad_relay_selector::{ updater::{RelayListUpdater, RelayListUpdaterHandle}, RelaySelector, SelectorConfig, }; +#[cfg(target_os = "android")] +use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; use mullvad_types::{ access_method::{AccessMethod, AccessMethodSetting}, - account::{AccountData, AccountToken, PlayPurchase, PlayPurchasePaymentToken, VoucherSubmission}, + account::{AccountData, AccountToken, VoucherSubmission}, auth_failed::AuthFailed, custom_list::CustomList, device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, @@ -1041,10 +1043,6 @@ where GetAccountData(tx, account_token) => self.on_get_account_data(tx, account_token), GetWwwAuthToken(tx) => self.on_get_www_auth_token(tx).await, SubmitVoucher(tx, voucher) => self.on_submit_voucher(tx, voucher), - InitPlayPurchase(tx) => self.on_init_play_purchase(tx), - VerifyPlayPurchase(tx, play_purchase) => { - self.on_verify_play_purchase(tx, play_purchase) - } GetRelayLocations(tx) => self.on_get_relay_locations(tx), UpdateRelayLocations => self.on_update_relay_locations().await, LoginAccount(tx, account_token) => self.on_login_account(tx, account_token), @@ -1127,6 +1125,12 @@ where PrepareRestart => self.on_prepare_restart(), #[cfg(target_os = "android")] BypassSocket(fd, tx) => self.on_bypass_socket(fd, tx), + #[cfg(target_os = "android")] + InitPlayPurchase(tx) => self.on_init_play_purchase(tx), + #[cfg(target_os = "android")] + VerifyPlayPurchase(tx, play_purchase) => { + self.on_verify_play_purchase(tx, play_purchase) + } } } @@ -1422,34 +1426,6 @@ where }); } - fn on_init_play_purchase(&mut self, tx: ResponseTx) { - let manager = self.account_manager.clone(); - tokio::spawn(async move { - Self::oneshot_send( - tx, - manager - .init_play_purchase() - .await - .map_err(Error::InitPlayPurchase), - "init_play_purchase response", - ); - }); - } - - fn on_verify_play_purchase(&mut self, tx: ResponseTx<(), Error>, play_purchase: PlayPurchase) { - let manager = self.account_manager.clone(); - tokio::spawn(async move { - Self::oneshot_send( - tx, - manager - .verify_play_purchase(play_purchase) - .await - .map_err(Error::VerifyPlayPurchase), - "verify_play_purchase response", - ); - }); - } - fn on_get_relay_locations(&mut self, tx: oneshot::Sender) { Self::oneshot_send(tx, self.relay_selector.get_locations(), "relay locations"); } @@ -2416,6 +2392,36 @@ where } } + #[cfg(target_os = "android")] + fn on_init_play_purchase(&mut self, tx: ResponseTx) { + let manager = self.account_manager.clone(); + tokio::spawn(async move { + Self::oneshot_send( + tx, + manager + .init_play_purchase() + .await + .map_err(Error::InitPlayPurchase), + "init_play_purchase response", + ); + }); + } + + #[cfg(target_os = "android")] + fn on_verify_play_purchase(&mut self, tx: ResponseTx<(), Error>, play_purchase: PlayPurchase) { + let manager = self.account_manager.clone(); + tokio::spawn(async move { + Self::oneshot_send( + tx, + manager + .verify_play_purchase(play_purchase) + .await + .map_err(Error::VerifyPlayPurchase), + "verify_play_purchase response", + ); + }); + } + /// Set the target state of the client. If it changed trigger the operations needed to /// progress towards that state. /// Returns a bool representing whether or not a state change was initiated. diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index ff7a978799e8..a40bf73fc13f 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -214,7 +214,7 @@ impl From> for PlayPurchaseInitResult { } impl From for PlayPurchaseInitError { - fn from(error: daemon_interface::Error) -> Self { + fn from(_error: daemon_interface::Error) -> Self { PlayPurchaseInitError::OtherError } } @@ -242,7 +242,7 @@ impl From> for PlayPurchaseVerifyResult { } impl From for PlayPurchaseVerifyError { - fn from(error: daemon_interface::Error) -> Self { + fn from(_error: daemon_interface::Error) -> Self { PlayPurchaseVerifyError::OtherError } } diff --git a/mullvad-types/src/account.rs b/mullvad-types/src/account.rs index 2d12a80a2a07..7adb7fbffa80 100644 --- a/mullvad-types/src/account.rs +++ b/mullvad-types/src/account.rs @@ -9,9 +9,9 @@ pub type AccountToken = String; /// Identifier used to authenticate a Mullvad account. pub type AccessToken = String; -// TODO: Should be only android /// The payment token returned by initiating a google play purchase. /// In the API this is called the `obfuscated_id`. +#[cfg(target_os = "android")] pub type PlayPurchasePaymentToken = String; /// Account expiration info returned by the API via `/v1/me`. @@ -44,14 +44,14 @@ pub struct VoucherSubmission { pub new_expiry: DateTime, } -/// TODO Should be only android /// `PlayPurchase` is provided to google in order to verify that a google play purchase was acknowledged. #[derive(Deserialize, Serialize, Debug, Clone)] #[cfg_attr(target_os = "android", derive(FromJava))] #[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] +#[cfg(target_os = "android")] pub struct PlayPurchase { pub product_id: String, - pub purchase_token: String, + pub purchase_token: PlayPurchasePaymentToken, } /// Token used for authentication in the API.