diff options
| -rw-r--r-- | ios/MullvadMockData/MullvadREST/DevicesProxy+Stubs.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift | 38 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift | 18 | ||||
| -rw-r--r-- | ios/MullvadREST/MullvadAPI/APIHandlers/MullvadDeviceProxy.swift | 147 | ||||
| -rw-r--r-- | ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift | 45 | ||||
| -rw-r--r-- | ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift | 44 | ||||
| -rw-r--r-- | ios/MullvadRustRuntime/include/mullvad_rust_runtime.h | 113 | ||||
| -rw-r--r-- | ios/MullvadTypes/RESTTypes.swift | 40 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/SetAccountOperation.swift | 2 | ||||
| -rw-r--r-- | mullvad-api/src/device.rs | 134 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/device.rs | 368 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/mod.rs | 19 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/problem_report.rs | 11 |
14 files changed, 883 insertions, 102 deletions
diff --git a/ios/MullvadMockData/MullvadREST/DevicesProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/DevicesProxy+Stubs.swift index 6748805b2c..acf3d6369a 100644 --- a/ios/MullvadMockData/MullvadREST/DevicesProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/DevicesProxy+Stubs.swift @@ -41,7 +41,7 @@ struct DevicesProxyStub: DeviceHandling { func createDevice( accountNumber: String, - request: REST.CreateDeviceRequest, + request: CreateDeviceRequest, retryStrategy: REST.RetryStrategy, completion: @escaping ProxyCompletionHandler<Device> ) -> Cancellable { diff --git a/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift b/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift index 20dd0d0539..5a6d369ef3 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift @@ -26,7 +26,7 @@ public protocol DeviceHandling: Sendable { func createDevice( accountNumber: String, - request: REST.CreateDeviceRequest, + request: CreateDeviceRequest, retryStrategy: REST.RetryStrategy, completion: @escaping @Sendable ProxyCompletionHandler<Device> ) -> Cancellable @@ -309,40 +309,4 @@ extension REST { return executor.execute(retryStrategy: retryStrategy, completionHandler: completion) } } - - public struct CreateDeviceRequest: Encodable, Sendable { - let publicKey: PublicKey - let hijackDNS: Bool - - public init(publicKey: PublicKey, hijackDNS: Bool) { - self.publicKey = publicKey - self.hijackDNS = hijackDNS - } - - private enum CodingKeys: String, CodingKey { - case hijackDNS = "hijackDns" - case publicKey = "pubkey" - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(publicKey.base64Key, forKey: .publicKey) - try container.encode(hijackDNS, forKey: .hijackDNS) - } - } - - private struct RotateDeviceKeyRequest: Encodable, Sendable { - let publicKey: PublicKey - - private enum CodingKeys: String, CodingKey { - case publicKey = "pubkey" - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(publicKey.base64Key, forKey: .publicKey) - } - } } diff --git a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift index ff3751c5bd..4d6f5eaa02 100644 --- a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift +++ b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift @@ -71,11 +71,29 @@ extension REST { } public func createAccountsProxy() -> RESTAccountHandling { + #if DEBUG + MullvadAccountProxy( + transportProvider: configuration.apiTransportProvider, + dispatchQueue: DispatchQueue(label: "MullvadAccountProxy.dispatchQueue"), + responseDecoder: Coding.makeJSONDecoder() + ) + + #else REST.AccountsProxy(configuration: configuration) + #endif } public func createDevicesProxy() -> DeviceHandling { + #if DEBUG + MullvadDeviceProxy( + transportProvider: configuration.apiTransportProvider, + dispatchQueue: DispatchQueue(label: "MullvadDeviceProxy.dispatchQueue"), + responseDecoder: Coding.makeJSONDecoder() + ) + + #else REST.DevicesProxy(configuration: configuration) + #endif } } } diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadDeviceProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadDeviceProxy.swift new file mode 100644 index 0000000000..f2bdb027cf --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadDeviceProxy.swift @@ -0,0 +1,147 @@ +// +// MullvadDeviceProxy.swift +// MullvadVPN +// +// Created by Mojgan on 2025-04-02. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// +import MullvadRustRuntime +import MullvadTypes +import Operations +import WireGuardKitTypes + +extension REST { + final class MullvadDeviceProxy: DeviceHandling, @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 + } + + func getDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Device> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: Device.self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .getDevice(retryStrategy, accountNumber: accountNumber, identifier: identifier), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + func getDevices( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<[Device]> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: [Device].self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .getDevices(retryStrategy, accountNumber: accountNumber), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + func createDevice( + accountNumber: String, + request: CreateDeviceRequest, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Device> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: Device.self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .createDevice(retryStrategy, accountNumber: accountNumber, request: request), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + func deleteDevice( + accountNumber: String, + identifier: String, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Bool> + ) -> Cancellable { + let responseHandler = rustEmptyResponseHandler() + + return createNetworkOperation( + request: .deleteDevice(retryStrategy, accountNumber: accountNumber, identifier: identifier), + responseHandler: responseHandler + ) { result in + if case let .failure(err) = result { + completion(.failure(err)) + } else { + completion(.success(true)) + } + } + } + + func rotateDeviceKey( + accountNumber: String, + identifier: String, + publicKey: PublicKey, + retryStrategy: REST.RetryStrategy, + completion: @escaping ProxyCompletionHandler<Device> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: Device.self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .rotateDeviceKey( + retryStrategy, + accountNumber: accountNumber, + identifier: identifier, + publicKey: publicKey + ), + responseHandler: responseHandler, + completionHandler: completion + ) + } + + private func createNetworkOperation<Success: Any>( + 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 92b6bb89b1..ab1087fe5e 100644 --- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -6,16 +6,31 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // import MullvadTypes +@preconcurrency import WireGuardKitTypes public enum APIRequest: Codable, Sendable { + // Api Proxy case getAddressList(_ retryStrategy: REST.RetryStrategy) case getRelayList(_ retryStrategy: REST.RetryStrategy, etag: String?) case sendProblemReport(_ retryStrategy: REST.RetryStrategy, problemReportRequest: ProblemReportRequest) + // Account Proxy case createAccount(_ retryStrategy: REST.RetryStrategy) case getAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) case deleteAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) + // Device Proxy + case getDevice(_ retryStrategy: REST.RetryStrategy, accountNumber: String, identifier: String) + case getDevices(_ retryStrategy: REST.RetryStrategy, accountNumber: String) + case createDevice(_ retryStrategy: REST.RetryStrategy, accountNumber: String, request: CreateDeviceRequest) + case deleteDevice(_ retryStrategy: REST.RetryStrategy, accountNumber: String, identifier: String) + case rotateDeviceKey( + _ retryStrategy: REST.RetryStrategy, + accountNumber: String, + identifier: String, + publicKey: PublicKey + ) + var name: String { switch self { case .getAddressList: @@ -30,19 +45,33 @@ public enum APIRequest: Codable, Sendable { "get-account" case .deleteAccount: "delete-account" + case .getDevice: + "get-device" + case .getDevices: + "get-devices" + case .deleteDevice: + "delete-device" + case .rotateDeviceKey: + "rotate-device-key" + case .createDevice: + "create-device" } } var retryStrategy: REST.RetryStrategy { switch self { - case - let .getAddressList(strategy), - let .getRelayList(strategy, _), - let .sendProblemReport(strategy, _), - let .createAccount(strategy), - let .getAccount(strategy, _), - let .deleteAccount(strategy, _): - strategy + case let .getAddressList(strategy), + let .getRelayList(strategy, _), + let .sendProblemReport(strategy, _), + let .createAccount(strategy), + let .getAccount(strategy, _), + let .deleteAccount(strategy, _), + let .createDevice(strategy, _, _), + let .getDevice(strategy, _, _), + let .getDevices(strategy, _), + let .deleteDevice(strategy, _, _), + let .rotateDeviceKey(strategy, _, _, _): + return strategy } } } diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift index 0b3271c80b..3f29468d9b 100644 --- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift @@ -67,6 +67,50 @@ public struct MullvadApiRequestFactory: Sendable { retryStrategy.toRustStrategy(), accountNumber )) + + // Device Proxy + case let .getDevice(retryStrategy, accountNumber: accountNumber, identifier): + return MullvadApiCancellable(handle: mullvad_ios_get_device( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber, + identifier + )) + + case let .getDevices(retryStrategy, accountNumber): + return MullvadApiCancellable(handle: mullvad_ios_get_devices( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber + )) + + case let .deleteDevice(retryStrategy, accountNumber, identifier): + return MullvadApiCancellable(handle: mullvad_ios_delete_device( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber, + identifier + )) + case let .rotateDeviceKey(retryStrategy, accountNumber, identifier, publicKey): + return MullvadApiCancellable(handle: mullvad_ios_rotate_device_key( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber, + identifier, + publicKey.rawValue.map { $0 } + )) + case let .createDevice(retryStrategy, accountNumber, request): + return MullvadApiCancellable(handle: mullvad_ios_create_device( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + accountNumber, + request.publicKey.rawValue.map { $0 } + )) } } } diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 6300b04902..b4f4a65e2f 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -361,6 +361,119 @@ extern void mullvad_api_completion_finish(struct SwiftMullvadApiResponse respons struct CompletionCookie completion_cookie); /** + * Get device info via the Mullvad API client. + * + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_ios_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_ios_completion_finish` + * when completion finishes (in completion.finish). + * + * the `account_number` must be a pointer to a null terminated string. + * the `identifier` 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_ios_get_device(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number, + const char *identifier); + +/** + * Get devices info via the Mullvad API client. + * + * # 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). + * + * the `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_ios_get_devices(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number); + +/** + * create device via the Mullvad API client. + * + * # 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). + * + * the `account_number` must be a pointer to a null terminated string. + * the `identifier` must be a pointer to a null terminated string. + * the `public_key` pointer must be a valid pointer to 32 unsigned bytes. + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_ios_create_device(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number, + const uint8_t *public_key); + +/** + * delete device via the Mullvad API client. + * + * # 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). + * + * the `account_number` must be a pointer to a null terminated string. + * the `identifier` 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_ios_delete_device(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number, + const char *identifier); + +/** + * rotate device key via the Mullvad API client. + * + * # 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). + * + * the `account_number` must be a pointer to a null terminated string. + * the `identifier` must be a pointer to a null terminated string. + * the `public_key` pointer must be a valid pointer to 32 unsigned bytes. + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_ios_rotate_device_key(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + const char *account_number, + const char *identifier, + const uint8_t *public_key); + +/** * Converts parameters into a boxed `Shadowsocks` configuration that is safe * to send across the FFI boundary * diff --git a/ios/MullvadTypes/RESTTypes.swift b/ios/MullvadTypes/RESTTypes.swift index 6ba5c08297..dee960ca27 100644 --- a/ios/MullvadTypes/RESTTypes.swift +++ b/ios/MullvadTypes/RESTTypes.swift @@ -69,3 +69,43 @@ public struct ProblemReportRequest: Codable, Sendable { self.metadata = metadata } } + +public struct CreateDeviceRequest: Codable, Sendable { + public let publicKey: PublicKey + public let hijackDNS: Bool + + public init(publicKey: PublicKey, hijackDNS: Bool) { + self.publicKey = publicKey + self.hijackDNS = hijackDNS + } + + private enum CodingKeys: String, CodingKey { + case hijackDNS = "hijackDns" + case publicKey = "pubkey" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(publicKey.base64Key, forKey: .publicKey) + try container.encode(hijackDNS, forKey: .hijackDNS) + } +} + +public struct RotateDeviceKeyRequest: Codable, Sendable { + let publicKey: PublicKey + + public init(publicKey: PublicKey) { + self.publicKey = publicKey + } + + private enum CodingKeys: String, CodingKey { + case publicKey = "pubkey" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(publicKey.base64Key, forKey: .publicKey) + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 26406d4230..9d291a7ae5 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -1026,6 +1026,7 @@ F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; }; F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; }; F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + F0A89CB32D9D6C2100580C27 /* MullvadDeviceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */; }; F0A89CB52D9D864B00580C27 /* RustProblemReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */; }; F0A89CB72D9D923300580C27 /* String+UnsafePointer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */; }; F0ACE30D2BE4E478006D5333 /* MullvadMockData.h in Headers */ = {isa = PBXBuildFile; fileRef = F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -2454,6 +2455,7 @@ F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = "<group>"; }; F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = "<group>"; }; F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = "<group>"; }; + F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadDeviceProxy.swift; sourceTree = "<group>"; }; F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+UnsafePointer.swift"; sourceTree = "<group>"; }; F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadMockData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadMockData.h; sourceTree = "<group>"; }; @@ -4223,6 +4225,7 @@ children = ( 7AB73F6D2D9AAD0400DA5E1D /* MullvadAccountProxy.swift */, 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */, + F0A89CB22D9D6C1400580C27 /* MullvadDeviceProxy.swift */, ); path = APIHandlers; sourceTree = "<group>"; @@ -5854,6 +5857,7 @@ A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */, A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */, 7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */, + F0A89CB32D9D6C2100580C27 /* MullvadDeviceProxy.swift in Sources */, F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */, 06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */, A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */, diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 07467a5ca8..c018078989 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -392,7 +392,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?>, @unchecked Senda completion: @escaping @Sendable (Result<NewDevice, Error>) -> Void ) { let privateKey = PrivateKey() - let request = REST.CreateDeviceRequest(publicKey: privateKey.publicKey, hijackDNS: false) + let request = CreateDeviceRequest(publicKey: privateKey.publicKey, hijackDNS: false) logger.debug("Create device...") diff --git a/mullvad-api/src/device.rs b/mullvad-api/src/device.rs index 062c06f28b..770c86a642 100644 --- a/mullvad-api/src/device.rs +++ b/mullvad-api/src/device.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use http::StatusCode; +use hyper::body::Incoming; use mullvad_types::{ account::AccountNumber, device::{Device, DeviceId, DeviceName}, @@ -39,26 +40,9 @@ impl DevicesProxy { ) -> impl Future< Output = Result<(Device, mullvad_types::wireguard::AssociatedAddresses), rest::Error>, > + use<> { - #[derive(serde::Serialize)] - struct DeviceSubmission { - pubkey: wireguard::PublicKey, - hijack_dns: bool, - } - - let submission = DeviceSubmission { - pubkey, - hijack_dns: false, - }; - - let service = self.handle.service.clone(); - let factory = self.handle.factory.clone(); + let request = self.create_response(account, pubkey); async move { - let request = factory - .post_json(&format!("{ACCOUNTS_URL_PREFIX}/devices"), &submission)? - .account(account)? - .expected_status(&[StatusCode::CREATED]); - let response = service.request(request).await?; let DeviceResponse { id, name, @@ -68,7 +52,7 @@ impl DevicesProxy { hijack_dns, created, .. - } = response.deserialize().await?; + } = request.await?.deserialize().await?; Ok(( Device { @@ -91,14 +75,10 @@ impl DevicesProxy { account: AccountNumber, id: DeviceId, ) -> impl Future<Output = Result<Device, rest::Error>> + use<> { - let service = self.handle.service.clone(); - let factory = self.handle.factory.clone(); + let request = self.get_response(account, id); async move { - let request = factory - .get(&format!("{ACCOUNTS_URL_PREFIX}/devices/{id}"))? - .expected_status(&[StatusCode::OK]) - .account(account)?; - service.request(request).await?.deserialize().await + let data = request.await?.deserialize().await?; + Ok(data) } } @@ -106,14 +86,10 @@ impl DevicesProxy { &self, account: AccountNumber, ) -> impl Future<Output = Result<Vec<Device>, rest::Error>> + use<> { - let service = self.handle.service.clone(); - let factory = self.handle.factory.clone(); + let request = self.list_response(account); async move { - let request = factory - .get(&format!("{ACCOUNTS_URL_PREFIX}/devices"))? - .expected_status(&[StatusCode::OK]) - .account(account)?; - service.request(request).await?.deserialize().await + let data = request.await?.deserialize().await?; + Ok(data) } } @@ -141,6 +117,59 @@ impl DevicesProxy { pubkey: wireguard::PublicKey, ) -> impl Future<Output = Result<mullvad_types::wireguard::AssociatedAddresses, rest::Error>> + use<> { + let request = self.replace_wg_key_response(account, id, pubkey); + async move { + let DeviceResponse { + ipv4_address, + ipv6_address, + .. + } = request.await?.deserialize().await?; + Ok(mullvad_types::wireguard::AssociatedAddresses { + ipv4_address, + ipv6_address, + }) + } + } + + pub fn get_response( + &self, + account: AccountNumber, + id: DeviceId, + ) -> 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}/devices/{id}"))? + .expected_status(&[StatusCode::OK]) + .account(account)?; + service.request(request).await + } + } + + pub fn list_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}/devices"))? + .expected_status(&[StatusCode::OK]) + .account(account)?; + service.request(request).await + } + } + + pub fn replace_wg_key_response( + &self, + account: AccountNumber, + id: DeviceId, + pubkey: wireguard::PublicKey, + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { #[derive(serde::Serialize)] struct RotateDevicePubkey { pubkey: wireguard::PublicKey, @@ -158,16 +187,35 @@ impl DevicesProxy { )? .expected_status(&[StatusCode::OK]) .account(account)?; - let response = service.request(request).await?; - let DeviceResponse { - ipv4_address, - ipv6_address, - .. - } = response.deserialize().await?; - Ok(mullvad_types::wireguard::AssociatedAddresses { - ipv4_address, - ipv6_address, - }) + service.request(request).await + } + } + + pub fn create_response( + &self, + account: AccountNumber, + pubkey: wireguard::PublicKey, + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { + #[derive(serde::Serialize)] + struct DeviceSubmission { + pubkey: wireguard::PublicKey, + hijack_dns: bool, + } + + let submission = DeviceSubmission { + pubkey, + hijack_dns: false, + }; + + let service = self.handle.service.clone(); + let factory = self.handle.factory.clone(); + + async move { + let request = factory + .post_json(&format!("{ACCOUNTS_URL_PREFIX}/devices"), &submission)? + .account(account)? + .expected_status(&[StatusCode::CREATED]); + service.request(request).await } } } diff --git a/mullvad-ios/src/api_client/device.rs b/mullvad-ios/src/api_client/device.rs new file mode 100644 index 0000000000..8ca287ebb2 --- /dev/null +++ b/mullvad-ios/src/api_client/device.rs @@ -0,0 +1,368 @@ +use libc::c_char; +use mullvad_api::{ + rest::{self, MullvadRestHandle}, + DevicesProxy, +}; + +use super::{ + cancellation::{RequestCancelHandle, SwiftCancelHandle}, + completion::{CompletionCookie, SwiftCompletionHandler}, + do_request, do_request_with_empty_body, get_string, + response::SwiftMullvadApiResponse, + retry_strategy::{RetryStrategy, SwiftRetryStrategy}, + SwiftApiContext, +}; +use std::ptr; +use talpid_types::net::wireguard; +use talpid_types::net::wireguard::PublicKey; + +/// Get device info via the Mullvad API client. +/// +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_ios_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_ios_completion_finish` +/// when completion finishes (in completion.finish). +/// +/// the `account_number` must be a pointer to a null terminated string. +/// the `identifier` must be a pointer to a null terminated string. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_ios_get_device( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, + identifier: *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.rust_context(); + // Safety: The caller must guarantee that `retry_strategy` is not null and has not been freed + let retry_strategy = unsafe { retry_strategy.into_rust() }; + let account_number = get_string(account_number); + let identifier = get_string(identifier); + + let completion = completion_handler.clone(); + let task = tokio_handle.spawn(async move { + match mullvad_ios_get_device_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + identifier, + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler).into_swift() +} + +/// Get devices info via the Mullvad API client. +/// +/// # 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). +/// +/// the `account_number` must be a pointer to a null terminated string. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_ios_get_devices( + 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.rust_context(); + // Safety: The caller must guarantee that `retry_strategy` is not null and has not been freed + let retry_strategy = unsafe { retry_strategy.into_rust() }; + let account_number = get_string(account_number); + + let completion = completion_handler.clone(); + let task = tokio_handle.spawn(async move { + match mullvad_ios_get_devices_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).into_swift() +} + +/// create device via the Mullvad API client. +/// +/// # 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). +/// +/// the `account_number` must be a pointer to a null terminated string. +/// the `identifier` must be a pointer to a null terminated string. +/// the `public_key` pointer must be a valid pointer to 32 unsigned bytes. +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_ios_create_device( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, + public_key: *const u8, +) -> 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.rust_context(); + // Safety: The caller must guarantee that `retry_strategy` is not null and has not been freed + let retry_strategy = unsafe { retry_strategy.into_rust() }; + let account_number = get_string(account_number); + // Safety: `public_key` pointer must be a valid pointer to 32 unsigned bytes. + let pub_key: [u8; 32] = unsafe { ptr::read(public_key as *const [u8; 32]) }; + + let completion = completion_handler.clone(); + let task = tokio_handle.spawn(async move { + match mullvad_ios_create_device_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + PublicKey::from(pub_key), + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler).into_swift() +} + +/// delete device via the Mullvad API client. +/// +/// # 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). +/// +/// the `account_number` must be a pointer to a null terminated string. +/// the `identifier` must be a pointer to a null terminated string. +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_ios_delete_device( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, + identifier: *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.rust_context(); + // Safety: The caller must guarantee that `retry_strategy` is not null and has not been freed + let retry_strategy = unsafe { retry_strategy.into_rust() }; + let account_number = get_string(account_number); + let identifier = get_string(identifier); + + let completion = completion_handler.clone(); + let task = tokio_handle.spawn(async move { + match mullvad_ios_delete_device_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + identifier, + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler).into_swift() +} + +/// rotate device key via the Mullvad API client. +/// +/// # 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). +/// +/// the `account_number` must be a pointer to a null terminated string. +/// the `identifier` must be a pointer to a null terminated string. +/// the `public_key` pointer must be a valid pointer to 32 unsigned bytes. +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_ios_rotate_device_key( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + account_number: *const c_char, + identifier: *const c_char, + public_key: *const u8, +) -> 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.rust_context(); + // Safety: The caller must guarantee that `retry_strategy` is not null and has not been freed + let retry_strategy = unsafe { retry_strategy.into_rust() }; + let account_number = get_string(account_number); + let identifier = get_string(identifier); + // Safety: `public_key` pointer must be a valid pointer to 32 unsigned bytes. + let pub_key: [u8; 32] = unsafe { ptr::read(public_key as *const [u8; 32]) }; + + let completion = completion_handler.clone(); + let task = tokio_handle.spawn(async move { + match mullvad_ios_rotate_device_key_inner( + api_context.rest_handle(), + retry_strategy, + account_number, + identifier, + PublicKey::from(pub_key), + ) + .await + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler).into_swift() +} + +async fn mullvad_ios_get_device_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, + identifier: String, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = DevicesProxy::new(rest_client); + + let future_factory = || api.get_response(account_number.clone(), identifier.clone()); + + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_ios_get_devices_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = DevicesProxy::new(rest_client); + + let future_factory = || api.list_response(account_number.clone()); + + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_ios_delete_device_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, + identifier: String, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = DevicesProxy::new(rest_client); + + let future_factory = || api.remove(account_number.clone(), identifier.clone()); + + do_request_with_empty_body(retry_strategy, future_factory).await +} + +async fn mullvad_ios_rotate_device_key_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, + identifier: String, + pub_key: wireguard::PublicKey, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = DevicesProxy::new(rest_client); + + let future_factory = + || api.replace_wg_key_response(account_number.clone(), identifier.clone(), pub_key.clone()); + + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_ios_create_device_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + account_number: String, + pub_key: wireguard::PublicKey, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = DevicesProxy::new(rest_client); + + let future_factory = || api.create_response(account_number.clone(), pub_key.clone()); + + do_request(retry_strategy, future_factory).await +} diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs index e8fe1ced24..bf849bc45c 100644 --- a/mullvad-ios/src/api_client/mod.rs +++ b/mullvad-ios/src/api_client/mod.rs @@ -1,4 +1,4 @@ -use std::{ffi::c_char, future::Future, sync::Arc}; +use std::{ffi::c_char, ffi::CStr, future::Future, sync::Arc}; use access_method_resolver::SwiftAccessMethodResolver; use access_method_settings::SwiftAccessMethodSettingsWrapper; @@ -21,6 +21,7 @@ mod account; mod api; mod cancellation; mod completion; +mod device; pub(super) mod helpers; mod mock; mod problem_report; @@ -169,7 +170,7 @@ pub extern "C" fn mullvad_api_init_new( host, address, domain, - true, + false, bridge_provider, settings_provider, ); @@ -297,3 +298,17 @@ where retry_future(future_factory, should_retry, retry_strategy.delays()).await } + +/// Try to convert a C string to an owned [String]. if `ptr` is null, an empty [String] is +/// returned. +/// +/// # Safety +/// - `ptr` must uphold all safety invariants as required by [CStr::from_ptr]. +fn get_string(ptr: *const c_char) -> String { + if ptr.is_null() { + return String::new(); + } + // Safety: See function doc comment. + let cstr = unsafe { CStr::from_ptr(ptr) }; + cstr.to_str().map(ToOwned::to_owned).unwrap_or_default() +} diff --git a/mullvad-ios/src/api_client/problem_report.rs b/mullvad-ios/src/api_client/problem_report.rs index 8c47da0eef..3254d7f6d2 100644 --- a/mullvad-ios/src/api_client/problem_report.rs +++ b/mullvad-ios/src/api_client/problem_report.rs @@ -8,7 +8,7 @@ use std::os::raw::c_char; use super::{ cancellation::{RequestCancelHandle, SwiftCancelHandle}, completion::{CompletionCookie, SwiftCompletionHandler}, - do_request_with_empty_body, + do_request_with_empty_body, get_string, response::SwiftMullvadApiResponse, retry_strategy::{RetryStrategy, SwiftRetryStrategy}, SwiftApiContext, @@ -119,15 +119,6 @@ struct ProblemReportRequest { impl ProblemReportRequest { // SAFETY: the members of `SwiftProblemReportRequest` must point to null-terminated strings unsafe fn from_swift_parameters(request: SwiftProblemReportRequest) -> Option<Self> { - fn get_string(ptr: *const c_char) -> String { - if ptr.is_null() { - return String::new(); - } - // Safety: `ptr` must be a valid, null-terminated C string. - let cstr = unsafe { CStr::from_ptr(ptr) }; - cstr.to_str().map(ToOwned::to_owned).unwrap_or_default() - } - let address = get_string(request.address); let message = get_string(request.message); let log = get_string(request.log).into(); |
