diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-03-31 11:45:12 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-03-31 11:45:12 +0200 |
| commit | 12b98273d2d3057dfa30d2367d80d3504f3b11e9 (patch) | |
| tree | 9d5eadcd15c4391e95f223a57db98c5228507d42 | |
| parent | 2640cd40b6a7a2468945b5c4c4be42bbe509c4e5 (diff) | |
| parent | b6a47bc377db48f39414d7587995b41bff4f8901 (diff) | |
| download | mullvadvpn-12b98273d2d3057dfa30d2367d80d3504f3b11e9.tar.xz mullvadvpn-12b98273d2d3057dfa30d2367d80d3504f3b11e9.zip | |
Merge branch 'implement-getrelays-using-mullvad-api-ios-1133'
19 files changed, 484 insertions, 139 deletions
diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift index 3c8350b8fb..1330be345f 100644 --- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift @@ -12,13 +12,6 @@ import MullvadTypes import WireGuardKitTypes struct APIProxyStub: APIQuerying { - func mullvadApiGetAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> 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 908e0bddd4..0d75b25d74 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -13,11 +13,6 @@ 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]> @@ -62,30 +57,6 @@ extension REST { ) } - public func mullvadApiGetAddressList( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> - ) -> Cancellable { - let responseHandler = rustResponseHandler( - decoding: [AnyIPEndpoint].self, - with: responseDecoder - ) - - let networkOperation = MullvadApiNetworkOperation( - name: "get-api-addrs", - dispatchQueue: dispatchQueue, - request: .getAddressList(retryStrategy), - 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]> @@ -314,64 +285,15 @@ extension REST { // MARK: - Response types - public enum ServerRelaysCacheResponse: Sendable { - 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 c6197e983e..f067ccabdf 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(_ body: Data?) -> REST.ResponseHandlerResult<Success> + func handleResponse(_ resonse: ProxyAPIResponse) -> REST.ResponseHandlerResult<Success> } extension REST { @@ -76,7 +76,7 @@ extension REST { } final class RustResponseHandler<Success>: RESTRustResponseHandler { - typealias HandlerBlock = (Data?) -> REST.ResponseHandlerResult<Success> + typealias HandlerBlock = (ProxyAPIResponse) -> REST.ResponseHandlerResult<Success> private let handlerBlock: HandlerBlock @@ -84,8 +84,8 @@ extension REST { handlerBlock = block } - func handleResponse(_ body: Data?) -> REST.ResponseHandlerResult<Success> { - handlerBlock(body) + func handleResponse(_ response: ProxyAPIResponse) -> REST.ResponseHandlerResult<Success> { + handlerBlock(response) } } @@ -96,13 +96,30 @@ extension REST { decoding type: T.Type, with decoder: JSONDecoder ) -> RustResponseHandler<T> { - RustResponseHandler { data in - guard let data else { + RustResponseHandler { (response: ProxyAPIResponse) in + guard let data = response.data else { return .unhandledResponse(nil) } - return if let decoded = try? decoder.decode(type, from: data) { - .decoding { decoded } + do { + let decoded = try decoder.decode(type, from: data) + return .decoding { decoded } + } catch { + return .unhandledResponse(ServerErrorResponse(code: .parsingError, detail: error.localizedDescription)) + } + } + } + + static func rustCustomResponseHandler<T: Decodable>( + conversion: @escaping (_ data: Data, _ etag: String?) -> T? + ) -> RustResponseHandler<T> { + RustResponseHandler { (response: ProxyAPIResponse) in + guard let data = response.data else { + return .unhandledResponse(nil) + } + + return if let convertedResponse = conversion(data, response.etag) { + .decoding { convertedResponse } } else { .unhandledResponse(nil) } 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/APIRequest/APIError.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIError.swift index f62fde619a..f62fde619a 100644 --- a/ios/MullvadREST/APIRequest/APIError.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIError.swift diff --git a/ios/MullvadREST/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift index 4fff7bd32b..ea51e22508 100644 --- a/ios/MullvadREST/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -8,11 +8,21 @@ 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-list" + } + } var retryStrategy: REST.RetryStrategy { switch self { - case let .getAddressList(strategy): - return strategy + case let .getAddressList(strategy), let .getRelayList(strategy, _): + strategy } } } @@ -30,9 +40,11 @@ public struct ProxyAPIRequest: Codable, Sendable { public struct ProxyAPIResponse: Codable, Sendable { public let data: Data? public let error: APIError? + public let etag: String? - public init(data: Data?, error: APIError?) { + public init(data: Data?, error: APIError?, etag: String? = nil) { self.data = data self.error = error + self.etag = etag } } diff --git a/ios/MullvadREST/APIRequest/APIRequestProxy.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift index 8e2ac4fad2..8e2ac4fad2 100644 --- a/ios/MullvadREST/APIRequest/APIRequestProxy.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift b/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift index 68d4ecb0c7..12d9c0346d 100644 --- a/ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift @@ -78,7 +78,7 @@ extension REST { return } - let decodedResponse = responseHandler.handleResponse(response.data) + let decodedResponse = responseHandler.handleResponse(response) switch decodedResponse { case let .success(value): diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift index d361beef1b..fd64408e0e 100644 --- a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift @@ -18,19 +18,27 @@ public struct MullvadApiRequestFactory: Sendable { public func makeRequest(_ request: APIRequest) -> REST.MullvadApiRequestHandler { { completion in - let pointerClass = MullvadApiCompletion { apiResponse in + let completionPointer = MullvadApiCompletion { apiResponse in try? completion?(apiResponse) } - let rawPointer = Unmanaged.passRetained(pointerClass).toOpaque() + let rawCompletionPointer = Unmanaged.passRetained(completionPointer).toOpaque() return switch request { case let .getAddressList(retryStrategy): MullvadApiCancellable(handle: mullvad_api_get_addresses( apiContext.context, - rawPointer, + rawCompletionPointer, retryStrategy.toRustStrategy() )) + + case let .getRelayList(retryStrategy, etag: etag): + MullvadApiCancellable(handle: mullvad_api_get_relays( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + etag + )) } } } diff --git a/ios/MullvadREST/Transport/APITransport.swift b/ios/MullvadREST/Transport/APITransport.swift index 811e775a19..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 ?? "", @@ -44,7 +45,8 @@ public final class APITransport: APITransportProtocol { completion(ProxyAPIResponse( data: response.body, - error: error + error: error, + etag: response.etag )) } } diff --git a/ios/MullvadRustRuntime/MullvadApiResponse.swift b/ios/MullvadRustRuntime/MullvadApiResponse.swift index 7836d43971..ddead026b3 100644 --- a/ios/MullvadRustRuntime/MullvadApiResponse.swift +++ b/ios/MullvadRustRuntime/MullvadApiResponse.swift @@ -25,6 +25,14 @@ public class MullvadApiResponse { return Data(UnsafeBufferPointer(start: body, count: Int(response.body_size))) } + public var etag: String? { + return if response.etag == nil { + nil + } else { + String(cString: response.etag) + } + } + public var errorDescription: String? { return if response.error_description == nil { nil diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index e054de0044..8a199559b5 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -43,6 +43,7 @@ typedef struct SwiftRetryStrategy { typedef struct SwiftMullvadApiResponse { uint8_t *body; uintptr_t body_size; + uint8_t *etag; uint16_t status_code; uint8_t *error_description; uint8_t *server_response_code; @@ -114,6 +115,25 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co 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, + const uint8_t *etag); + +/** * Called by the Swift side to signal that a Mullvad API call should be cancelled. * After this call, the cancel token is no longer valid. * diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 1e623f8bc0..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>"; }; @@ -2681,8 +2683,8 @@ 06799ABD28F98E1D00ACD94E /* MullvadREST */ = { isa = PBXGroup; children = ( + 7A2C0E872D82E450003D8048 /* MullvadAPI */, F06045F02B2324DA00B2D37A /* ApiHandlers */, - 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */, 062B45A228FD4C0F00746E77 /* Assets */, 7AD63A422CDA661B00445268 /* Extensions */, 582FFA82290A84E700895745 /* Info.plist */, @@ -4161,6 +4163,25 @@ path = Alert; sourceTree = "<group>"; }; + 7A2C0E872D82E450003D8048 /* MullvadAPI */ = { + isa = PBXGroup; + children = ( + 7A2C0E8A2D8B13DB003D8048 /* APIHandlers */, + 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */, + 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */, + 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */, + ); + path = MullvadAPI; + sourceTree = "<group>"; + }; + 7A2C0E8A2D8B13DB003D8048 /* APIHandlers */ = { + isa = PBXGroup; + children = ( + 7A2C0E8B2D8B13E8003D8048 /* MullvadAPIProxy.swift */, + ); + path = APIHandlers; + sourceTree = "<group>"; + }; 7A2E7B6B2D6C9E45009EF2C3 /* APIRequest */ = { isa = PBXGroup; children = ( @@ -4540,7 +4561,6 @@ 06AC114128F8413A0037AF9A /* AddressCache.swift */, A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */, 06FAE67128F83CA40033DD93 /* HTTP.swift */, - 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */, 06FAE67228F83CA40033DD93 /* RESTAccessTokenManager.swift */, 06FAE66828F83CA30033DD93 /* RESTAccountsProxy.swift */, 06FAE67328F83CA40033DD93 /* RESTAPIProxy.swift */, @@ -4559,7 +4579,6 @@ 06FAE66628F83CA30033DD93 /* RESTResponseHandler.swift */, 06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */, 06FAE66528F83CA30033DD93 /* RESTURLSession.swift */, - 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */, 06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */, 06FAE66B28F83CA30033DD93 /* SSLPinningURLSessionDelegate.swift */, ); @@ -5709,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/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 f19961bc8a..4dfc9b2e8b 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -2,7 +2,7 @@ use crate::rest; -use hyper::{header, StatusCode}; +use hyper::{body::Incoming, header, StatusCode}; use mullvad_types::{location, relay_list}; use talpid_types::net::wireguard; @@ -32,7 +32,27 @@ impl RelayListProxy { pub fn relay_list( &self, etag: Option<String>, - ) -> impl Future<Output = Result<Option<relay_list::RelayList>, rest::Error>> + use<> { + ) -> impl Future<Output = Result<Option<relay_list::RelayList>, rest::Error>> { + let request = self.relay_list_response(etag.clone()); + + 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 relay_list: ServerRelayList = response.deserialize().await?; + Ok(Some(relay_list.into_relay_list(etag))) + } + } + + pub fn relay_list_response( + &self, + etag: Option<String>, + ) -> impl Future<Output = Result<rest::Response<Incoming>, rest::Error>> { let service = self.handle.service.clone(); let request = self.handle.factory.get("app/v1/relays"); @@ -46,25 +66,23 @@ impl RelayListProxy { } let response = service.request(request).await?; - if etag.is_some() && response.status() == StatusCode::NOT_MODIFIED { - return Ok(None); - } - - let etag = response - .headers() - .get(header::ETAG) - .and_then(|tag| match tag.to_str() { - Ok(tag) => Some(tag.to_string()), - Err(_) => { - log::error!("Ignoring invalid tag from server: {:?}", tag.as_bytes()); - None - } - }); - let relay_list: ServerRelayList = response.deserialize().await?; - Ok(Some(relay_list.into_relay_list(etag))) + Ok(response) } } + + pub fn extract_etag(response: &rest::Response<Incoming>) -> Option<String> { + response + .headers() + .get(header::ETAG) + .and_then(|tag| match tag.to_str() { + Ok(tag) => Some(tag.to_string()), + Err(_) => { + log::error!("Ignoring invalid tag from server: {:?}", tag.as_bytes()); + None + } + }) + } } #[derive(Debug, serde::Deserialize)] diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs index 847e81f0eb..c918dda61f 100644 --- a/mullvad-ios/src/api_client/api.rs +++ b/mullvad-ios/src/api_client/api.rs @@ -1,12 +1,14 @@ +use std::ffi::CStr; + use mullvad_api::{ rest::{self, MullvadRestHandle}, - ApiProxy, + 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, @@ -52,6 +54,57 @@ pub unsafe extern "C" fn mullvad_api_get_addresses( RequestCancelHandle::new(task, completion_handler.clone()).into_swift() } +/// # 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, + completion_cookie: *mut libc::c_void, + retry_strategy: SwiftRetryStrategy, + etag: *const u8, +) -> SwiftCancelHandle { + let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie)); + + let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { + completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); + return SwiftCancelHandle::empty(); + }; + + let api_context = api_context.into_rust_context(); + let retry_strategy = unsafe { retry_strategy.into_rust() }; + + let mut maybe_etag: Option<String> = None; + 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 + { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler.clone()).into_swift() +} + async fn mullvad_api_get_addresses_inner( rest_client: MullvadRestHandle, retry_strategy: RetryStrategy, @@ -60,12 +113,17 @@ async fn mullvad_api_get_addresses_inner( 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, - }; + do_request(retry_strategy, future_factory).await +} + +async fn mullvad_api_get_relays_inner( + rest_client: MullvadRestHandle, + retry_strategy: RetryStrategy, + etag: Option<String>, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = RelayListProxy::new(rest_client); - let response = retry_future(future_factory, should_retry, retry_strategy.delays()).await?; + let future_factory = || api.relay_list_response(etag.clone()); - SwiftMullvadApiResponse::with_body(response).await + 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 6ffbadb5d6..ac6c0feecc 100644 --- a/mullvad-ios/src/api_client/response.rs +++ b/mullvad-ios/src/api_client/response.rs @@ -1,27 +1,47 @@ -use std::{ffi::CString, ptr::null_mut}; +use std::{ + ffi::CString, + ptr::{self, null_mut}, +}; -use mullvad_api::rest::{self, Response}; +use mullvad_api::{ + rest::{self, Response}, + RelayListProxy, +}; #[repr(C)] pub struct SwiftMullvadApiResponse { body: *mut u8, body_size: usize, + etag: *mut u8, status_code: u16, error_description: *mut u8, server_response_code: *mut u8, success: bool, } + impl SwiftMullvadApiResponse { pub async fn with_body(response: Response<hyper::body::Incoming>) -> Result<Self, rest::Error> { + let maybe_etag = RelayListProxy::extract_etag(&response); + let status_code: u16 = response.status().into(); let body: Vec<u8> = response.body().await?; let body_size = body.len(); let body = body.into_boxed_slice(); + let etag = match maybe_etag { + Some(etag) => { + let header_value = + CString::new(etag).map_err(|_| rest::Error::InvalidHeaderError)?; + header_value.into_raw().cast() + } + None => ptr::null_mut(), + }; + Ok(Self { body: Box::<[u8]>::into_raw(body).cast(), body_size, + etag, status_code, error_description: null_mut(), server_response_code: null_mut(), @@ -51,6 +71,7 @@ impl SwiftMullvadApiResponse { Self { body: null_mut(), body_size: 0, + etag: null_mut(), status_code, error_description, server_response_code, @@ -64,6 +85,7 @@ impl SwiftMullvadApiResponse { error_description: c"Request was cancelled".to_owned().into_raw().cast(), body: null_mut(), body_size: 0, + etag: null_mut(), status_code: 0, server_response_code: null_mut(), } @@ -75,6 +97,7 @@ impl SwiftMullvadApiResponse { error_description: c"Failed to get Tokio runtime".to_owned().into_raw().cast(), body: null_mut(), body_size: 0, + etag: null_mut(), status_code: 0, server_response_code: null_mut(), } @@ -94,6 +117,10 @@ pub unsafe extern "C" fn mullvad_response_drop(response: SwiftMullvadApiResponse let _ = Vec::from_raw_parts(response.body, response.body_size, response.body_size); } + if !response.etag.is_null() { + let _ = CString::from_raw(response.etag.cast()); + } + if !response.error_description.is_null() { let _ = CString::from_raw(response.error_description.cast()); } |
