diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-08 16:51:28 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-08 16:51:28 +0200 |
| commit | 9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe (patch) | |
| tree | b19fdb5f1a4b299de54d323965be32a3756270d6 | |
| parent | 5a53a0479d33d9cdab1f3859706fb2ff776ee56a (diff) | |
| parent | 4ae7d50075a6e82a0d1edabf26ce13d9357479cb (diff) | |
| download | mullvadvpn-9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe.tar.xz mullvadvpn-9a6938a4a32ecc162cc5afcf15fa636de2f9b9fe.zip | |
Merge branch 'use-mullvad-api-instead-of-urlsession-in-accounts-proxy-ios-982'
26 files changed, 658 insertions, 133 deletions
diff --git a/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift index 0b7f62adee..ebb69f1b03 100644 --- a/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/AccountsProxy+Stubs.swift @@ -13,25 +13,28 @@ import MullvadTypes struct AccountProxyStubError: Error {} struct AccountsProxyStub: RESTAccountHandling { - var createAccountResult: Result<REST.NewAccountData, Error> = .failure(AccountProxyStubError()) + var createAccountResult: Result<NewAccountData, Error> = .failure(AccountProxyStubError()) var deleteAccountResult: Result<Void, Error> = .failure(AccountProxyStubError()) func createAccount( retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler<REST.NewAccountData> + completion: @escaping ProxyCompletionHandler<NewAccountData> ) -> Cancellable { completion(createAccountResult) return AnyCancellable() } - func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> { - RESTRequestExecutorStub<Account>(success: { - Account( - id: accountNumber, - expiry: Calendar.current.date(byAdding: .day, value: 38, to: Date())!, - maxDevices: 1, - canAddDevices: true - ) - }) + func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Account> + ) -> Cancellable { + completion(.success(Account( + id: accountNumber, + expiry: Calendar.current.date(byAdding: .day, value: 38, to: Date())!, + maxDevices: 1, + canAddDevices: true + ))) + return AnyCancellable() } func deleteAccount( diff --git a/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift b/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift new file mode 100644 index 0000000000..f50b990f65 --- /dev/null +++ b/ios/MullvadMockData/MullvadTypes/NewAccountDataMock.swift @@ -0,0 +1,23 @@ +// +// NewAccountDataMock.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-04-08. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes + +extension NewAccountData { + public static func mockValue() -> NewAccountData { + return NewAccountData( + id: UUID().uuidString, + expiry: Date().addingTimeInterval(3600), + maxPorts: 2, + canAddPorts: false, + maxDevices: 5, + canAddDevices: false, + number: "1234567890123456" + ) + } +} diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift index 0d75b25d74..d45745446e 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -12,37 +12,6 @@ import MullvadTypes import Operations import WireGuardKitTypes -public protocol APIQuerying: Sendable { - func getAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> Cancellable - - func getRelays( - etag: String?, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> - ) -> Cancellable - - func createApplePayment( - accountNumber: String, - receiptString: Data - ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse> - - func sendProblemReport( - _ body: REST.ProblemReportRequest, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<Void> - ) -> Cancellable - - func submitVoucher( - voucherCode: String, - accountNumber: String, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse> - ) -> Cancellable -} - extension REST { public final class APIProxy: Proxy<AuthProxyConfiguration>, APIQuerying, @unchecked Sendable { public init(configuration: AuthProxyConfiguration) { diff --git a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift index 79e9e6dd2a..9bcc9c70b0 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift @@ -9,21 +9,6 @@ import Foundation import MullvadTypes -public protocol RESTAccountHandling: Sendable { - func createAccount( - retryStrategy: REST.RetryStrategy, - completion: @escaping @Sendable ProxyCompletionHandler<REST.NewAccountData> - ) -> Cancellable - - func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> - - func deleteAccount( - accountNumber: String, - retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler<Void> - ) -> Cancellable -} - extension REST { public final class AccountsProxy: Proxy<AuthProxyConfiguration>, RESTAccountHandling, @unchecked Sendable { public init(configuration: AuthProxyConfiguration) { @@ -64,7 +49,11 @@ extension REST { return executor.execute(retryStrategy: retryStrategy, completionHandler: completion) } - public func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> { + public func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Account> + ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in var requestBuilder = try self.requestFactory.createRequestBuilder( @@ -85,11 +74,13 @@ extension REST { with: responseDecoder ) - return makeRequestExecutor( + let executor = makeRequestExecutor( name: "get-my-account", requestHandler: requestHandler, responseHandler: responseHandler ) + + return executor.execute(retryStrategy: retryStrategy, completionHandler: completion) } public func deleteAccount( @@ -134,28 +125,4 @@ extension REST { return executor.execute(retryStrategy: retryStrategy, completionHandler: completion) } } - - public struct NewAccountData: Decodable, Sendable { - public let id: String - public let expiry: Date - public let maxPorts: Int - public let canAddPorts: Bool - public let maxDevices: Int - public let canAddDevices: Bool - public let number: String - } -} - -extension REST.NewAccountData { - public static func mockValue() -> REST.NewAccountData { - return REST.NewAccountData( - id: UUID().uuidString, - expiry: Date().addingTimeInterval(3600), - maxPorts: 2, - canAddPorts: false, - maxDevices: 5, - canAddDevices: false, - number: "1234567890123456" - ) - } } diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift index c6d6fab25e..47a157dd8a 100644 --- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift @@ -138,7 +138,11 @@ extension REST { public let wireguard: ServerWireguardTunnels public let bridge: ServerBridges - public init(locations: [String: ServerLocation], wireguard: ServerWireguardTunnels, bridge: ServerBridges) { + public init( + locations: [String: ServerLocation], + wireguard: ServerWireguardTunnels, + bridge: ServerBridges + ) { self.locations = locations self.wireguard = wireguard self.bridge = bridge diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift index be478ecb59..5d2555f07f 100644 --- a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift @@ -11,6 +11,37 @@ import MullvadTypes import Operations import WireGuardKitTypes +public protocol APIQuerying: Sendable { + func getAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable + + func getRelays( + etag: String?, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> + ) -> Cancellable + + func createApplePayment( + accountNumber: String, + receiptString: Data + ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse> + + func sendProblemReport( + _ body: REST.ProblemReportRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<Void> + ) -> Cancellable + + func submitVoucher( + voucherCode: String, + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse> + ) -> Cancellable +} + extension REST { public final class MullvadAPIProxy: APIQuerying, @unchecked Sendable { let transportProvider: APITransportProviderProtocol diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift new file mode 100644 index 0000000000..7ee228b8a5 --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAccountProxy.swift @@ -0,0 +1,125 @@ +// +// MullvadAccountProxy.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-03-31. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadRustRuntime +import MullvadTypes +import Operations +import WireGuardKitTypes + +public protocol RESTAccountHandling: Sendable { + func createAccount( + retryStrategy: REST.RetryStrategy, + completion: @escaping @Sendable ProxyCompletionHandler<NewAccountData> + ) -> Cancellable + + func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping @Sendable ProxyCompletionHandler<Account> + ) -> Cancellable + + func deleteAccount( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Void> + ) -> Cancellable +} + +extension REST { + public final class MullvadAccountProxy: RESTAccountHandling, @unchecked Sendable { + let transportProvider: APITransportProviderProtocol + let dispatchQueue: DispatchQueue + let operationQueue = AsyncOperationQueue() + let responseDecoder: JSONDecoder + + public init( + transportProvider: APITransportProviderProtocol, + dispatchQueue: DispatchQueue, + responseDecoder: JSONDecoder + ) { + self.transportProvider = transportProvider + self.dispatchQueue = dispatchQueue + self.responseDecoder = responseDecoder + } + + public func createAccount( + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<NewAccountData> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: NewAccountData.self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .createAccount(retryStrategy), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + public func getAccountData( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Account> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: Account.self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .getAccount(retryStrategy, accountNumber: accountNumber), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + public func deleteAccount( + accountNumber: String, + retryStrategy: RetryStrategy, + completion: @escaping ProxyCompletionHandler<Void> + ) -> Cancellable { + let request = APIRequest.deleteAccount(retryStrategy, accountNumber: accountNumber) + + let networkOperation = MullvadApiNetworkOperation( + name: request.name, + dispatchQueue: dispatchQueue, + request: request, + transportProvider: transportProvider, + responseDecoder: responseDecoder, + responseHandler: rustEmptyResponseHandler(), + completionHandler: completion + ) + + operationQueue.addOperation(networkOperation) + + return networkOperation + } + + private func createNetworkOperation<Success: Decodable>( + request: APIRequest, + responseHandler: RustResponseHandler<Success>, + completionHandler: @escaping @Sendable ProxyCompletionHandler<Success> + ) -> MullvadApiNetworkOperation<Success> { + let networkOperation = MullvadApiNetworkOperation( + name: request.name, + dispatchQueue: dispatchQueue, + request: request, + transportProvider: transportProvider, + responseDecoder: responseDecoder, + responseHandler: responseHandler, + completionHandler: completionHandler + ) + + operationQueue.addOperation(networkOperation) + + return networkOperation + } + } +} diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift index ea51e22508..68e7e41663 100644 --- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -9,6 +9,9 @@ public enum APIRequest: Codable, Sendable { case getAddressList(_ retryStrategy: REST.RetryStrategy) case getRelayList(_ retryStrategy: REST.RetryStrategy, etag: String?) + case createAccount(_ retryStrategy: REST.RetryStrategy) + case getAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) + case deleteAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) var name: String { switch self { @@ -16,12 +19,23 @@ public enum APIRequest: Codable, Sendable { "get-address-list" case .getRelayList: "get-relay-list" + case .createAccount: + "create-account" + case .getAccount: + "get-account" + case .deleteAccount: + "delete-account" } } var retryStrategy: REST.RetryStrategy { switch self { - case let .getAddressList(strategy), let .getRelayList(strategy, _): + case + let .getAddressList(strategy), + let .getRelayList(strategy, _), + let .createAccount(strategy), + let .getAccount(strategy, _), + let .deleteAccount(strategy, _): strategy } } diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift index fd64408e0e..298d3ac31f 100644 --- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift @@ -39,6 +39,26 @@ public struct MullvadApiRequestFactory: Sendable { retryStrategy.toRustStrategy(), etag )) + case let .getAccount(retryStrategy, accountNumber: accountNumber): + MullvadApiCancellable(handle: mullvad_api_get_account( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber + )) + case let .createAccount(retryStrategy): + MullvadApiCancellable(handle: mullvad_api_create_account( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy() + )) + case let .deleteAccount(retryStrategy, accountNumber: accountNumber): + MullvadApiCancellable(handle: mullvad_api_delete_account( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber + )) } } } diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 8a199559b5..12ce0fd4c0 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -51,7 +51,7 @@ typedef struct SwiftMullvadApiResponse { } SwiftMullvadApiResponse; typedef struct CompletionCookie { - void *_0; + void *inner; } CompletionCookie; typedef struct ProxyHandle { @@ -104,9 +104,63 @@ struct SwiftApiContext mullvad_api_init_new(const uint8_t *host, * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created * by calling `mullvad_api_init_new`. * - * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is - * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this - * type is intended to be used. + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). + * + * `account_number` must be a pointer to a null terminated string. + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_api_get_account(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number); + +/** + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_api_create_account(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy); + +/** + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). + * + * `account_number` must be a pointer to a null terminated string. + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_api_delete_account(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number); + +/** + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). * * This function is not safe to call multiple times with the same `CompletionCookie`. */ @@ -120,9 +174,9 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created * by calling `mullvad_api_init_new`. * - * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is - * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this - * type is intended to be used. + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). * * `etag` must be a pointer to a null terminated string. * @@ -131,7 +185,7 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co struct SwiftCancelHandle mullvad_api_get_relays(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, - const uint8_t *etag); + const char *etag); /** * Called by the Swift side to signal that a Mullvad API call should be cancelled. diff --git a/ios/MullvadTypes/NewAccountData.swift b/ios/MullvadTypes/NewAccountData.swift new file mode 100644 index 0000000000..c405f40394 --- /dev/null +++ b/ios/MullvadTypes/NewAccountData.swift @@ -0,0 +1,35 @@ +// +// NewAccountData.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-04-08. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +public struct NewAccountData: Decodable, Sendable { + public let id: String + public let expiry: Date + public let maxPorts: Int + public let canAddPorts: Bool + public let maxDevices: Int + public let canAddDevices: Bool + public let number: String + + public init( + id: String, + expiry: Date, + maxPorts: Int, + canAddPorts: Bool, + maxDevices: Int, + canAddDevices: Bool, + number: String + ) { + self.id = id + self.expiry = expiry + self.maxPorts = maxPorts + self.canAddPorts = canAddPorts + self.maxDevices = maxDevices + self.canAddDevices = canAddDevices + self.number = number + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index b9c42d131a..050bd50e1c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -645,8 +645,11 @@ 7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; }; 7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; }; 7AB3BEB52BD7A6CB00E34384 /* LocationViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */; }; + 7AB401852DA53D5300522E17 /* NewAccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB401842DA53D4E00522E17 /* NewAccountData.swift */; }; + 7AB401872DA53DA300522E17 /* NewAccountDataMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */; }; 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */; }; 7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; }; + 7AB73F6E2D9AAD0A00DA5E1D /* MullvadAccountProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */; }; 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */; }; 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */; }; 7AB9312F2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */; }; @@ -2166,8 +2169,11 @@ 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; }; 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = "<group>"; }; 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewControllerWrapper.swift; sourceTree = "<group>"; }; + 7AB401842DA53D4E00522E17 /* NewAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAccountData.swift; sourceTree = "<group>"; }; + 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAccountDataMock.swift; sourceTree = "<group>"; }; 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTests.swift; sourceTree = "<group>"; }; 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.swift; sourceTree = "<group>"; }; + 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAccountProxy.swift; sourceTree = "<group>"; }; 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiContext.swift; sourceTree = "<group>"; }; 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiResponse.swift; sourceTree = "<group>"; }; 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiNetworkOperation.swift; sourceTree = "<group>"; }; @@ -2989,6 +2995,7 @@ 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */, 58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */, 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */, + 7AB401842DA53D4E00522E17 /* NewAccountData.swift */, A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */, 58CC40EE24A601900019D96E /* ObserverList.swift */, 58CAFA01298530DC00BE19F7 /* Promise.swift */, @@ -4177,6 +4184,7 @@ 7A2C0E8A2D8B13DB003D8048 /* APIHandlers */ = { isa = PBXGroup; children = ( + 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */, 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */, ); path = APIHandlers; @@ -4644,6 +4652,7 @@ children = ( 449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */, 449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */, + 7AB401862DA53D9B00522E17 /* NewAccountDataMock.swift */, ); path = MullvadTypes; sourceTree = "<group>"; @@ -5757,6 +5766,7 @@ A90763BE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift in Sources */, A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */, 06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */, + 7AB73F6E2D9AAD0A00DA5E1D /* MullvadAccountProxy.swift in Sources */, A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */, A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */, A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */, @@ -6653,6 +6663,7 @@ 58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */, 7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */, 58D22413294C90210029F5F8 /* RelayConstraints.swift in Sources */, + 7AB401852DA53D5300522E17 /* NewAccountData.swift in Sources */, A97275562CE36CAE00029F15 /* DaitaV2Parameters.swift in Sources */, 7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */, 58D22414294C90210029F5F8 /* RelayLocation.swift in Sources */, @@ -6834,6 +6845,7 @@ 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */, 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */, F0FA160C2D7F2BF2007E2546 /* ServerRelaysResponse+Stubs.swift in Sources */, + 7AB401872DA53DA300522E17 /* NewAccountDataMock.swift in Sources */, F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */, F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */, ); diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 58dac8181d..e811732a35 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -839,4 +839,4 @@ extension DeviceState { var splitViewMode: UISplitViewController.DisplayMode { isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly } -} +} // swiftlint:disable:this file_length diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index 3ad770653a..5c4dfd8836 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -225,10 +225,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchec completionHandler: @escaping @Sendable (StorePaymentManagerError?) -> Void ) { let accountOperation = ResultBlockOperation<Account>(dispatchQueue: .main) { finish in - self.accountsProxy.getAccountData(accountNumber: accountNumber).execute( - retryStrategy: .default, - completionHandler: finish - ) + self.accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .default, completion: finish) } accountOperation.addObserver(BackgroundObserver( diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index ef45960320..07467a5ca8 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -267,7 +267,8 @@ class SetAccountOperation: ResultOperation<StoredAccountData?>, @unchecked Senda ) { logger.debug("Request account data...") - let task = accountsProxy.getAccountData(accountNumber: accountNumber).execute( + let task = accountsProxy.getAccountData( + accountNumber: accountNumber, retryStrategy: .default ) { [self] result in dispatchQueue.async { [self] in diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift index 9af9799702..0e06dbfb6f 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift @@ -36,13 +36,15 @@ class UpdateAccountDataOperation: ResultOperation<Void>, @unchecked Sendable { return } - task = accountsProxy.getAccountData(accountNumber: accountData.number).execute( - retryStrategy: .default - ) { result in - self.dispatchQueue.async { - self.didReceiveAccountData(result: result) + task = accountsProxy.getAccountData( + accountNumber: accountData.number, + retryStrategy: .default, + completion: { result in + self.dispatchQueue.async { + self.didReceiveAccountData(result: result) + } } - } + ) } override func operationDidCancel() { diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift index cf76e17fe3..fe348f0a7d 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift @@ -57,15 +57,19 @@ final class RedeemVoucherInteractor: @unchecked Sendable { } private func verifyVoucherAsAccount(code: String) { - let executer = accountsProxy.getAccountData(accountNumber: code) - tasks.append(executer.execute { [weak self] result in + let task = accountsProxy.getAccountData( + accountNumber: code, + retryStrategy: .noRetry + ) { [weak self] result in guard let self, case .success = result else { return } showLogoutDialog?() preferredAccountNumber = code - }) + } + + tasks.append(task) } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift index 9066ed59e9..c3e5349314 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift @@ -25,7 +25,8 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible { "VPN_SETTINGS_LOCAL_NETWORK_SHARING", tableName: "LocalNetworkSharing", value: """ - This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc. + This feature allows access to other devices on the local network, such as for sharing, printing, \ + streaming, etc. Attention: toggling “Local network sharing” requires restarting the VPN connection. """, comment: "" diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift index 12e1b933e9..b44927f931 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift @@ -84,7 +84,7 @@ class TunnelManagerTests: XCTestCase { } func testLogInStartsKeyRotations() async throws { - accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) + accountProxy.createAccountResult = .success(NewAccountData.mockValue()) let tunnelManager = TunnelManager( backgroundTaskProvider: application, @@ -102,7 +102,7 @@ class TunnelManagerTests: XCTestCase { } func testLogOutStopsKeyRotations() async throws { - accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) + accountProxy.createAccountResult = .success(NewAccountData.mockValue()) let tunnelManager = TunnelManager( backgroundTaskProvider: application, @@ -125,7 +125,7 @@ class TunnelManagerTests: XCTestCase { let blockedExpectation = expectation(description: "Relay constraints aren't satisfied!") let connectedExpectation = expectation(description: "Connected!") - accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) + accountProxy.createAccountResult = .success(NewAccountData.mockValue()) let relaySelector = RelaySelectorStub { _ in try RelaySelectorStub.unsatisfied().selectRelays( @@ -196,7 +196,7 @@ class TunnelManagerTests: XCTestCase { var connectedExpectation = expectation(description: "Connected!") let disconnectedExpectation = expectation(description: "Disconnected!") - accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) + accountProxy.createAccountResult = .success(NewAccountData.mockValue()) let relaySelector = RelaySelectorStub { _ in try RelaySelectorStub.nonFallible().selectRelays( diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift index e8738bb1d6..5110a8808e 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift @@ -25,7 +25,11 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol { accountNumber: String, completion: @escaping @Sendable (Result<Account, Error>) -> Void ) -> Cancellable { - accountsProxy.getAccountData(accountNumber: accountNumber).execute(completionHandler: completion) + accountsProxy.getAccountData( + accountNumber: accountNumber, + retryStrategy: .noRetry, + completion: completion + ) } func getDevice( diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index 1ced489080..2d2814d099 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -506,15 +506,24 @@ impl AccountsProxy { &self, account: AccountNumber, ) -> impl Future<Output = Result<AccountData, rest::Error>> + use<> { + let request = self.get_data_response(account); + + async move { request.await?.deserialize().await } + } + + pub fn get_data_response( + &self, + account: AccountNumber, + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { let service = self.handle.service.clone(); let factory = self.handle.factory.clone(); + async move { let request = factory .get(&format!("{ACCOUNTS_URL_PREFIX}/accounts/me"))? .expected_status(&[StatusCode::OK]) .account(account)?; - let response = service.request(request).await?; - response.deserialize().await + service.request(request).await } } @@ -526,6 +535,17 @@ impl AccountsProxy { number: AccountNumber, } + let request = self.create_account_response(); + + async move { + let account: AccountCreationResponse = request.await?.deserialize().await?; + Ok(account.number) + } + } + + pub fn create_account_response( + &self, + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { let service = self.handle.service.clone(); let factory = self.handle.factory.clone(); @@ -533,9 +553,7 @@ impl AccountsProxy { let request = factory .post(&format!("{ACCOUNTS_URL_PREFIX}/accounts"))? .expected_status(&[StatusCode::CREATED]); - let response = service.request(request).await?; - let account: AccountCreationResponse = response.deserialize().await?; - Ok(account.number) + service.request(request).await } } diff --git a/mullvad-ios/src/api_client/account.rs b/mullvad-ios/src/api_client/account.rs new file mode 100644 index 0000000000..ff3b0294cf --- /dev/null +++ b/mullvad-ios/src/api_client/account.rs @@ -0,0 +1,196 @@ +use std::ffi::CStr; +use std::os::raw::c_char; + +use mullvad_api::{ + rest::{self, MullvadRestHandle}, + AccountsProxy, +}; + +use super::{ + cancellation::{RequestCancelHandle, SwiftCancelHandle}, + completion::{CompletionCookie, SwiftCompletionHandler}, + do_request, do_request_with_empty_body, + response::SwiftMullvadApiResponse, + retry_strategy::{RetryStrategy, SwiftRetryStrategy}, + SwiftApiContext, +}; + +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_api_init_new`. +/// +/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift +/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` +/// when completion finishes (in completion.finish). +/// +/// `account_number` must be a pointer to a null terminated string. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[no_mangle] +pub unsafe extern "C" fn mullvad_api_get_account( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, +) -> SwiftCancelHandle { + let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie)); + + let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { + completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); + return SwiftCancelHandle::empty(); + }; + + let api_context = api_context.into_rust_context(); + let retry_strategy = unsafe { retry_strategy.into_rust() }; + // SAFETY: See param documentation for `account_number`. + let account_number = unsafe { CStr::from_ptr(account_number.cast()) } + .to_str() + .unwrap(); + let account_number = String::from(account_number); + + let completion = completion_handler.clone(); + let task = tokio_handle.clone().spawn(async move { + match mullvad_api_get_account_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler.clone()).into_swift() +} + +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_api_init_new`. +/// +/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift +/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` +/// when completion finishes (in completion.finish). +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[no_mangle] +pub unsafe extern "C" fn mullvad_api_create_account( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, +) -> SwiftCancelHandle { + let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie)); + + let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { + completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); + return SwiftCancelHandle::empty(); + }; + + let api_context = api_context.into_rust_context(); + let retry_strategy = unsafe { retry_strategy.into_rust() }; + + let completion = completion_handler.clone(); + let task = tokio_handle.clone().spawn(async move { + match mullvad_api_create_account_inner(api_context.rest_handle(), retry_strategy).await { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler.clone()).into_swift() +} + +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_api_init_new`. +/// +/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift +/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` +/// when completion finishes (in completion.finish). +/// +/// `account_number` must be a pointer to a null terminated string. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[no_mangle] +pub unsafe extern "C" fn mullvad_api_delete_account( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, +) -> SwiftCancelHandle { + let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie)); + + let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { + completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); + return SwiftCancelHandle::empty(); + }; + + let api_context = api_context.into_rust_context(); + let retry_strategy = unsafe { retry_strategy.into_rust() }; + // SAFETY: See param documentation for `account_number`. + let account_number = unsafe { CStr::from_ptr(account_number.cast()) } + .to_str() + .unwrap(); + let account_number = String::from(account_number); + + let completion = completion_handler.clone(); + let task = tokio_handle.clone().spawn(async move { + match mullvad_api_delete_account_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler.clone()).into_swift() +} + +async fn mullvad_api_get_account_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = AccountsProxy::new(rest_client); + let future_factory = || api.get_data_response(account_number.clone()); + + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_api_create_account_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = AccountsProxy::new(rest_client); + let future_factory = || api.create_account_response(); + + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_api_delete_account_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = AccountsProxy::new(rest_client); + let future_factory = || api.delete_account(account_number.clone()); + + do_request_with_empty_body(retry_strategy, future_factory).await +} diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs index c918dda61f..c6c14bb14c 100644 --- a/mullvad-ios/src/api_client/api.rs +++ b/mullvad-ios/src/api_client/api.rs @@ -1,4 +1,5 @@ use std::ffi::CStr; +use std::os::raw::c_char; use mullvad_api::{ rest::{self, MullvadRestHandle}, @@ -19,9 +20,9 @@ use super::{ /// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created /// by calling `mullvad_api_init_new`. /// -/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is -/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this -/// type is intended to be used. +/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift +/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` +/// when completion finishes (in completion.finish). /// /// This function is not safe to call multiple times with the same `CompletionCookie`. #[no_mangle] @@ -30,7 +31,7 @@ pub unsafe extern "C" fn mullvad_api_get_addresses( completion_cookie: *mut libc::c_void, retry_strategy: SwiftRetryStrategy, ) -> SwiftCancelHandle { - let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie)); + let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie)); let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); @@ -59,9 +60,9 @@ pub unsafe extern "C" fn mullvad_api_get_addresses( /// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created /// by calling `mullvad_api_init_new`. /// -/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is -/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this -/// type is intended to be used. +/// This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift +/// object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` +/// when completion finishes (in completion.finish). /// /// `etag` must be a pointer to a null terminated string. /// @@ -71,9 +72,9 @@ pub unsafe extern "C" fn mullvad_api_get_relays( api_context: SwiftApiContext, completion_cookie: *mut libc::c_void, retry_strategy: SwiftRetryStrategy, - etag: *const u8, + etag: *const c_char, ) -> SwiftCancelHandle { - let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie)); + let completion_handler = SwiftCompletionHandler::new(CompletionCookie::new(completion_cookie)); let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); @@ -85,6 +86,7 @@ pub unsafe extern "C" fn mullvad_api_get_relays( let mut maybe_etag: Option<String> = None; if !etag.is_null() { + // SAFETY: See param documentation for `etag`. let unwrapped_tag = unsafe { CStr::from_ptr(etag.cast()) }.to_str().unwrap(); maybe_etag = Some(String::from(unwrapped_tag)); } diff --git a/mullvad-ios/src/api_client/completion.rs b/mullvad-ios/src/api_client/completion.rs index 11a05acf8e..db15a6b8b6 100644 --- a/mullvad-ios/src/api_client/completion.rs +++ b/mullvad-ios/src/api_client/completion.rs @@ -20,8 +20,16 @@ extern "C" { } #[repr(C)] -pub struct CompletionCookie(pub *mut std::ffi::c_void); +pub struct CompletionCookie { + inner: *mut std::ffi::c_void, +} unsafe impl Send for CompletionCookie {} +impl CompletionCookie { + /// `inner` must be pointing to a valid instance of Swift object `MullvadApiCompletion`. + pub unsafe fn new(inner: *mut std::ffi::c_void) -> Self { + Self { inner } + } +} #[derive(Clone)] pub struct SwiftCompletionHandler { diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs index 98b41c103f..fd9fde7aa7 100644 --- a/mullvad-ios/src/api_client/mod.rs +++ b/mullvad-ios/src/api_client/mod.rs @@ -9,6 +9,7 @@ use response::SwiftMullvadApiResponse; use retry_strategy::RetryStrategy; use talpid_future::retry::retry_future; +mod account; mod api; mod cancellation; mod completion; @@ -93,12 +94,34 @@ where F: Fn() -> T, T: Future<Output = Result<rest::Response<hyper::body::Incoming>, rest::Error>>, { + let response = retry_request(retry_strategy, future_factory).await?; + SwiftMullvadApiResponse::with_body(response).await +} + +async fn do_request_with_empty_body<F, T>( + retry_strategy: RetryStrategy, + future_factory: F, +) -> Result<SwiftMullvadApiResponse, rest::Error> +where + F: Fn() -> T, + T: Future<Output = Result<(), rest::Error>>, +{ + retry_request(retry_strategy, future_factory).await?; + Ok(SwiftMullvadApiResponse::ok()) +} + +async fn retry_request<F, T, U>( + retry_strategy: RetryStrategy, + future_factory: F, +) -> Result<U, rest::Error> +where + F: Fn() -> T, + T: Future<Output = Result<U, rest::Error>>, +{ let should_retry = |result: &Result<_, rest::Error>| match result { Err(err) => err.is_network_error(), Ok(_) => false, }; - let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?; - - SwiftMullvadApiResponse::with_body(response).await + retry_future(future_factory, should_retry, retry_strategy.delays()).await } diff --git a/mullvad-ios/src/api_client/response.rs b/mullvad-ios/src/api_client/response.rs index ac6c0feecc..3b017e37c6 100644 --- a/mullvad-ios/src/api_client/response.rs +++ b/mullvad-ios/src/api_client/response.rs @@ -5,7 +5,7 @@ use std::{ use mullvad_api::{ rest::{self, Response}, - RelayListProxy, + RelayListProxy, StatusCode, }; #[repr(C)] @@ -49,6 +49,18 @@ impl SwiftMullvadApiResponse { }) } + pub fn ok() -> Self { + Self { + success: true, + error_description: null_mut(), + body: null_mut(), + body_size: 0, + etag: null_mut(), + status_code: StatusCode::NO_CONTENT.as_u16(), + server_response_code: null_mut(), + } + } + pub fn rest_error(err: mullvad_api::rest::Error) -> Self { if err.is_aborted() { return Self::cancelled(); |
