diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-02-21 15:08:46 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-02-21 15:08:46 +0100 |
| commit | cd92d028a9a72a2870cefed3c16958765538bbdc (patch) | |
| tree | 13ed3e3895269a8bb6980b5d6eab3238f1e09252 /ios/MullvadREST | |
| parent | 9a8535ef787d784d2d75dbd1474e1a1846413e83 (diff) | |
| parent | 041bf3f20f3cb0ed6703065eee4d09b5f938f730 (diff) | |
| download | mullvadvpn-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.swift | 37 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift | 33 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTDefaults.swift | 8 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTProxy.swift | 15 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift | 57 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift | 168 |
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?) +} |
