summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-02-21 15:08:46 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-02-21 15:08:46 +0100
commitcd92d028a9a72a2870cefed3c16958765538bbdc (patch)
tree13ed3e3895269a8bb6980b5d6eab3238f1e09252 /ios/MullvadREST
parent9a8535ef787d784d2d75dbd1474e1a1846413e83 (diff)
parent041bf3f20f3cb0ed6703065eee4d09b5f938f730 (diff)
downloadmullvadvpn-cd92d028a9a72a2870cefed3c16958765538bbdc.tar.xz
mullvadvpn-cd92d028a9a72a2870cefed3c16958765538bbdc.zip
Merge branch 'urlsession'prepare-2025.5-beta1
Diffstat (limited to 'ios/MullvadREST')
-rw-r--r--ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift37
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift33
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTDefaults.swift8
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxy.swift15
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift14
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift57
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift168
7 files changed, 325 insertions, 7 deletions
diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
new file mode 100644
index 0000000000..89bf2dd725
--- /dev/null
+++ b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
@@ -0,0 +1,37 @@
+//
+// MullvadApiRequestFactory.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-02-07.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadRustRuntime
+import MullvadTypes
+
+enum MullvadApiRequest {
+ case getAddressList
+}
+
+struct MullvadApiRequestFactory {
+ let apiContext: MullvadApiContext
+
+ func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler {
+ { completion in
+ let pointerClass = MullvadApiCompletion { apiResponse in
+ try? completion?(apiResponse)
+ }
+
+ let rawPointer = Unmanaged.passRetained(pointerClass).toOpaque()
+
+ return switch request {
+ case .getAddressList:
+ MullvadApiCancellable(handle: mullvad_api_get_addresses(apiContext.context, rawPointer))
+ }
+ }
+ }
+}
+
+extension REST {
+ typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
+}
diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
index 5ac58668c0..4c8e144d6d 100644
--- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
@@ -7,10 +7,17 @@
//
import Foundation
+import MullvadRustRuntime
import MullvadTypes
+import Operations
import WireGuardKitTypes
public protocol APIQuerying: Sendable {
+ func mullvadApiGetAddressList(
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
+ ) -> Cancellable
+
func getAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
@@ -55,6 +62,32 @@ extension REST {
)
}
+ public func mullvadApiGetAddressList(
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
+ ) -> Cancellable {
+ let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList)
+
+ let responseHandler = rustResponseHandler(
+ decoding: [AnyIPEndpoint].self,
+ with: responseDecoder
+ )
+
+ let networkOperation = MullvadApiNetworkOperation(
+ name: "get-api-addrs",
+ dispatchQueue: dispatchQueue,
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseDecoder: responseDecoder,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+
+ operationQueue.addOperation(networkOperation)
+
+ return networkOperation
+ }
+
public func getAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
index 250401b019..d115abc37a 100644
--- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadRustRuntime
import MullvadTypes
// swiftlint:disable force_cast
@@ -28,6 +29,13 @@ extension REST {
/// Default network timeout for API requests.
public static let defaultAPINetworkTimeout: Duration = .seconds(10)
+
+ /// API context used for API requests via Rust runtime.
+ // swiftlint:disable:next force_try
+ public static let apiContext = try! MullvadApiContext(
+ host: defaultAPIHostname,
+ address: defaultAPIEndpoint
+ )
}
// swiftlint:enable force_cast
diff --git a/ios/MullvadREST/ApiHandlers/RESTProxy.swift b/ios/MullvadREST/ApiHandlers/RESTProxy.swift
index 0da8ab546c..75d1e31fd8 100644
--- a/ios/MullvadREST/ApiHandlers/RESTProxy.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTProxy.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadRustRuntime
import MullvadTypes
import Operations
@@ -26,6 +27,8 @@ extension REST {
/// URL request factory.
let requestFactory: REST.RequestFactory
+ let mullvadApiRequestFactory: MullvadApiRequestFactory
+
/// URL response decoder.
let responseDecoder: JSONDecoder
@@ -40,6 +43,7 @@ extension REST {
self.configuration = configuration
self.requestFactory = requestFactory
+ self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext)
self.responseDecoder = responseDecoder
}
@@ -132,13 +136,16 @@ extension REST {
public class ProxyConfiguration: @unchecked Sendable {
public let transportProvider: RESTTransportProvider
public let addressCacheStore: AddressCache
+ public let apiContext: MullvadApiContext
public init(
transportProvider: RESTTransportProvider,
- addressCacheStore: AddressCache
+ addressCacheStore: AddressCache,
+ apiContext: MullvadApiContext
) {
self.transportProvider = transportProvider
self.addressCacheStore = addressCacheStore
+ self.apiContext = apiContext
}
}
@@ -147,13 +154,15 @@ extension REST {
public init(
proxyConfiguration: ProxyConfiguration,
- accessTokenManager: RESTAccessTokenManagement
+ accessTokenManager: RESTAccessTokenManagement,
+ apiContext: MullvadApiContext
) {
self.accessTokenManager = accessTokenManager
super.init(
transportProvider: proxyConfiguration.transportProvider,
- addressCacheStore: proxyConfiguration.addressCacheStore
+ addressCacheStore: proxyConfiguration.addressCacheStore,
+ apiContext: apiContext
)
}
}
diff --git a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
index 331fb49030..46acaa94bf 100644
--- a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
@@ -7,6 +7,8 @@
//
import Foundation
+import MullvadRustRuntime
+
public protocol ProxyFactoryProtocol {
var configuration: REST.AuthProxyConfiguration { get }
@@ -16,7 +18,8 @@ public protocol ProxyFactoryProtocol {
static func makeProxyFactory(
transportProvider: RESTTransportProvider,
- addressCache: REST.AddressCache
+ addressCache: REST.AddressCache,
+ apiContext: MullvadApiContext
) -> ProxyFactoryProtocol
}
@@ -26,11 +29,13 @@ extension REST {
public static func makeProxyFactory(
transportProvider: any RESTTransportProvider,
- addressCache: REST.AddressCache
+ addressCache: REST.AddressCache,
+ apiContext: MullvadApiContext
) -> any ProxyFactoryProtocol {
let basicConfiguration = REST.ProxyConfiguration(
transportProvider: transportProvider,
- addressCacheStore: addressCache
+ addressCacheStore: addressCache,
+ apiContext: apiContext
)
let authenticationProxy = REST.AuthenticationProxy(
@@ -42,7 +47,8 @@ extension REST {
let authConfiguration = REST.AuthProxyConfiguration(
proxyConfiguration: basicConfiguration,
- accessTokenManager: accessTokenManager
+ accessTokenManager: accessTokenManager,
+ apiContext: apiContext
)
return ProxyFactory(configuration: authConfiguration)
diff --git a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
index 9790514507..1b6d7f950b 100644
--- a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadRustRuntime
import MullvadTypes
protocol RESTResponseHandler<Success> {
@@ -15,7 +16,14 @@ protocol RESTResponseHandler<Success> {
func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success>
}
+protocol RESTRustResponseHandler<Success> {
+ associatedtype Success
+
+ func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
+}
+
extension REST {
+ // TODO: We could probably remove the `decoding` case when network requests are fully merged to Mullvad API.
/// Responser handler result type.
enum ResponseHandlerResult<Success> {
/// Response handler succeeded and produced a value.
@@ -66,4 +74,53 @@ extension REST {
}
}
}
+
+ final class RustResponseHandler<Success>: RESTRustResponseHandler {
+ typealias HandlerBlock = (MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
+
+ private let handlerBlock: HandlerBlock
+
+ init(_ block: @escaping HandlerBlock) {
+ handlerBlock = block
+ }
+
+ func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> {
+ handlerBlock(response)
+ }
+ }
+
+ /// Returns default response handler that parses JSON response into the
+ /// given `Decodable` type if possible, otherwise attempts to decode
+ /// the server error.
+ static func rustResponseHandler<T: Decodable>(
+ decoding type: T.Type,
+ with decoder: JSONDecoder
+ ) -> RustResponseHandler<T> {
+ RustResponseHandler { response in
+ guard let body = response.body 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
+ )
+ )
+ }
+ }
+ }
+
+ /// Returns default response handler that parses JSON response into the
+ /// given `Decodable` type if possible, otherwise attempts to decode
+ /// the server error.
+ static func rustEmptyResponseHandler() -> RustResponseHandler<Void> {
+ RustResponseHandler { _ in
+ .success(())
+ }
+ }
}
diff --git a/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift b/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift
new file mode 100644
index 0000000000..1bcd218444
--- /dev/null
+++ b/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift
@@ -0,0 +1,168 @@
+//
+// RESTRustNetworkOperation.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
+
+extension REST {
+ class MullvadApiNetworkOperation<Success: Sendable>: ResultOperation<Success>, @unchecked Sendable {
+ private let logger: Logger
+
+ private let requestHandler: MullvadApiRequestHandler
+ private var responseDecoder: JSONDecoder
+ private let responseHandler: any RESTRustResponseHandler<Success>
+ private var networkTask: MullvadApiCancellable?
+
+ private let retryStrategy: RetryStrategy
+ private var retryDelayIterator: AnyIterator<Duration>
+ private var retryTimer: DispatchSourceTimer?
+ private var retryCount = 0
+
+ init(
+ name: String,
+ dispatchQueue: DispatchQueue,
+ retryStrategy: RetryStrategy,
+ requestHandler: @escaping MullvadApiRequestHandler,
+ responseDecoder: JSONDecoder,
+ responseHandler: some RESTRustResponseHandler<Success>,
+ completionHandler: CompletionHandler? = nil
+ ) {
+ self.retryStrategy = retryStrategy
+ retryDelayIterator = retryStrategy.makeDelayIterator()
+ self.responseDecoder = responseDecoder
+ self.requestHandler = requestHandler
+ 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() {
+ retryTimer?.cancel()
+ networkTask?.cancel()
+
+ retryTimer = nil
+ networkTask = nil
+ }
+
+ override public func main() {
+ startRequest()
+ }
+
+ func startRequest() {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ guard !isCancelled else {
+ finish(result: .failure(OperationError.cancelled))
+ return
+ }
+
+ networkTask = requestHandler { [weak self] response in
+ guard let self else { return }
+
+ if let error = response.restError() {
+ if response.shouldRetry {
+ retryRequest(with: error)
+ } else {
+ finish(result: .failure(error))
+ }
+
+ return
+ }
+
+ let decodedResponse = responseHandler.handleResponse(response)
+
+ switch decodedResponse {
+ case let .success(value):
+ finish(result: .success(value))
+ case let .decoding(block):
+ finish(result: .success(try block()))
+ case let .unhandledResponse(error):
+ finish(result: .failure(REST.Error.unhandledResponse(Int(response.statusCode), error)))
+ }
+ }
+ }
+
+ private func retryRequest(with error: REST.Error) {
+ // Check if retry count is not exceeded.
+ guard retryCount < retryStrategy.maxRetryCount else {
+ if retryStrategy.maxRetryCount > 0 {
+ logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))")
+ }
+ finish(result: .failure(error))
+ return
+ }
+
+ // Increment retry count.
+ retryCount += 1
+
+ // Retry immediately if retry delay is set to never.
+ guard retryStrategy.delay != .never else {
+ startRequest()
+ return
+ }
+
+ guard let waitDelay = retryDelayIterator.next() else {
+ logger.debug("Retry delay iterator failed to produce next value.")
+
+ finish(result: .failure(error))
+ return
+ }
+
+ logger.debug("Retry in \(waitDelay.logFormat()).")
+
+ // Create timer to delay retry.
+ let timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
+
+ timer.setEventHandler { [weak self] in
+ self?.startRequest()
+ }
+
+ timer.setCancelHandler { [weak self] in
+ self?.finish(result: .failure(OperationError.cancelled))
+ }
+
+ timer.schedule(wallDeadline: .now() + waitDelay.timeInterval)
+ timer.activate()
+
+ retryTimer = timer
+ }
+ }
+}
+
+extension MullvadApiResponse {
+ public func restError() -> REST.Error? {
+ guard !success else {
+ return nil
+ }
+
+ guard let serverResponseCode else {
+ return .transport(MullvadApiTransportError.connectionFailed(description: errorDescription))
+ }
+
+ let response = REST.ServerErrorResponse(
+ code: REST.ServerResponseCode(rawValue: serverResponseCode),
+ detail: errorDescription
+ )
+ return .unhandledResponse(Int(statusCode), response)
+ }
+}
+
+enum MullvadApiTransportError: Error {
+ case connectionFailed(description: String?)
+}