Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add piping for google play payment API requests #5230

Merged
merged 5 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.mullvad.mullvadvpn.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize data class PlayPurchase(val productId: String, val purchaseToken: String) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.mullvad.mullvadvpn.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
enum class PlayPurchaseInitError : Parcelable {
// TODO: Add more errors here.
OtherError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.mullvad.mullvadvpn.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

sealed class PlayPurchaseInitResult : Parcelable {
@Parcelize data class Ok(val obfuscatedId: String) : PlayPurchaseInitResult()

@Parcelize
data class Error(val error: PlayPurchaseInitError) :
PlayPurchaseInitResult()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.mullvad.mullvadvpn.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
enum class PlayPurchaseVerifyError : Parcelable {
// TODO: Add more errors here.
OtherError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.mullvad.mullvadvpn.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

sealed class PlayPurchaseVerifyResult : Parcelable {
@Parcelize data object Ok : PlayPurchaseVerifyResult()

@Parcelize
data class Error(val error: PlayPurchaseVerifyError) : PlayPurchaseVerifyResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import net.mullvad.mullvadvpn.model.GeoIpLocation
import net.mullvad.mullvadvpn.model.GetAccountDataResult
import net.mullvad.mullvadvpn.model.LoginResult
import net.mullvad.mullvadvpn.model.ObfuscationSettings
import net.mullvad.mullvadvpn.model.PlayPurchase
import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
import net.mullvad.mullvadvpn.model.QuantumResistantState
import net.mullvad.mullvadvpn.model.RelayList
import net.mullvad.mullvadvpn.model.RelaySettingsUpdate
Expand Down Expand Up @@ -171,6 +174,14 @@ class MullvadDaemon(
return submitVoucher(daemonInterfaceAddress, voucher)
}

fun initPlayPurchase(): PlayPurchaseInitResult {
return initPlayPurchase(daemonInterfaceAddress)
}

fun verifyPlayPurchase(playPurchase: PlayPurchase): PlayPurchaseVerifyResult {
return verifyPlayPurchase(daemonInterfaceAddress, playPurchase)
}

fun updateRelaySettings(update: RelaySettingsUpdate) {
updateRelaySettings(daemonInterfaceAddress, update)
}
Expand Down Expand Up @@ -271,6 +282,13 @@ class MullvadDaemon(
voucher: String
): VoucherSubmissionResult

private external fun initPlayPurchase(daemonInterfaceAddress: Long): PlayPurchaseInitResult

private external fun verifyPlayPurchase(
daemonInterfaceAddress: Long,
playPurchase: PlayPurchase,
): PlayPurchaseVerifyResult

private external fun updateRelaySettings(
daemonInterfaceAddress: Long,
update: RelaySettingsUpdate
Expand Down
62 changes: 62 additions & 0 deletions mullvad-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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, VoucherSubmission},
version::AppVersion,
Expand Down Expand Up @@ -63,6 +65,8 @@ 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<ApiEndpoint> = LazyManual::new(ApiEndpoint::from_env_vars);

Expand Down Expand Up @@ -457,6 +461,64 @@ impl AccountsProxy {
}
}

#[cfg(target_os = "android")]
pub fn init_play_purchase(
&mut self,
account_token: AccountToken,
) -> impl Future<Output = Result<PlayPurchasePaymentToken, rest::Error>> {
#[derive(serde::Deserialize)]
struct PlayPurchaseInitResponse {
obfuscated_id: String,
}

let service = self.handle.service.clone();
let factory = self.handle.factory.clone();
let access_proxy = self.handle.token_store.clone();

async move {
let response = rest::send_json_request(
&factory,
service,
&format!("{GOOGLE_PAYMENTS_URL_PREFIX}/init"),
Method::POST,
&(),
Some((access_proxy, account_token)),
&[StatusCode::OK],
)
.await;

let PlayPurchaseInitResponse { obfuscated_id } =
rest::deserialize_body(response?).await?;

Ok(obfuscated_id)
}
}

#[cfg(target_os = "android")]
pub fn verify_play_purchase(
&mut self,
account_token: AccountToken,
play_purchase: PlayPurchase,
) -> impl Future<Output = Result<(), rest::Error>> {
let service = self.handle.service.clone();
let factory = self.handle.factory.clone();
let access_proxy = self.handle.token_store.clone();

async move {
rest::send_json_request(
&factory,
service,
&format!("{GOOGLE_PAYMENTS_URL_PREFIX}/acknowledge"),
Method::POST,
&play_purchase,
Some((access_proxy, account_token)),
&[StatusCode::ACCEPTED],
)
.await?;
Ok(())
}
}

pub fn get_www_auth_token(
&self,
account: AccountToken,
Expand Down
32 changes: 29 additions & 3 deletions mullvad-api/src/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,17 @@ impl From<Request> 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<String>,
}

#[derive(Clone)]
pub struct RequestFactory {
hostname: String,
Expand Down Expand Up @@ -600,8 +607,27 @@ pub async fn handle_error_response<T>(response: Response) -> Result<T> {
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))
}
};
}
},
};
Expand Down
59 changes: 59 additions & 0 deletions mullvad-daemon/src/device/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::pin::Pin;

use chrono::{DateTime, Utc};
use futures::{future::FusedFuture, Future};
#[cfg(target_os = "android")]
use mullvad_types::account::PlayPurchasePaymentToken;
use mullvad_types::{account::VoucherSubmission, device::Device, wireguard::WireguardData};

use super::{Error, PrivateAccountAndDevice, ResponseTx};
Expand Down Expand Up @@ -47,6 +49,27 @@ 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<PlayPurchasePaymentToken>,
tx: ResponseTx<PlayPurchasePaymentToken>,
) {
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<()>,
tx: ResponseTx<()>,
) {
self.current_call = Some(Call::VerifyPlayPurchase(
verify_play_purchase_call,
Some(tx),
));
}

pub fn is_validating(&self) -> bool {
matches!(
&self.current_call,
Expand Down Expand Up @@ -109,6 +132,13 @@ enum Call {
ApiCall<VoucherSubmission>,
Option<ResponseTx<VoucherSubmission>>,
),
#[cfg(target_os = "android")]
InitPlayPurchase(
ApiCall<PlayPurchasePaymentToken>,
Option<ResponseTx<PlayPurchasePaymentToken>>,
),
#[cfg(target_os = "android")]
VerifyPlayPurchase(ApiCall<()>, Option<ResponseTx<()>>),
ExpiryCheck(ApiCall<DateTime<Utc>>),
}

Expand Down Expand Up @@ -142,6 +172,28 @@ 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(
response,
tx.take().unwrap(),
))
} else {
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(
response,
tx.take().unwrap(),
))
} else {
std::task::Poll::Pending
}
}
ExpiryCheck(call) => Pin::new(call).poll(cx).map(ApiResult::ExpiryCheck),
}
}
Expand All @@ -155,5 +207,12 @@ pub(crate) enum ApiResult {
Result<VoucherSubmission, Error>,
ResponseTx<VoucherSubmission>,
),
#[cfg(target_os = "android")]
InitPlayPurchase(
Result<PlayPurchasePaymentToken, Error>,
ResponseTx<PlayPurchasePaymentToken>,
),
#[cfg(target_os = "android")]
VerifyPlayPurchase(Result<(), Error>, ResponseTx<()>),
ExpiryCheck(Result<DateTime<Utc>, Error>),
}
Loading
Loading