diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-03-19 16:32:36 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-03-31 11:39:29 +0200 |
| commit | b6a47bc377db48f39414d7587995b41bff4f8901 (patch) | |
| tree | 9d5eadcd15c4391e95f223a57db98c5228507d42 | |
| parent | eb72686c74607872ee510b432a442ea10baa1b86 (diff) | |
| download | mullvadvpn-b6a47bc377db48f39414d7587995b41bff4f8901.tar.xz mullvadvpn-b6a47bc377db48f39414d7587995b41bff4f8901.zip | |
Move Mullvad API functions to new file
| -rw-r--r-- | ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift | 15 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift | 130 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTError.swift | 1 | ||||
| -rw-r--r-- | ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift | 9 | ||||
| -rw-r--r-- | ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift | 218 | ||||
| -rw-r--r-- | ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadREST/Transport/APITransport.swift | 3 | ||||
| -rw-r--r-- | ios/MullvadRustRuntime/include/mullvad_rust_runtime.h | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift | 9 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift | 8 | ||||
| -rw-r--r-- | mullvad-api/src/relay_list.rs | 48 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/api.rs | 64 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/mod.rs | 25 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/response.rs | 2 |
15 files changed, 342 insertions, 220 deletions
diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift index 6f0e60a4ad..1330be345f 100644 --- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift @@ -12,21 +12,6 @@ import MullvadTypes import WireGuardKitTypes struct APIProxyStub: APIQuerying { - func mullvadApiGetAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> Cancellable { - AnyCancellable() - } - - func mullvadApiGetRelayList( - retryStrategy: REST.RetryStrategy, - etag: String?, - completionHandler: @escaping ProxyCompletionHandler<REST.ServerRelaysCacheResponse>) - -> Cancellable { - AnyCancellable() - } - func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift index 38486d3115..0d75b25d74 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -13,17 +13,6 @@ import Operations import WireGuardKitTypes public protocol APIQuerying: Sendable { - func mullvadApiGetAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> Cancellable - - func mullvadApiGetRelayList( - retryStrategy: REST.RetryStrategy, - etag: String?, - completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> - ) -> Cancellable - func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> @@ -68,76 +57,6 @@ extension REST { ) } - public func mullvadApiGetAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> Cancellable { - let responseHandler = rustResponseHandler( - decoding: [AnyIPEndpoint].self, - with: responseDecoder - ) - - return createNetworkOperation( - request: .getAddressList(retryStrategy), - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - public func mullvadApiGetRelayList( - retryStrategy: REST.RetryStrategy, - etag: String?, - completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> - ) -> Cancellable { - if var etag { - // Enforce weak validator to account for some backend caching quirks. - if etag.starts(with: "\"") { - etag.insert(contentsOf: "W/", at: etag.startIndex) - } - } - - let responseHandler = rustCustomResponseHandler { [weak self] (data, responseEtag) in - // Discarding result since we're only interested in knowing that it's parseable. - let canDecodeResponse = (try? self?.responseDecoder.decode(REST.ServerRelaysResponse.self, from: data)) != nil - - return if canDecodeResponse { - if let responseEtag, responseEtag == etag { - REST.ServerRelaysCacheResponse.notModified - } else { - REST.ServerRelaysCacheResponse.newContent(responseEtag, data) - } - } else { - nil - } - } - - return createNetworkOperation( - request: .getRelayList(retryStrategy, etag: etag), - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - private func createNetworkOperation<Success: Decodable>( - request: APIRequest, - responseHandler: RustResponseHandler<Success>, - completionHandler: @escaping @Sendable ProxyCompletionHandler<Success> - ) -> MullvadApiNetworkOperation<Success> { - let networkOperation = MullvadApiNetworkOperation( - name: request.name, - dispatchQueue: dispatchQueue, - request: request, - transportProvider: configuration.apiTransportProvider, - responseDecoder: responseDecoder, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - - operationQueue.addOperation(networkOperation) - - return networkOperation - } - public func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> @@ -366,64 +285,15 @@ extension REST { // MARK: - Response types - public enum ServerRelaysCacheResponse: Sendable, Decodable { - case notModified - case newContent(_ etag: String?, _ rawData: Data) - } - private struct CreateApplePaymentRequest: Encodable, Sendable { let receiptString: Data } - public enum CreateApplePaymentResponse: Sendable { - case noTimeAdded(_ expiry: Date) - case timeAdded(_ timeAdded: Int, _ newExpiry: Date) - - public var newExpiry: Date { - switch self { - case let .noTimeAdded(expiry), let .timeAdded(_, expiry): - return expiry - } - } - - public var timeAdded: TimeInterval { - switch self { - case .noTimeAdded: - return 0 - case let .timeAdded(timeAdded, _): - return TimeInterval(timeAdded) - } - } - - /// Returns a formatted string for the `timeAdded` interval, i.e "30 days" - public var formattedTimeAdded: String? { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - - return formatter.string(from: self.timeAdded) - } - } - private struct CreateApplePaymentRawResponse: Decodable, Sendable { let timeAdded: Int let newExpiry: Date } - public struct ProblemReportRequest: Encodable, Sendable { - public let address: String - public let message: String - public let log: String - public let metadata: [String: String] - - public init(address: String, message: String, log: String, metadata: [String: String]) { - self.address = address - self.message = message - self.log = log - self.metadata = metadata - } - } - private struct SubmitVoucherRequest: Encodable, Sendable { let voucherCode: String } diff --git a/ios/MullvadREST/ApiHandlers/RESTError.swift b/ios/MullvadREST/ApiHandlers/RESTError.swift index 542ee97882..d1c02fa835 100644 --- a/ios/MullvadREST/ApiHandlers/RESTError.swift +++ b/ios/MullvadREST/ApiHandlers/RESTError.swift @@ -115,6 +115,7 @@ extension REST { public static let tooManyRequests = ServerResponseCode(rawValue: "TOO_MANY_REQUESTS") public static let invalidVoucher = ServerResponseCode(rawValue: "INVALID_VOUCHER") public static let usedVoucher = ServerResponseCode(rawValue: "VOUCHER_USED") + public static let parsingError = ServerResponseCode(rawValue: "PARSING_ERROR") public let rawValue: String public init(rawValue: String) { diff --git a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift index e4acc1482c..f067ccabdf 100644 --- a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift +++ b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift @@ -101,10 +101,11 @@ extension REST { return .unhandledResponse(nil) } - return if let decoded = try? decoder.decode(type, from: data) { - .decoding { decoded } - } else { - .unhandledResponse(nil) + do { + let decoded = try decoder.decode(type, from: data) + return .decoding { decoded } + } catch { + return .unhandledResponse(ServerErrorResponse(code: .parsingError, detail: error.localizedDescription)) } } } diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift new file mode 100644 index 0000000000..be478ecb59 --- /dev/null +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift @@ -0,0 +1,218 @@ +// +// MullvadAPIProxy.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-03-19. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadRustRuntime +import MullvadTypes +import Operations +import WireGuardKitTypes + +extension REST { + public final class MullvadAPIProxy: APIQuerying, @unchecked Sendable { + let transportProvider: APITransportProviderProtocol + let dispatchQueue: DispatchQueue + let operationQueue = AsyncOperationQueue() + let responseDecoder: JSONDecoder + + public init( + transportProvider: APITransportProviderProtocol, + dispatchQueue: DispatchQueue, + responseDecoder: JSONDecoder + ) { + self.transportProvider = transportProvider + self.dispatchQueue = dispatchQueue + self.responseDecoder = responseDecoder + } + + public func getAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable { + let responseHandler = rustResponseHandler( + decoding: [AnyIPEndpoint].self, + with: responseDecoder + ) + + return createNetworkOperation( + request: .getAddressList(retryStrategy), + responseHandler: responseHandler, + completionHandler: completionHandler + ) + } + + public func getRelays( + etag: String?, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> + ) -> Cancellable { + if var etag { + // Enforce weak validator to account for some backend caching quirks. + if etag.starts(with: "\"") { + etag.insert(contentsOf: "W/", at: etag.startIndex) + } + } + + let responseHandler = rustCustomResponseHandler { [weak self] data, responseEtag in + if let responseEtag, responseEtag == etag { + return REST.ServerRelaysCacheResponse.notModified + } else { + // Discarding result since we're only interested in knowing that it's parseable. + let canDecodeResponse = (try? self?.responseDecoder.decode( + REST.ServerRelaysResponse.self, + from: data + )) != nil + + return canDecodeResponse ? REST.ServerRelaysCacheResponse.newContent(responseEtag, data) : nil + } + } + + return createNetworkOperation( + request: .getRelayList(retryStrategy, etag: etag), + responseHandler: responseHandler, + completionHandler: completionHandler + ) + } + + public func createApplePayment( + accountNumber: String, + receiptString: Data + ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse> { + RESTRequestExecutorStub<REST.CreateApplePaymentResponse>(success: { + .timeAdded(42, .distantFuture) + }) + } + + public func sendProblemReport( + _ body: ProblemReportRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<Void> + ) -> Cancellable { + AnyCancellable() + } + + public func submitVoucher( + voucherCode: String, + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<REST.SubmitVoucherResponse> + ) -> Cancellable { + AnyCancellable() + } + + private func createNetworkOperation<Success: Decodable>( + request: APIRequest, + responseHandler: RustResponseHandler<Success>, + completionHandler: @escaping @Sendable ProxyCompletionHandler<Success> + ) -> MullvadApiNetworkOperation<Success> { + let networkOperation = MullvadApiNetworkOperation( + name: request.name, + dispatchQueue: dispatchQueue, + request: request, + transportProvider: transportProvider, + responseDecoder: responseDecoder, + responseHandler: responseHandler, + completionHandler: completionHandler + ) + + operationQueue.addOperation(networkOperation) + + return networkOperation + } + } + + // MARK: - Response types + + public enum ServerRelaysCacheResponse: Sendable, Decodable { + case notModified + case newContent(_ etag: String?, _ rawData: Data) + } + + private struct CreateApplePaymentRequest: Encodable, Sendable { + let receiptString: Data + } + + public enum CreateApplePaymentResponse: Sendable { + case noTimeAdded(_ expiry: Date) + case timeAdded(_ timeAdded: Int, _ newExpiry: Date) + + public var newExpiry: Date { + switch self { + case let .noTimeAdded(expiry), let .timeAdded(_, expiry): + return expiry + } + } + + public var timeAdded: TimeInterval { + switch self { + case .noTimeAdded: + return 0 + case let .timeAdded(timeAdded, _): + return TimeInterval(timeAdded) + } + } + + /// Returns a formatted string for the `timeAdded` interval, i.e "30 days" + public var formattedTimeAdded: String? { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + + return formatter.string(from: self.timeAdded) + } + } + + private struct CreateApplePaymentRawResponse: Decodable, Sendable { + let timeAdded: Int + let newExpiry: Date + } + + public struct ProblemReportRequest: Encodable, Sendable { + public let address: String + public let message: String + public let log: String + public let metadata: [String: String] + + public init(address: String, message: String, log: String, metadata: [String: String]) { + self.address = address + self.message = message + self.log = log + self.metadata = metadata + } + } +} + +// TODO: Remove when "createApplePayment" func is implemented. +private struct RESTRequestExecutorStub<Success: Sendable>: RESTRequestExecutor { + var success: (() -> Success)? + + func execute(completionHandler: @escaping (Result<Success, Error>) -> Void) -> Cancellable { + if let result = success?() { + completionHandler(.success(result)) + } + return AnyCancellable() + } + + func execute( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping (Result<Success, Error>) -> Void + ) -> Cancellable { + if let result = success?() { + completionHandler(.success(result)) + } + return AnyCancellable() + } + + func execute() async throws -> Success { + try await execute(retryStrategy: .noRetry) + } + + func execute(retryStrategy: REST.RetryStrategy) async throws -> Success { + guard let success = success else { throw POSIXError(.EINVAL) } + + return success() + } +} diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift index 2462a998a7..ea51e22508 100644 --- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -15,13 +15,13 @@ public enum APIRequest: Codable, Sendable { case .getAddressList: "get-address-list" case .getRelayList: - "get-relay-lisy" + "get-relay-list" } } var retryStrategy: REST.RetryStrategy { switch self { - case .getAddressList(let strategy), .getRelayList(let strategy, _): + case let .getAddressList(strategy), let .getRelayList(strategy, _): strategy } } diff --git a/ios/MullvadREST/Transport/APITransport.swift b/ios/MullvadREST/Transport/APITransport.swift index 84b76a05af..9af1f2779f 100644 --- a/ios/MullvadREST/Transport/APITransport.swift +++ b/ios/MullvadREST/Transport/APITransport.swift @@ -34,7 +34,8 @@ public final class APITransport: APITransportProtocol { let apiRequest = requestFactory.makeRequest(request) return apiRequest { response in - let error: APIError? = if response.statusCode != 200 { + + let error: APIError? = if !response.success { APIError( statusCode: Int(response.statusCode), errorDescription: response.errorDescription ?? "", diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 8ebdea8863..8a199559b5 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -114,6 +114,20 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co void *completion_cookie, struct SwiftRetryStrategy retry_strategy); +/** + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is + * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this + * type is intended to be used. + * + * `etag` must be a pointer to a null terminated string. + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ struct SwiftCancelHandle mullvad_api_get_relays(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 25dbbd2c96..b9c42d131a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -500,6 +500,7 @@ 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 */; }; + 7A2C0E8C2D8B13F0003D8048 /* MullvadAPIProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.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 */; }; @@ -2034,6 +2035,7 @@ 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>"; }; + 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAPIProxy.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>"; }; @@ -4164,6 +4166,7 @@ 7A2C0E872D82E450003D8048 /* MullvadAPI */ = { isa = PBXGroup; children = ( + 7A2C0E8A2D8B13DB003D8048 /* APIHandlers */, 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */, 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */, 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */, @@ -4171,6 +4174,14 @@ path = MullvadAPI; sourceTree = "<group>"; }; + 7A2C0E8A2D8B13DB003D8048 /* APIHandlers */ = { + isa = PBXGroup; + children = ( + 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */, + ); + path = APIHandlers; + sourceTree = "<group>"; + }; 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */ = { isa = PBXGroup; children = ( @@ -5717,6 +5728,7 @@ 06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */, A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */, A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */, + 7A2C0E8C2D8B13F0003D8048 /* MullvadAPIProxy.swift in Sources */, 06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */, 06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */, A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */, diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index b3db3ab240..e1fd209f95 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -26,7 +26,7 @@ protocol RelayCacheTrackerProtocol: Sendable { final class RelayCacheTracker: RelayCacheTrackerProtocol, @unchecked Sendable { /// Relay update interval. - static let relayUpdateInterval: Duration = .seconds(30) + static let relayUpdateInterval: Duration = .hours(1) /// Tracker log. nonisolated(unsafe) private let logger = Logger(label: "RelayCacheTracker") @@ -174,14 +174,9 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol, @unchecked Sendable { return AnyCancellable() } - return self.apiProxy.getRelays(etag: "hello", retryStrategy: .noRetry) { result in - print(result) + return self.apiProxy.getRelays(etag: cachedRelays?.etag, retryStrategy: .noRetry) { result in finish(self.handleResponse(result: result)) } - -// return self.apiProxy.mullvadApiGetRelayList(retryStrategy: .noRetry, etag: cachedRelays.etag) { result in -// finish(self.handleResponse(result: result)) -// } } operation.addObserver( diff --git a/ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift b/ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift index f011a15af8..074e970ead 100644 --- a/ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift @@ -43,7 +43,7 @@ struct RelayFilterViewModelTests { arguments: [ RelayFilter.Ownership.any, RelayFilter.Ownership.owned, - RelayFilter.Ownership.rented + RelayFilter.Ownership.rented, ] ) func testAvailableProvidersByOwnership(_ ownership: RelayFilter.Ownership) { @@ -61,7 +61,7 @@ struct RelayFilterViewModelTests { arguments: [ RelayFilterDataSourceItem(name: "DataPacket", type: .provider, isEnabled: true), RelayFilterDataSourceItem(name: "All Providers", type: .allProviders, isEnabled: true), - RelayFilterDataSourceItem(name: "Blix", type: .provider, isEnabled: true) + RelayFilterDataSourceItem(name: "Blix", type: .provider, isEnabled: true), ] ) func testToggleFilterItem(_ item: RelayFilterDataSourceItem) { @@ -85,7 +85,7 @@ struct RelayFilterViewModelTests { "Toggles relay provider filter items correctly", arguments: [ RelayFilterDataSourceItem.ownedOwnershipItem, - RelayFilterDataSourceItem.rentedOwnershipItem + RelayFilterDataSourceItem.rentedOwnershipItem, ] ) func testToggleRelayProviderFilterItem(_ item: RelayFilterDataSourceItem) { @@ -110,7 +110,7 @@ struct RelayFilterViewModelTests { arguments: [ (RelayFilter.Ownership.any, RelayFilterDataSourceItem.anyOwnershipItem), (RelayFilter.Ownership.owned, RelayFilterDataSourceItem.ownedOwnershipItem), - (RelayFilter.Ownership.rented, RelayFilterDataSourceItem.rentedOwnershipItem) + (RelayFilter.Ownership.rented, RelayFilterDataSourceItem.rentedOwnershipItem), ] ) func testOwnershipItemForFilter( diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index d3cc30108d..4dfc9b2e8b 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -8,6 +8,7 @@ use talpid_types::net::wireguard; use std::{ collections::BTreeMap, + future::Future, net::{IpAddr, Ipv4Addr, Ipv6Addr}, ops::RangeInclusive, time::Duration, @@ -28,42 +29,48 @@ impl RelayListProxy { } /// Fetch the relay list - pub async fn relay_list( + pub fn relay_list( &self, etag: Option<String>, - ) -> Result<Option<relay_list::RelayList>, rest::Error> { - let response = self.relay_list_response(etag.clone()).await.map_err(rest::Error::from)?; + ) -> impl Future<Output = Result<Option<relay_list::RelayList>, rest::Error>> { + let request = self.relay_list_response(etag.clone()); - if etag.is_some() && response.status() == StatusCode::NOT_MODIFIED { - return Ok(None); - } + async move { + let response = request.await.map_err(rest::Error::from)?; + + if etag.is_some() && response.status() == StatusCode::NOT_MODIFIED { + return Ok(None); + } - let etag = Self::extract_etag(&response); + let etag = Self::extract_etag(&response); - let relay_list: ServerRelayList = response.deserialize().await?; - Ok(Some(relay_list.into_relay_list(etag))) + let relay_list: ServerRelayList = response.deserialize().await?; + Ok(Some(relay_list.into_relay_list(etag))) + } } - pub async fn relay_list_response( + pub fn relay_list_response( &self, etag: Option<String>, - ) -> Result<rest::Response<Incoming>, rest::Error> { + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { let service = self.handle.service.clone(); let request = self.handle.factory.get("app/v1/relays"); - let mut request = request? - .timeout(RELAY_LIST_TIMEOUT) - .expected_status(&[StatusCode::NOT_MODIFIED, StatusCode::OK]); + async move { + let mut request = request? + .timeout(RELAY_LIST_TIMEOUT) + .expected_status(&[StatusCode::NOT_MODIFIED, StatusCode::OK]); - if let Some(ref tag) = etag { - request = request.header(header::IF_NONE_MATCH, tag)?; - } + if let Some(ref tag) = etag { + request = request.header(header::IF_NONE_MATCH, tag)?; + } - let response = service.request(request).await?; + let response = service.request(request).await?; - Ok(response) + Ok(response) + } } - + pub fn extract_etag(response: &rest::Response<Incoming>) -> Option<String> { response .headers() @@ -78,7 +85,6 @@ impl RelayListProxy { } } - #[derive(Debug, serde::Deserialize)] struct ServerRelayList { locations: BTreeMap<String, Location>, diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs index ef7b18af9b..c918dda61f 100644 --- a/mullvad-ios/src/api_client/api.rs +++ b/mullvad-ios/src/api_client/api.rs @@ -1,14 +1,14 @@ -use std::{ffi::CStr, ptr::null}; +use std::ffi::CStr; use mullvad_api::{ rest::{self, MullvadRestHandle}, ApiProxy, RelayListProxy, }; -use talpid_future::retry::retry_future; use super::{ cancellation::{RequestCancelHandle, SwiftCancelHandle}, completion::{CompletionCookie, SwiftCompletionHandler}, + do_request, response::SwiftMullvadApiResponse, retry_strategy::{RetryStrategy, SwiftRetryStrategy}, SwiftApiContext, @@ -54,24 +54,18 @@ pub unsafe extern "C" fn mullvad_api_get_addresses( RequestCancelHandle::new(task, completion_handler.clone()).into_swift() } -async fn mullvad_api_get_addresses_inner( - rest_client: MullvadRestHandle, - retry_strategy: RetryStrategy, -) -> Result<SwiftMullvadApiResponse, rest::Error> { - let api = ApiProxy::new(rest_client); - - let future_factory = || api.get_api_addrs_response(); - - let should_retry = |result: &Result<_, rest::Error>| match result { - Err(err) => err.is_network_error(), - Ok(_) => false, - }; - - let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?; - - SwiftMullvadApiResponse::with_body(response).await -} - +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_api_init_new`. +/// +/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is +/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this +/// type is intended to be used. +/// +/// `etag` must be a pointer to a null terminated string. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. #[no_mangle] pub unsafe extern "C" fn mullvad_api_get_relays( api_context: SwiftApiContext, @@ -90,14 +84,16 @@ pub unsafe extern "C" fn mullvad_api_get_relays( let retry_strategy = unsafe { retry_strategy.into_rust() }; let mut maybe_etag: Option<String> = None; - if etag != null() { + if !etag.is_null() { let unwrapped_tag = unsafe { CStr::from_ptr(etag.cast()) }.to_str().unwrap(); maybe_etag = Some(String::from(unwrapped_tag)); } let completion = completion_handler.clone(); let task = tokio_handle.clone().spawn(async move { - match mullvad_api_get_relays_inner(api_context.rest_handle(), retry_strategy, maybe_etag).await { + match mullvad_api_get_relays_inner(api_context.rest_handle(), retry_strategy, maybe_etag) + .await + { Ok(response) => completion.finish(response), Err(err) => { log::error!("{err:?}"); @@ -109,6 +105,17 @@ pub unsafe extern "C" fn mullvad_api_get_relays( RequestCancelHandle::new(task, completion_handler.clone()).into_swift() } +async fn mullvad_api_get_addresses_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = ApiProxy::new(rest_client); + + let future_factory = || api.get_api_addrs_response(); + + do_request(retry_strategy, future_factory).await +} + async fn mullvad_api_get_relays_inner( rest_client: MullvadRestHandle, retry_strategy: RetryStrategy, @@ -118,14 +125,5 @@ async fn mullvad_api_get_relays_inner( let future_factory = || api.relay_list_response(etag.clone()); - let should_retry = |result: &Result<_, rest::Error>| match result { - Err(err) => err.is_network_error(), - Ok(_) => false, - }; - - - - let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?; - - SwiftMullvadApiResponse::with_body(response).await -}
\ No newline at end of file + do_request(retry_strategy, future_factory).await +} diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs index 98443fd0d9..98b41c103f 100644 --- a/mullvad-ios/src/api_client/mod.rs +++ b/mullvad-ios/src/api_client/mod.rs @@ -1,10 +1,13 @@ -use std::{ffi::CStr, sync::Arc}; +use std::{ffi::CStr, future::Future, sync::Arc}; use mullvad_api::{ proxy::{ApiConnectionMode, StaticConnectionModeProvider}, - rest::MullvadRestHandle, + rest::{self, MullvadRestHandle}, ApiEndpoint, Runtime, }; +use response::SwiftMullvadApiResponse; +use retry_strategy::RetryStrategy; +use talpid_future::retry::retry_future; mod api; mod cancellation; @@ -81,3 +84,21 @@ pub extern "C" fn mullvad_api_init_new(host: *const u8, address: *const u8) -> S SwiftApiContext::new(api_context) } + +async fn do_request<F, T>( + retry_strategy: RetryStrategy, + future_factory: F, +) -> Result<SwiftMullvadApiResponse, rest::Error> +where + F: Fn() -> T, + T: Future<Output = Result<rest::Response<hyper::body::Incoming>, rest::Error>>, +{ + let should_retry = |result: &Result<_, rest::Error>| match result { + Err(err) => err.is_network_error(), + Ok(_) => false, + }; + + let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?; + + SwiftMullvadApiResponse::with_body(response).await +} diff --git a/mullvad-ios/src/api_client/response.rs b/mullvad-ios/src/api_client/response.rs index 7f09ae30b1..ac6c0feecc 100644 --- a/mullvad-ios/src/api_client/response.rs +++ b/mullvad-ios/src/api_client/response.rs @@ -34,7 +34,7 @@ impl SwiftMullvadApiResponse { let header_value = CString::new(etag).map_err(|_| rest::Error::InvalidHeaderError)?; header_value.into_raw().cast() - }, + } None => ptr::null_mut(), }; |
