diff options
Diffstat (limited to 'ios/MullvadREST/MullvadAPI/APIRequest')
3 files changed, 158 insertions, 0 deletions
diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIError.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIError.swift new file mode 100644 index 0000000000..f62fde619a --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/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/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift new file mode 100644 index 0000000000..2462a998a7 --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -0,0 +1,50 @@ +// +// 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) + case getRelayList(_ retryStrategy: REST.RetryStrategy, etag: String?) + + var name: String { + switch self { + case .getAddressList: + "get-address-list" + case .getRelayList: + "get-relay-lisy" + } + } + + var retryStrategy: REST.RetryStrategy { + switch self { + case .getAddressList(let strategy), .getRelayList(let strategy, _): + 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 let etag: String? + + public init(data: Data?, error: APIError?, etag: String? = nil) { + self.data = data + self.error = error + self.etag = etag + } +} diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift new file mode 100644 index 0000000000..8e2ac4fad2 --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/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) + } +} |
