summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadMockData/MullvadREST/MockProxyFactory.swift11
-rw-r--r--ios/MullvadREST/APIRequest/APIError.swift19
-rw-r--r--ios/MullvadREST/APIRequest/APIRequest.swift38
-rw-r--r--ios/MullvadREST/APIRequest/APIRequestProxy.swift89
-rw-r--r--ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift110
-rw-r--r--ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift14
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift5
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxy.swift18
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift15
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift30
-rw-r--r--ios/MullvadREST/RetryStrategy/RetryStrategy.swift33
-rw-r--r--ios/MullvadREST/Transport/APITransport.swift51
-rw-r--r--ios/MullvadREST/Transport/APITransportProvider.swift37
-rw-r--r--ios/MullvadRESTTests/RequestExecutorTests.swift23
-rw-r--r--ios/MullvadRESTTests/RetryStrategyTests.swift4
-rw-r--r--ios/MullvadRustRuntime/MullvadApiCancellable.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj44
-rw-r--r--ios/MullvadVPN/AppDelegate.swift37
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift30
-rw-r--r--ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift38
-rw-r--r--ios/MullvadVPN/TransportMonitor/TransportMonitor.swift41
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift43
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift22
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift23
-rw-r--r--ios/PacketTunnelCore/IPC/AppMessageHandler.swift16
-rw-r--r--ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift11
-rw-r--r--ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift1
-rw-r--r--ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift1
-rw-r--r--ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift43
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift22
30 files changed, 773 insertions, 100 deletions
diff --git a/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift b/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift
index f6a101808c..62f31c5b94 100644
--- a/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift
+++ b/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift
@@ -29,13 +29,13 @@ public struct MockProxyFactory: ProxyFactoryProtocol {
public static func makeProxyFactory(
transportProvider: any RESTTransportProvider,
- addressCache: REST.AddressCache,
- apiContext: MullvadApiContext
+ apiTransportProvider: any APITransportProviderProtocol,
+ addressCache: REST.AddressCache
) -> any ProxyFactoryProtocol {
let basicConfiguration = REST.ProxyConfiguration(
transportProvider: transportProvider,
- addressCacheStore: addressCache,
- apiContext: apiContext
+ apiTransportProvider: apiTransportProvider,
+ addressCacheStore: addressCache
)
let authenticationProxy = REST.AuthenticationProxy(
@@ -47,8 +47,7 @@ public struct MockProxyFactory: ProxyFactoryProtocol {
let authConfiguration = REST.AuthProxyConfiguration(
proxyConfiguration: basicConfiguration,
- accessTokenManager: accessTokenManager,
- apiContext: apiContext
+ accessTokenManager: accessTokenManager
)
return MockProxyFactory(configuration: authConfiguration)
diff --git a/ios/MullvadREST/APIRequest/APIError.swift b/ios/MullvadREST/APIRequest/APIError.swift
new file mode 100644
index 0000000000..f62fde619a
--- /dev/null
+++ b/ios/MullvadREST/APIRequest/APIError.swift
@@ -0,0 +1,19 @@
+//
+// APIError.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-02-24.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+public struct APIError: Error, Codable, Sendable {
+ public let statusCode: Int
+ public let errorDescription: String
+ public let serverResponseCode: String?
+
+ public init(statusCode: Int, errorDescription: String, serverResponseCode: String?) {
+ self.statusCode = statusCode
+ self.errorDescription = errorDescription
+ self.serverResponseCode = serverResponseCode
+ }
+}
diff --git a/ios/MullvadREST/APIRequest/APIRequest.swift b/ios/MullvadREST/APIRequest/APIRequest.swift
new file mode 100644
index 0000000000..4fff7bd32b
--- /dev/null
+++ b/ios/MullvadREST/APIRequest/APIRequest.swift
@@ -0,0 +1,38 @@
+//
+// APIRequest.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-02-24.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+public enum APIRequest: Codable, Sendable {
+ case getAddressList(_ retryStrategy: REST.RetryStrategy)
+
+ var retryStrategy: REST.RetryStrategy {
+ switch self {
+ case let .getAddressList(strategy):
+ return strategy
+ }
+ }
+}
+
+public struct ProxyAPIRequest: Codable, Sendable {
+ public let id: UUID
+ public let request: APIRequest
+
+ public init(id: UUID, request: APIRequest) {
+ self.id = id
+ self.request = request
+ }
+}
+
+public struct ProxyAPIResponse: Codable, Sendable {
+ public let data: Data?
+ public let error: APIError?
+
+ public init(data: Data?, error: APIError?) {
+ self.data = data
+ self.error = error
+ }
+}
diff --git a/ios/MullvadREST/APIRequest/APIRequestProxy.swift b/ios/MullvadREST/APIRequest/APIRequestProxy.swift
new file mode 100644
index 0000000000..8e2ac4fad2
--- /dev/null
+++ b/ios/MullvadREST/APIRequest/APIRequestProxy.swift
@@ -0,0 +1,89 @@
+//
+// APIRequestProxy.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-02-13.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadRustRuntime
+import MullvadTypes
+
+public protocol APIRequestProxyProtocol {
+ func sendRequest(_ proxyRequest: ProxyAPIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void)
+ func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse
+ func cancelRequest(identifier: UUID)
+}
+
+/// Network request proxy capable of passing serializable requests and responses over the given transport provider.
+public final class APIRequestProxy: APIRequestProxyProtocol, @unchecked Sendable {
+ /// Serial queue used for synchronizing access to class members.
+ private let dispatchQueue: DispatchQueue
+
+ private let transportProvider: APITransportProviderProtocol
+
+ /// List of all proxied network requests bypassing VPN.
+ private var proxiedRequests: [UUID: Cancellable] = [:]
+
+ public init(
+ dispatchQueue: DispatchQueue,
+ transportProvider: APITransportProviderProtocol
+ ) {
+ self.dispatchQueue = dispatchQueue
+ self.transportProvider = transportProvider
+ }
+
+ public func sendRequest(
+ _ proxyRequest: ProxyAPIRequest,
+ completion: @escaping @Sendable (ProxyAPIResponse) -> Void
+ ) {
+ dispatchQueue.async {
+ guard let transport = self.transportProvider.makeTransport() else {
+ // Cancel old task, if there's one scheduled.
+ self.cancelRequest(identifier: proxyRequest.id)
+
+ completion(ProxyAPIResponse(data: nil, error: nil))
+ return
+ }
+
+ let cancellable = transport.sendRequest(proxyRequest.request) { [weak self] response in
+ guard let self else { return }
+
+ // Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
+ dispatchQueue.async {
+ _ = self.removeRequest(identifier: proxyRequest.id)
+ completion(response)
+ }
+ }
+
+ // Cancel old task, if there's one scheduled.
+ let oldTask = self.addRequest(identifier: proxyRequest.id, task: cancellable)
+ oldTask?.cancel()
+ }
+ }
+
+ public func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse {
+ return await withCheckedContinuation { continuation in
+ sendRequest(proxyRequest) { proxyResponse in
+ continuation.resume(returning: proxyResponse)
+ }
+ }
+ }
+
+ public func cancelRequest(identifier: UUID) {
+ dispatchQueue.async {
+ let task = self.removeRequest(identifier: identifier)
+ task?.cancel()
+ }
+ }
+
+ private func addRequest(identifier: UUID, task: Cancellable) -> Cancellable? {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+ return proxiedRequests.updateValue(task, forKey: identifier)
+ }
+
+ private func removeRequest(identifier: UUID) -> Cancellable? {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+ return proxiedRequests.removeValue(forKey: identifier)
+ }
+}
diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift b/ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift
new file mode 100644
index 0000000000..68d4ecb0c7
--- /dev/null
+++ b/ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift
@@ -0,0 +1,110 @@
+//
+// MullvadApiNetworkOperation.swift
+// MullvadREST
+//
+// Created by Jon Petersson on 2025-01-29.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadRustRuntime
+import MullvadTypes
+import Operations
+
+private enum MullvadApiTransportError: Error {
+ case connectionFailed(description: String?)
+}
+
+extension REST {
+ class MullvadApiNetworkOperation<Success: Sendable>: ResultOperation<Success>, @unchecked Sendable {
+ private let logger: Logger
+
+ private let request: APIRequest
+ private let transportProvider: APITransportProviderProtocol
+ private var responseDecoder: JSONDecoder
+ private let responseHandler: any RESTRustResponseHandler<Success>
+ private var networkTask: Cancellable?
+
+ init(
+ name: String,
+ dispatchQueue: DispatchQueue,
+ request: APIRequest,
+ transportProvider: APITransportProviderProtocol,
+ responseDecoder: JSONDecoder,
+ responseHandler: some RESTRustResponseHandler<Success>,
+ completionHandler: CompletionHandler? = nil
+ ) {
+ self.request = request
+ self.transportProvider = transportProvider
+ self.responseDecoder = responseDecoder
+ self.responseHandler = responseHandler
+
+ var logger = Logger(label: "REST.RustNetworkOperation")
+
+ logger[metadataKey: "name"] = .string(name)
+ self.logger = logger
+
+ super.init(
+ dispatchQueue: dispatchQueue,
+ completionQueue: .main,
+ completionHandler: completionHandler
+ )
+ }
+
+ override public func operationDidCancel() {
+ networkTask?.cancel()
+ networkTask = nil
+ }
+
+ override public func main() {
+ startRequest()
+ }
+
+ func startRequest() {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ guard !isCancelled else {
+ finish(result: .failure(OperationError.cancelled))
+ return
+ }
+
+ let transport = transportProvider.makeTransport()
+ networkTask = transport?.sendRequest(request) { [weak self] response in
+ guard let self else { return }
+
+ if let apiError = response.error {
+ finish(result: .failure(restError(apiError: apiError)))
+ return
+ }
+
+ let decodedResponse = responseHandler.handleResponse(response.data)
+
+ switch decodedResponse {
+ case let .success(value):
+ finish(result: .success(value))
+ case let .decoding(block):
+ do {
+ finish(result: .success(try block()))
+ } catch {
+ finish(result: .failure(REST.Error.unhandledResponse(0, nil)))
+ }
+ case let .unhandledResponse(error):
+ finish(result: .failure(REST.Error.unhandledResponse(0, error)))
+ }
+ }
+ }
+
+ private func restError(apiError: APIError) -> Error {
+ guard let serverResponseCode = apiError.serverResponseCode else {
+ return .transport(MullvadApiTransportError.connectionFailed(description: apiError.errorDescription))
+ }
+
+ let response = REST.ServerErrorResponse(
+ code: REST.ServerResponseCode(rawValue: serverResponseCode),
+ detail: apiError.errorDescription
+ )
+ return .unhandledResponse(apiError.statusCode, response)
+ }
+ }
+}
diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
index d40039d558..d361beef1b 100644
--- a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
+++ b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
@@ -9,14 +9,14 @@
import MullvadRustRuntime
import MullvadTypes
-enum MullvadApiRequest {
- case getAddressList(retryStrategy: REST.RetryStrategy)
-}
+public struct MullvadApiRequestFactory: Sendable {
+ public let apiContext: MullvadApiContext
-struct MullvadApiRequestFactory {
- let apiContext: MullvadApiContext
+ public init(apiContext: MullvadApiContext) {
+ self.apiContext = apiContext
+ }
- func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler {
+ public func makeRequest(_ request: APIRequest) -> REST.MullvadApiRequestHandler {
{ completion in
let pointerClass = MullvadApiCompletion { apiResponse in
try? completion?(apiResponse)
@@ -37,5 +37,5 @@ struct MullvadApiRequestFactory {
}
extension REST {
- typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
+ public typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
}
diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
index 1dfd01b545..908e0bddd4 100644
--- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
@@ -66,8 +66,6 @@ extension REST {
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
) -> Cancellable {
- let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList(retryStrategy: retryStrategy))
-
let responseHandler = rustResponseHandler(
decoding: [AnyIPEndpoint].self,
with: responseDecoder
@@ -76,7 +74,8 @@ extension REST {
let networkOperation = MullvadApiNetworkOperation(
name: "get-api-addrs",
dispatchQueue: dispatchQueue,
- requestHandler: requestHandler,
+ request: .getAddressList(retryStrategy),
+ transportProvider: configuration.apiTransportProvider,
responseDecoder: responseDecoder,
responseHandler: responseHandler,
completionHandler: completionHandler
diff --git a/ios/MullvadREST/ApiHandlers/RESTProxy.swift b/ios/MullvadREST/ApiHandlers/RESTProxy.swift
index 75d1e31fd8..cabfd4bfdf 100644
--- a/ios/MullvadREST/ApiHandlers/RESTProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTProxy.swift
@@ -27,8 +27,6 @@ extension REST {
/// URL request factory.
let requestFactory: REST.RequestFactory
- let mullvadApiRequestFactory: MullvadApiRequestFactory
-
/// URL response decoder.
let responseDecoder: JSONDecoder
@@ -43,7 +41,6 @@ extension REST {
self.configuration = configuration
self.requestFactory = requestFactory
- self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext)
self.responseDecoder = responseDecoder
}
@@ -135,17 +132,17 @@ extension REST {
public class ProxyConfiguration: @unchecked Sendable {
public let transportProvider: RESTTransportProvider
+ public let apiTransportProvider: APITransportProviderProtocol
public let addressCacheStore: AddressCache
- public let apiContext: MullvadApiContext
public init(
transportProvider: RESTTransportProvider,
- addressCacheStore: AddressCache,
- apiContext: MullvadApiContext
+ apiTransportProvider: APITransportProviderProtocol,
+ addressCacheStore: AddressCache
) {
self.transportProvider = transportProvider
+ self.apiTransportProvider = apiTransportProvider
self.addressCacheStore = addressCacheStore
- self.apiContext = apiContext
}
}
@@ -154,15 +151,14 @@ extension REST {
public init(
proxyConfiguration: ProxyConfiguration,
- accessTokenManager: RESTAccessTokenManagement,
- apiContext: MullvadApiContext
+ accessTokenManager: RESTAccessTokenManagement
) {
self.accessTokenManager = accessTokenManager
super.init(
transportProvider: proxyConfiguration.transportProvider,
- addressCacheStore: proxyConfiguration.addressCacheStore,
- apiContext: apiContext
+ apiTransportProvider: proxyConfiguration.apiTransportProvider,
+ addressCacheStore: proxyConfiguration.addressCacheStore
)
}
}
diff --git a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
index 46acaa94bf..7515b92c5c 100644
--- a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
@@ -18,8 +18,8 @@ public protocol ProxyFactoryProtocol {
static func makeProxyFactory(
transportProvider: RESTTransportProvider,
- addressCache: REST.AddressCache,
- apiContext: MullvadApiContext
+ apiTransportProvider: APITransportProviderProtocol,
+ addressCache: REST.AddressCache
) -> ProxyFactoryProtocol
}
@@ -29,13 +29,13 @@ extension REST {
public static func makeProxyFactory(
transportProvider: any RESTTransportProvider,
- addressCache: REST.AddressCache,
- apiContext: MullvadApiContext
+ apiTransportProvider: any APITransportProviderProtocol,
+ addressCache: REST.AddressCache
) -> any ProxyFactoryProtocol {
let basicConfiguration = REST.ProxyConfiguration(
transportProvider: transportProvider,
- addressCacheStore: addressCache,
- apiContext: apiContext
+ apiTransportProvider: apiTransportProvider,
+ addressCacheStore: addressCache
)
let authenticationProxy = REST.AuthenticationProxy(
@@ -47,8 +47,7 @@ extension REST {
let authConfiguration = REST.AuthProxyConfiguration(
proxyConfiguration: basicConfiguration,
- accessTokenManager: accessTokenManager,
- apiContext: apiContext
+ accessTokenManager: accessTokenManager
)
return ProxyFactory(configuration: authConfiguration)
diff --git a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
index 1b6d7f950b..c6197e983e 100644
--- a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
@@ -19,7 +19,7 @@ protocol RESTResponseHandler<Success> {
protocol RESTRustResponseHandler<Success> {
associatedtype Success
- func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
+ func handleResponse(_ body: Data?) -> REST.ResponseHandlerResult<Success>
}
extension REST {
@@ -76,7 +76,7 @@ extension REST {
}
final class RustResponseHandler<Success>: RESTRustResponseHandler {
- typealias HandlerBlock = (MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
+ typealias HandlerBlock = (Data?) -> REST.ResponseHandlerResult<Success>
private let handlerBlock: HandlerBlock
@@ -84,8 +84,8 @@ extension REST {
handlerBlock = block
}
- func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> {
- handlerBlock(response)
+ func handleResponse(_ body: Data?) -> REST.ResponseHandlerResult<Success> {
+ handlerBlock(body)
}
}
@@ -96,28 +96,20 @@ extension REST {
decoding type: T.Type,
with decoder: JSONDecoder
) -> RustResponseHandler<T> {
- RustResponseHandler { response in
- guard let body = response.body else {
+ RustResponseHandler { data in
+ guard let data else {
return .unhandledResponse(nil)
}
- do {
- let decoded = try decoder.decode(type, from: body)
- return .decoding { decoded }
- } catch {
- return .unhandledResponse(
- try? decoder.decode(
- ServerErrorResponse.self,
- from: body
- )
- )
+ return if let decoded = try? decoder.decode(type, from: data) {
+ .decoding { decoded }
+ } else {
+ .unhandledResponse(nil)
}
}
}
- /// Returns default response handler that parses JSON response into the
- /// given `Decodable` type if possible, otherwise attempts to decode
- /// the server error.
+ /// Response handler for reponses where the body is empty.
static func rustEmptyResponseHandler() -> RustResponseHandler<Void> {
RustResponseHandler { _ in
.success(())
diff --git a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift
index 4f0d7016af..a8b4cb4980 100644
--- a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift
+++ b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift
@@ -11,7 +11,7 @@ import MullvadRustRuntime
import MullvadTypes
extension REST {
- public struct RetryStrategy: Sendable {
+ public struct RetryStrategy: Codable, Sendable {
public var maxRetryCount: Int
public var delay: RetryDelay
public var applyJitter: Bool
@@ -50,6 +50,8 @@ extension REST {
AnyIterator(Jittered(inner))
case let .exponentialBackoff(_, _, maxDelay):
AnyIterator(Transformer(inner: Jittered(inner)) { nextValue in
+ let maxDelay = maxDelay.duration
+
guard let nextValue else { return maxDelay }
return nextValue >= maxDelay ? maxDelay : nextValue
})
@@ -108,15 +110,15 @@ extension REST {
)
}
- public enum RetryDelay: Equatable, Sendable {
+ public enum RetryDelay: Codable, Equatable, Sendable {
/// Never wait to retry.
case never
/// Constant delay.
- case constant(Duration)
+ case constant(CodableDuration)
/// Exponential backoff.
- case exponentialBackoff(initial: Duration, multiplier: UInt64, maxDelay: Duration)
+ case exponentialBackoff(initial: CodableDuration, multiplier: UInt64, maxDelay: CodableDuration)
func makeIterator() -> AnyIterator<Duration> {
switch self {
@@ -127,16 +129,33 @@ extension REST {
case let .constant(duration):
return AnyIterator {
- duration
+ duration.duration
}
case let .exponentialBackoff(initial, multiplier, maxDelay):
return AnyIterator(ExponentialBackoff(
- initial: initial,
+ initial: initial.duration,
multiplier: multiplier,
- maxDelay: maxDelay
+ maxDelay: maxDelay.duration
))
}
}
}
+
+ public struct CodableDuration: Codable, Equatable, Sendable {
+ public var seconds: Int64
+ public var attoseconds: Int64
+
+ public var duration: Duration {
+ Duration(secondsComponent: seconds, attosecondsComponent: attoseconds)
+ }
+
+ public static func seconds(_ seconds: Int) -> CodableDuration {
+ return CodableDuration(seconds: Int64(seconds), attoseconds: 0)
+ }
+
+ public static func minutes(_ minutes: Int) -> CodableDuration {
+ return .seconds(minutes.saturatingMultiplication(60))
+ }
+ }
}
diff --git a/ios/MullvadREST/Transport/APITransport.swift b/ios/MullvadREST/Transport/APITransport.swift
new file mode 100644
index 0000000000..811e775a19
--- /dev/null
+++ b/ios/MullvadREST/Transport/APITransport.swift
@@ -0,0 +1,51 @@
+//
+// APITransport.swift
+// MullvadVPNUITests
+//
+// Created by Jon Petersson on 2025-02-24.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadRustRuntime
+import MullvadTypes
+
+public protocol APITransportProtocol {
+ var name: String { get }
+
+ func sendRequest(_ request: APIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void)
+ -> Cancellable
+}
+
+public final class APITransport: APITransportProtocol {
+ public var name: String {
+ "app-transport"
+ }
+
+ public let requestFactory: MullvadApiRequestFactory
+
+ public init(requestFactory: MullvadApiRequestFactory) {
+ self.requestFactory = requestFactory
+ }
+
+ public func sendRequest(
+ _ request: APIRequest,
+ completion: @escaping @Sendable (ProxyAPIResponse) -> Void
+ ) -> Cancellable {
+ let apiRequest = requestFactory.makeRequest(request)
+
+ return apiRequest { response in
+ let error: APIError? = if response.statusCode != 200 {
+ APIError(
+ statusCode: Int(response.statusCode),
+ errorDescription: response.errorDescription ?? "",
+ serverResponseCode: response.serverResponseCode
+ )
+ } else { nil }
+
+ completion(ProxyAPIResponse(
+ data: response.body,
+ error: error
+ ))
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/APITransportProvider.swift b/ios/MullvadREST/Transport/APITransportProvider.swift
new file mode 100644
index 0000000000..da5757d511
--- /dev/null
+++ b/ios/MullvadREST/Transport/APITransportProvider.swift
@@ -0,0 +1,37 @@
+//
+// APITransportProvider.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-02-24.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+public protocol APITransportProviderProtocol {
+ func makeTransport() -> APITransportProtocol?
+}
+
+public final class APITransportProvider: APITransportProviderProtocol, Sendable {
+ let requestFactory: MullvadApiRequestFactory
+
+ public init(requestFactory: MullvadApiRequestFactory) {
+ self.requestFactory = requestFactory
+ }
+
+ public func makeTransport() -> APITransportProtocol? {
+ APITransport(requestFactory: requestFactory)
+ }
+}
+
+extension REST {
+ public struct AnyAPITransportProvider: APITransportProviderProtocol {
+ private let block: () -> APITransportProtocol?
+
+ public init(_ block: @escaping @Sendable () -> APITransportProtocol?) {
+ self.block = block
+ }
+
+ public func makeTransport() -> APITransportProtocol? {
+ block()
+ }
+ }
+}
diff --git a/ios/MullvadRESTTests/RequestExecutorTests.swift b/ios/MullvadRESTTests/RequestExecutorTests.swift
index 36b3ca2b3c..636dca3379 100644
--- a/ios/MullvadRESTTests/RequestExecutorTests.swift
+++ b/ios/MullvadRESTTests/RequestExecutorTests.swift
@@ -25,10 +25,14 @@ final class RequestExecutorTests: XCTestCase {
}
}
+ let apiTransportProvider = REST.AnyAPITransportProvider {
+ APITransportStub()
+ }
+
let proxyFactory = REST.ProxyFactory.makeProxyFactory(
transportProvider: transportProvider,
- addressCache: addressCache,
- apiContext: REST.apiContext
+ apiTransportProvider: apiTransportProvider,
+ addressCache: addressCache
)
timerServerProxy = TimeServerProxy(configuration: proxyFactory.configuration)
}
@@ -76,3 +80,18 @@ final class RequestExecutorTests: XCTestCase {
waitForExpectations(timeout: .UnitTest.timeout)
}
}
+
+extension RequestExecutorTests {
+ final class APITransportStub: APITransportProtocol, Sendable {
+ public var name: String {
+ "app-transport-dummy"
+ }
+
+ public func sendRequest(
+ _ request: APIRequest,
+ completion: @escaping @Sendable (ProxyAPIResponse) -> Void
+ ) -> Cancellable {
+ AnyCancellable()
+ }
+ }
+}
diff --git a/ios/MullvadRESTTests/RetryStrategyTests.swift b/ios/MullvadRESTTests/RetryStrategyTests.swift
index 2af2d2d475..51e66132dd 100644
--- a/ios/MullvadRESTTests/RetryStrategyTests.swift
+++ b/ios/MullvadRESTTests/RetryStrategyTests.swift
@@ -13,7 +13,7 @@ import XCTest
class RetryStrategyTests: XCTestCase {
func testJitteredBackoffDoesNotGoBeyondMaxDelay() throws {
- let maxDelay = Duration(secondsComponent: 10, attosecondsComponent: 0)
+ let maxDelay = REST.CodableDuration(seconds: 10, attoseconds: 0)
let retryDelay = REST.RetryDelay.exponentialBackoff(initial: .seconds(1), multiplier: 2, maxDelay: maxDelay)
let retry = REST.RetryStrategy(maxRetryCount: 0, delay: retryDelay, applyJitter: true)
let iterator = retry.makeDelayIterator()
@@ -22,7 +22,7 @@ class RetryStrategyTests: XCTestCase {
for _ in 0 ... 10 {
let currentDelay = try XCTUnwrap(iterator.next())
XCTAssertLessThanOrEqual(previousDelay, currentDelay)
- XCTAssertLessThanOrEqual(currentDelay, maxDelay)
+ XCTAssertLessThanOrEqual(currentDelay, maxDelay.duration)
previousDelay = currentDelay
}
}
diff --git a/ios/MullvadRustRuntime/MullvadApiCancellable.swift b/ios/MullvadRustRuntime/MullvadApiCancellable.swift
index 0f0e0fe6e4..6c76bc3c14 100644
--- a/ios/MullvadRustRuntime/MullvadApiCancellable.swift
+++ b/ios/MullvadRustRuntime/MullvadApiCancellable.swift
@@ -6,7 +6,9 @@
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
-public class MullvadApiCancellable {
+import MullvadTypes
+
+public class MullvadApiCancellable: Cancellable {
private let handle: SwiftCancelHandle
public init(handle: consuming SwiftCancelHandle) {
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 8a6ed1259c..aeb4065128 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -502,6 +502,11 @@
7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */; };
7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; };
7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; };
+ 7A2E7B702D6C9FCF009EF2C3 /* APITransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B672D6C9D7A009EF2C3 /* APITransport.swift */; };
+ 7A2E7B712D6C9FE0009EF2C3 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B6E2D6C9ED9009EF2C3 /* APIError.swift */; };
+ 7A2E7B722D6C9FE5009EF2C3 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B6C2D6C9E53009EF2C3 /* APIRequest.swift */; };
+ 7A2E7B732D6C9FEB009EF2C3 /* APIRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */; };
+ 7A2E7B752D6CA0B1009EF2C3 /* APITransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B742D6CA0AC009EF2C3 /* APITransportProvider.swift */; };
7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307AD82A8CD8DA0017618B /* Duration.swift */; };
7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */; };
7A3215722D3934E6005DF395 /* MullvadApiCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */; };
@@ -598,8 +603,8 @@
7A8A19242CF4C9BF000BCB5B /* MultihopPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */; };
7A8A19262CF4D37B000BCB5B /* DAITAPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19252CF4D373000BCB5B /* DAITAPage.swift */; };
7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */; };
- 7A95B6792D5F729300687524 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */; };
7A95B67B2D5F758300687524 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A95B67A2D5F758300687524 /* relays.json */; };
+ 7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */; };
7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */; };
7A99D3712D56222000891FF7 /* MullvadApiCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */; };
7A9BE5A22B8F88C500E2A7D0 /* LocationNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */; };
@@ -644,7 +649,7 @@
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; };
7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */; };
7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */; };
- 7AB9312F2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift */; };
+ 7AB9312F2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */; };
7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; };
7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; };
@@ -2026,6 +2031,10 @@
7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsCoordinator.swift; sourceTree = "<group>"; };
7A2960F52A963F7500389B82 /* AlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCoordinator.swift; sourceTree = "<group>"; };
7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = "<group>"; };
+ 7A2E7B672D6C9D7A009EF2C3 /* APITransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITransport.swift; sourceTree = "<group>"; };
+ 7A2E7B6C2D6C9E53009EF2C3 /* APIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = "<group>"; };
+ 7A2E7B6E2D6C9ED9009EF2C3 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = "<group>"; };
+ 7A2E7B742D6CA0AC009EF2C3 /* APITransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITransportProvider.swift; sourceTree = "<group>"; };
7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = "<group>"; };
7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCompletion.swift; sourceTree = "<group>"; };
@@ -2111,8 +2120,9 @@
7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPage.swift; sourceTree = "<group>"; };
7A8A19252CF4D373000BCB5B /* DAITAPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAPage.swift; sourceTree = "<group>"; };
7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewControllerFactory.swift; sourceTree = "<group>"; };
- 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; };
+ 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestProxy.swift; sourceTree = "<group>"; };
7A95B67A2D5F758300687524 /* relays.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; };
+ 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; };
7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiRequestFactory.swift; sourceTree = "<group>"; };
7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCancellable.swift; sourceTree = "<group>"; };
7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNodeTests.swift; sourceTree = "<group>"; };
@@ -2154,7 +2164,7 @@
7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.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 /* RESTRustNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRustNetworkOperation.swift; sourceTree = "<group>"; };
+ 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiNetworkOperation.swift; sourceTree = "<group>"; };
7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; };
7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = "<group>"; };
7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
@@ -2662,6 +2672,7 @@
isa = PBXGroup;
children = (
F06045F02B2324DA00B2D37A /* ApiHandlers */,
+ 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */,
062B45A228FD4C0F00746E77 /* Assets */,
7AD63A422CDA661B00445268 /* Extensions */,
582FFA82290A84E700895745 /* Info.plist */,
@@ -4139,6 +4150,16 @@
path = Alert;
sourceTree = "<group>";
};
+ 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */ = {
+ isa = PBXGroup;
+ children = (
+ 7A2E7B6E2D6C9ED9009EF2C3 /* APIError.swift */,
+ 7A2E7B6C2D6C9E53009EF2C3 /* APIRequest.swift */,
+ 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */,
+ );
+ path = APIRequest;
+ sourceTree = "<group>";
+ };
7A45CFCD2C08697100D80B21 /* Screenshots */ = {
isa = PBXGroup;
children = (
@@ -4242,7 +4263,7 @@
7A8A19082CE5FFD7000BCB5B /* DAITA */ = {
isa = PBXGroup;
children = (
- 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */,
+ 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */,
F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */,
7A8A19132CEF2527000BCB5B /* DAITATunnelSettingsViewModel.swift */,
7A8A19092CE5FFDF000BCB5B /* SettingsDAITAView.swift */,
@@ -4505,6 +4526,7 @@
06AC114128F8413A0037AF9A /* AddressCache.swift */,
A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */,
06FAE67128F83CA40033DD93 /* HTTP.swift */,
+ 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */,
06FAE67228F83CA40033DD93 /* RESTAccessTokenManager.swift */,
06FAE66828F83CA30033DD93 /* RESTAccountsProxy.swift */,
06FAE67328F83CA40033DD93 /* RESTAPIProxy.swift */,
@@ -4521,7 +4543,6 @@
06FAE66A28F83CA30033DD93 /* RESTRequestFactory.swift */,
06FAE67428F83CA40033DD93 /* RESTRequestHandler.swift */,
06FAE66628F83CA30033DD93 /* RESTResponseHandler.swift */,
- 7AB9312D2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift */,
06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */,
06FAE66528F83CA30033DD93 /* RESTURLSession.swift */,
7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */,
@@ -4636,6 +4657,8 @@
isa = PBXGroup;
children = (
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */,
+ 7A2E7B672D6C9D7A009EF2C3 /* APITransport.swift */,
+ 7A2E7B742D6CA0AC009EF2C3 /* APITransportProvider.swift */,
F0DC77A32B2315800087F09D /* Direct */,
F0E5B2F62C9C689C0007F78C /* EncryptedDNS */,
A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */,
@@ -5664,6 +5687,7 @@
06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */,
A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */,
06799AEC28F98E4800ACD94E /* RESTTaskIdentifier.swift in Sources */,
+ 7A2E7B712D6C9FE0009EF2C3 /* APIError.swift in Sources */,
58E7BA192A975DF70068EC3A /* RESTTransportProvider.swift in Sources */,
F0B894F52BF7528700817A42 /* RelaySelector+Shadowsocks.swift in Sources */,
06799ADE28F98E4800ACD94E /* RESTRequestHandler.swift in Sources */,
@@ -5673,6 +5697,7 @@
7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */,
F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */,
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */,
+ 7A2E7B732D6C9FEB009EF2C3 /* APIRequestProxy.swift in Sources */,
A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */,
7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */,
7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */,
@@ -5695,6 +5720,8 @@
06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */,
A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */,
7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */,
+ 7A2E7B702D6C9FCF009EF2C3 /* APITransport.swift in Sources */,
+ 7A2E7B752D6CA0B1009EF2C3 /* APITransportProvider.swift in Sources */,
06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */,
F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */,
7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */,
@@ -5710,7 +5737,7 @@
F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */,
F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */,
A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */,
- 7AB9312F2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift in Sources */,
+ 7AB9312F2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift in Sources */,
06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */,
F0DDE42A2B220A15006B57A7 /* Haversine.swift in Sources */,
589E76C02A9378F100E502F3 /* RESTRequestExecutor.swift in Sources */,
@@ -5737,6 +5764,7 @@
06799AE428F98E4800ACD94E /* RESTAccountsProxy.swift in Sources */,
5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */,
44CAEAA12D442F5E004A8E65 /* LocationIdentifier.swift in Sources */,
+ 7A2E7B722D6C9FE5009EF2C3 /* APIRequest.swift in Sources */,
06799AE328F98E4800ACD94E /* RESTNetworkOperation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -6073,7 +6101,7 @@
44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */,
7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */,
4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */,
- 7A95B6792D5F729300687524 /* DAITASettingsCoordinator.swift in Sources */,
+ 7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */,
7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */,
5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */,
586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 5e30f2fae9..c88f37ba4b 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -42,6 +42,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
nonisolated(unsafe) private(set) var relayCacheTracker: RelayCacheTracker!
nonisolated(unsafe) private(set) var storePaymentManager: StorePaymentManager!
nonisolated(unsafe) private var transportMonitor: TransportMonitor!
+ nonisolated(unsafe) private var apiTransportMonitor: APITransportMonitor!
private var settingsObserver: TunnelBlockObserver!
private var migrationManager: MigrationManager!
@@ -155,8 +156,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
transportStrategy: transportStrategy,
encryptedDNSTransport: encryptedDNSTransport
)
+
+ let apiRequestFactory = MullvadApiRequestFactory(apiContext: REST.apiContext)
+ let apiTransportProvider = APITransportProvider(requestFactory: apiRequestFactory)
+
+ apiTransportMonitor = APITransportMonitor(
+ tunnelManager: tunnelManager,
+ tunnelStore: tunnelStore,
+ requestFactory: apiRequestFactory
+ )
+
setUpTransportMonitor(transportProvider: transportProvider)
- setUpSimulatorHost(transportProvider: transportProvider, relaySelector: relaySelector)
+ setUpSimulatorHost(
+ transportProvider: transportProvider,
+ apiTransportProvider: apiTransportProvider,
+ relaySelector: relaySelector
+ )
registerBackgroundTasks()
setupPaymentHandler()
@@ -188,18 +203,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if launchArguments.target == .screenshots {
proxyFactory = MockProxyFactory.makeProxyFactory(
transportProvider: REST.AnyTransportProvider { [weak self] in
- return self?.transportMonitor.makeTransport()
+ self?.transportMonitor.makeTransport()
+ },
+ apiTransportProvider: REST.AnyAPITransportProvider { [weak self] in
+ self?.apiTransportMonitor.makeTransport()
},
- addressCache: addressCache,
- apiContext: REST.apiContext
+ addressCache: addressCache
)
} else {
proxyFactory = REST.ProxyFactory.makeProxyFactory(
transportProvider: REST.AnyTransportProvider { [weak self] in
- return self?.transportMonitor.makeTransport()
+ self?.transportMonitor.makeTransport()
},
- addressCache: addressCache,
- apiContext: REST.apiContext
+ apiTransportProvider: REST.AnyAPITransportProvider { [weak self] in
+ self?.apiTransportMonitor.makeTransport()
+ },
+ addressCache: addressCache
)
}
apiProxy = proxyFactory.createAPIProxy()
@@ -217,13 +236,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func setUpSimulatorHost(
transportProvider: TransportProvider,
+ apiTransportProvider: APITransportProvider,
relaySelector: RelaySelectorWrapper
) {
#if targetEnvironment(simulator)
// Configure mock tunnel provider on simulator
simulatorTunnelProviderHost = SimulatorTunnelProviderHost(
relaySelector: relaySelector,
- transportProvider: transportProvider
+ transportProvider: transportProvider,
+ apiTransportProvider: apiTransportProvider
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
#endif
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index 5351853472..75a92e1b03 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
@@ -20,17 +20,26 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate, @unche
private var observedState: ObservedState = .disconnected
private var selectedRelays: SelectedRelays?
private let urlRequestProxy: URLRequestProxy
+ private let apiRequestProxy: APIRequestProxy
private let relaySelector: RelaySelectorProtocol
private let providerLogger = Logger(label: "SimulatorTunnelProviderHost")
private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue")
- init(relaySelector: RelaySelectorProtocol, transportProvider: TransportProvider) {
+ init(
+ relaySelector: RelaySelectorProtocol,
+ transportProvider: TransportProvider,
+ apiTransportProvider: APITransportProvider
+ ) {
self.relaySelector = relaySelector
self.urlRequestProxy = URLRequestProxy(
dispatchQueue: dispatchQueue,
transportProvider: transportProvider
)
+ self.apiRequestProxy = APIRequestProxy(
+ dispatchQueue: dispatchQueue,
+ transportProvider: apiTransportProvider
+ )
}
override func startTunnel(
@@ -153,11 +162,30 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate, @unche
handler?(reply)
}
+ case let .sendAPIRequest(proxyRequest):
+ apiRequestProxy.sendRequest(proxyRequest) { response in
+ var reply: Data?
+ do {
+ reply = try TunnelProviderReply(response).encode()
+ } catch {
+ self.providerLogger.error(
+ error: error,
+ message: "Failed to encode ProxyURLResponse."
+ )
+ }
+ handler?(reply)
+ }
+
case let .cancelURLRequest(listId):
urlRequestProxy.cancelRequest(identifier: listId)
completionHandler?(nil)
+ case let .cancelAPIRequest(listId):
+ apiRequestProxy.cancelRequest(identifier: listId)
+
+ completionHandler?(nil)
+
case .privateKeyRotation:
completionHandler?(nil)
}
diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift
index 3c40c7d75a..ee788e5e7d 100644
--- a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift
+++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift
@@ -8,6 +8,7 @@
import Foundation
import MullvadREST
+import MullvadRustRuntime
import MullvadTypes
import Operations
import PacketTunnelCore
@@ -55,3 +56,40 @@ struct PacketTunnelTransport: RESTTransport {
}
}
}
+
+final class PacketTunnelAPITransport: APITransportProtocol {
+ var name: String {
+ "packet-tunnel-transport"
+ }
+
+ let tunnel: any TunnelProtocol
+
+ init(tunnel: any TunnelProtocol) {
+ self.tunnel = tunnel
+ }
+
+ func sendRequest(
+ _ request: APIRequest,
+ completion: @escaping @Sendable (ProxyAPIResponse) -> Void
+ ) -> Cancellable {
+ let proxyRequest = ProxyAPIRequest(
+ id: UUID(),
+ request: request
+ )
+
+ return tunnel.sendAPIRequest(proxyRequest) { result in
+ switch result {
+ case let .success(reply):
+ completion(reply)
+
+ case let .failure(error):
+ let error = error.isOperationCancellationError ? URLError(.cancelled) : error
+ completion(ProxyAPIResponse(data: nil, error: APIError(
+ statusCode: 0,
+ errorDescription: error.localizedDescription,
+ serverResponseCode: nil
+ )))
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
index 3335352de2..1d0472783b 100644
--- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
+++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
@@ -60,3 +60,44 @@ final class TransportMonitor: RESTTransportProvider {
}
}
}
+
+final class APITransportMonitor: APITransportProviderProtocol {
+ private let tunnelManager: TunnelManager
+ private let tunnelStore: TunnelStore
+ private let requestFactory: MullvadApiRequestFactory
+
+ init(tunnelManager: TunnelManager, tunnelStore: TunnelStore, requestFactory: MullvadApiRequestFactory) {
+ self.tunnelManager = tunnelManager
+ self.tunnelStore = tunnelStore
+ self.requestFactory = requestFactory
+ }
+
+ func makeTransport() -> APITransportProtocol? {
+ let tunnel = tunnelStore.getPersistentTunnels().first { tunnel in
+ tunnel.status == .connecting || tunnel.status == .reasserting || tunnel.status == .connected
+ }
+
+ return if let tunnel, shouldBypassVPN(tunnel: tunnel) {
+ PacketTunnelAPITransport(tunnel: tunnel)
+ } else {
+ APITransport(requestFactory: requestFactory)
+ }
+ }
+
+ private func shouldBypassVPN(tunnel: any TunnelProtocol) -> Bool {
+ switch tunnel.status {
+ case .connected:
+ if case .error = tunnelManager.tunnelStatus.state {
+ true
+ } else {
+ tunnelManager.isConfigurationLoaded && tunnelManager.deviceState == .revoked
+ }
+
+ case .connecting, .reasserting:
+ true
+
+ default:
+ false
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
index 2f31f2f3e8..71d8238545 100644
--- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
+++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
@@ -109,6 +109,49 @@ extension TunnelProtocol {
return operation
}
+ /// Send API request via packet tunnel process bypassing VPN.
+ func sendAPIRequest(
+ _ proxyRequest: ProxyAPIRequest,
+ completionHandler: @escaping @Sendable (Result<ProxyAPIResponse, Error>) -> Void
+ ) -> Cancellable {
+ let decoderHandler: (Data?) throws -> ProxyAPIResponse = { data in
+ if let data {
+ return try TunnelProviderReply<ProxyAPIResponse>(messageData: data).value
+ } else {
+ throw EmptyTunnelProviderResponseError()
+ }
+ }
+
+ let operation = SendTunnelProviderMessageOperation(
+ dispatchQueue: dispatchQueue,
+ backgroundTaskProvider: backgroundTaskProvider,
+ tunnel: self,
+ message: .sendAPIRequest(proxyRequest),
+ timeout: proxyRequestTimeout,
+ decoderHandler: decoderHandler,
+ completionHandler: completionHandler
+ )
+
+ operation.onCancel { [weak self] _ in
+ guard let self else { return }
+
+ let cancelOperation = SendTunnelProviderMessageOperation(
+ dispatchQueue: dispatchQueue,
+ backgroundTaskProvider: backgroundTaskProvider,
+ tunnel: self,
+ message: .cancelAPIRequest(proxyRequest.id),
+ decoderHandler: decoderHandler,
+ completionHandler: nil
+ )
+
+ operationQueue.addOperation(cancelOperation)
+ }
+
+ operationQueue.addOperation(operation)
+
+ return operation
+ }
+
/// Notify tunnel about private key rotation.
func notifyKeyRotation(
completionHandler: @escaping @Sendable (Result<Void, Error>) -> Void
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
index 0c21fdacc6..12e1b933e9 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
@@ -147,7 +147,10 @@ class TunnelManagerTests: XCTestCase {
let simulatorTunnelProviderHost = SimulatorTunnelProviderHost(
relaySelector: relaySelector,
- transportProvider: transportProvider
+ transportProvider: transportProvider,
+ apiTransportProvider: APITransportProvider(
+ requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext)
+ )
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
@@ -215,18 +218,18 @@ class TunnelManagerTests: XCTestCase {
let simulatorTunnelProviderHost = SimulatorTunnelProviderHost(
relaySelector: relaySelector,
- transportProvider: transportProvider
+ transportProvider: transportProvider,
+ apiTransportProvider: APITransportProvider(
+ requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext)
+ )
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
let tunnelObserver = TunnelBlockObserver(
didUpdateTunnelStatus: { _, tunnelStatus in
switch tunnelStatus.state {
- case .connected:
- connectedExpectation.fulfill()
- case .disconnected:
- disconnectedExpectation.fulfill()
- default:
- return
+ case .connected: connectedExpectation.fulfill()
+ case .disconnected: disconnectedExpectation.fulfill()
+ default: return
}
}
)
@@ -240,8 +243,7 @@ class TunnelManagerTests: XCTestCase {
tunnelManager.startTunnel()
await fulfillment(of: [connectedExpectation])
- tunnelManager
- .reapplyTunnelConfiguration()
+ tunnelManager.reapplyTunnelConfiguration()
connectedExpectation = expectation(description: "Connected!")
await fulfillment(
of: [disconnectedExpectation, connectedExpectation],
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index 228a6f9d9e..4b943396a6 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -64,6 +64,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
addressCache: addressCache
)
+ let apiTransportProvider = APITransportProvider(
+ requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext)
+ )
+
adapter = WgAdapter(packetTunnelProvider: self)
let pinger = TunnelPinger(pingProvider: adapter.icmpPingProvider, replyQueue: internalQueue)
@@ -77,8 +81,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
let proxyFactory = REST.ProxyFactory.makeProxyFactory(
transportProvider: transportProvider,
- addressCache: addressCache,
- apiContext: REST.apiContext
+ apiTransportProvider: apiTransportProvider,
+ addressCache: addressCache
)
let accountsProxy = proxyFactory.createAccountsProxy()
let devicesProxy = proxyFactory.createDevicesProxy()
@@ -102,8 +106,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
protocolObfuscator: ProtocolObfuscator<TunnelObfuscator>()
)
- let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider)
- appMessageHandler = AppMessageHandler(packetTunnelActor: actor, urlRequestProxy: urlRequestProxy)
+ let urlRequestProxy = URLRequestProxy(
+ dispatchQueue: internalQueue,
+ transportProvider: transportProvider
+ )
+ let apiRequestProxy = APIRequestProxy(
+ dispatchQueue: internalQueue,
+ transportProvider: apiTransportProvider
+ )
+ appMessageHandler = AppMessageHandler(
+ packetTunnelActor: actor,
+ urlRequestProxy: urlRequestProxy,
+ apiRequestProxy: apiRequestProxy
+ )
ephemeralPeerExchangingPipeline = EphemeralPeerExchangingPipeline(
EphemeralPeerExchangeActor(
diff --git a/ios/PacketTunnelCore/IPC/AppMessageHandler.swift b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift
index f3473fed67..fc99fa7c22 100644
--- a/ios/PacketTunnelCore/IPC/AppMessageHandler.swift
+++ b/ios/PacketTunnelCore/IPC/AppMessageHandler.swift
@@ -8,6 +8,7 @@
import Foundation
import MullvadLogging
+import MullvadREST
/**
Actor handling packet tunnel IPC (app) messages and patching them through to the right facility.
@@ -16,10 +17,16 @@ public struct AppMessageHandler {
private let logger = Logger(label: "AppMessageHandler")
private let packetTunnelActor: PacketTunnelActorProtocol
private let urlRequestProxy: URLRequestProxyProtocol
+ private let apiRequestProxy: APIRequestProxyProtocol
- public init(packetTunnelActor: PacketTunnelActorProtocol, urlRequestProxy: URLRequestProxyProtocol) {
+ public init(
+ packetTunnelActor: PacketTunnelActorProtocol,
+ urlRequestProxy: URLRequestProxyProtocol,
+ apiRequestProxy: APIRequestProxyProtocol
+ ) {
self.packetTunnelActor = packetTunnelActor
self.urlRequestProxy = urlRequestProxy
+ self.apiRequestProxy = apiRequestProxy
}
/**
@@ -42,10 +49,17 @@ public struct AppMessageHandler {
case let .sendURLRequest(request):
return await encodeReply(urlRequestProxy.sendRequest(request))
+ case let .sendAPIRequest(request):
+ return await encodeReply(apiRequestProxy.sendRequest(request))
+
case let .cancelURLRequest(id):
urlRequestProxy.cancelRequest(identifier: id)
return nil
+ case let .cancelAPIRequest(id):
+ apiRequestProxy.cancelRequest(identifier: id)
+ return nil
+
case .getTunnelStatus:
return await encodeReply(packetTunnelActor.observedState)
diff --git a/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
index 955f119dbe..2ba2691ed3 100644
--- a/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
+++ b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
/// Enum describing supported app messages handled by packet tunnel provider.
public enum TunnelProviderMessage: Codable, CustomStringConvertible {
@@ -19,9 +20,15 @@ public enum TunnelProviderMessage: Codable, CustomStringConvertible {
/// Send HTTP request outside of VPN tunnel.
case sendURLRequest(ProxyURLRequest)
+ /// Send API request outside of VPN tunnel.
+ case sendAPIRequest(ProxyAPIRequest)
+
/// Cancel HTTP request sent outside of VPN tunnel.
case cancelURLRequest(UUID)
+ /// Cancel API request sent outside of VPN tunnel.
+ case cancelAPIRequest(UUID)
+
/// Notify tunnel about private key rotation.
case privateKeyRotation
@@ -33,8 +40,12 @@ public enum TunnelProviderMessage: Codable, CustomStringConvertible {
return "get-tunnel-status"
case .sendURLRequest:
return "send-http-request"
+ case .sendAPIRequest:
+ return "send-api-request"
case .cancelURLRequest:
return "cancel-http-request"
+ case .cancelAPIRequest:
+ return "cancel-api-request"
case .privateKeyRotation:
return "private-key-rotation"
}
diff --git a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift
index 3c2a2e797b..e2c6e5065e 100644
--- a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift
+++ b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
/// Struct describing serializable URLResponse data.
public struct ProxyURLResponse: Codable, Sendable {
diff --git a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift
index 300de8a479..b848f48dde 100644
--- a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift
+++ b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxyProtocol.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
public protocol URLRequestProxyProtocol {
func sendRequest(_ proxyRequest: ProxyURLRequest, completionHandler: @escaping @Sendable (ProxyURLResponse) -> Void)
diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
index 4b652f179f..16332271ed 100644
--- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
+++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
@@ -14,6 +14,8 @@ import PacketTunnelCore
import XCTest
final class AppMessageHandlerTests: XCTestCase {
+ // MARK: URLRequest
+
func testHandleAppMessageForSendURLRequest() async throws {
let sendRequestExpectation = expectation(description: "Expect sending request")
@@ -46,6 +48,41 @@ final class AppMessageHandlerTests: XCTestCase {
await fulfillment(of: [cancelRequestExpectation], timeout: .UnitTest.timeout)
}
+ // MARK: APIRequest
+
+ func testHandleAppMessageForSendAPIRequest() async throws {
+ let sendRequestExpectation = expectation(description: "Expect sending request")
+
+ let apiRequestProxy = APIRequestProxyStub(sendRequestExpectation: sendRequestExpectation)
+ let appMessageHandler = createAppMessageHandler(apiRequestProxy: apiRequestProxy)
+
+ let apiRequest = ProxyAPIRequest(
+ id: UUID(),
+ request: .getAddressList(.default)
+ )
+
+ _ = try? await appMessageHandler.handleAppMessage(
+ TunnelProviderMessage.sendAPIRequest(apiRequest).encode()
+ )
+
+ await fulfillment(of: [sendRequestExpectation], timeout: .UnitTest.timeout)
+ }
+
+ func testHandleAppMessageForCancelAPIRequest() async throws {
+ let cancelRequestExpectation = expectation(description: "Expect cancelling request")
+
+ let apiRequestProxy = APIRequestProxyStub(cancelRequestExpectation: cancelRequestExpectation)
+ let appMessageHandler = createAppMessageHandler(apiRequestProxy: apiRequestProxy)
+
+ _ = try? await appMessageHandler.handleAppMessage(
+ TunnelProviderMessage.cancelAPIRequest(UUID()).encode()
+ )
+
+ await fulfillment(of: [cancelRequestExpectation], timeout: .UnitTest.timeout)
+ }
+
+ // MARK: Other
+
func testHandleAppMessageForTunnelStatus() async throws {
let stateExpectation = expectation(description: "Expect getting state")
@@ -117,11 +154,13 @@ final class AppMessageHandlerTests: XCTestCase {
extension AppMessageHandlerTests {
func createAppMessageHandler(
actor: PacketTunnelActorProtocol = PacketTunnelActorStub(),
- urlRequestProxy: URLRequestProxyProtocol = URLRequestProxyStub()
+ urlRequestProxy: URLRequestProxyProtocol = URLRequestProxyStub(),
+ apiRequestProxy: APIRequestProxyProtocol = APIRequestProxyStub()
) -> AppMessageHandler {
return AppMessageHandler(
packetTunnelActor: actor,
- urlRequestProxy: urlRequestProxy
+ urlRequestProxy: urlRequestProxy,
+ apiRequestProxy: apiRequestProxy
)
}
}
diff --git a/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift b/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift
index 554ebbb3bc..847e0ae090 100644
--- a/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/URLRequestProxyStub.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
import PacketTunnelCore
import XCTest
@@ -30,3 +31,24 @@ struct URLRequestProxyStub: URLRequestProxyProtocol {
cancelRequestExpectation?.fulfill()
}
}
+
+struct APIRequestProxyStub: APIRequestProxyProtocol {
+ var sendRequestExpectation: XCTestExpectation?
+ var cancelRequestExpectation: XCTestExpectation?
+
+ func sendRequest(
+ _ proxyRequest: ProxyAPIRequest,
+ completion: @escaping @Sendable (ProxyAPIResponse) -> Void
+ ) {
+ sendRequestExpectation?.fulfill()
+ }
+
+ func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse {
+ sendRequestExpectation?.fulfill()
+ return ProxyAPIResponse(data: nil, error: nil)
+ }
+
+ func cancelRequest(identifier: UUID) {
+ cancelRequestExpectation?.fulfill()
+ }
+}