summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-03-31 11:45:12 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-03-31 11:45:12 +0200
commit12b98273d2d3057dfa30d2367d80d3504f3b11e9 (patch)
tree9d5eadcd15c4391e95f223a57db98c5228507d42
parent2640cd40b6a7a2468945b5c4c4be42bbe509c4e5 (diff)
parentb6a47bc377db48f39414d7587995b41bff4f8901 (diff)
downloadmullvadvpn-12b98273d2d3057dfa30d2367d80d3504f3b11e9.tar.xz
mullvadvpn-12b98273d2d3057dfa30d2367d80d3504f3b11e9.zip
Merge branch 'implement-getrelays-using-mullvad-api-ios-1133'
-rw-r--r--ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift7
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift78
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTError.swift1
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift33
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift218
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIError.swift (renamed from ios/MullvadREST/APIRequest/APIError.swift)0
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift (renamed from ios/MullvadREST/APIRequest/APIRequest.swift)18
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequestProxy.swift (renamed from ios/MullvadREST/APIRequest/APIRequestProxy.swift)0
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift (renamed from ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift)2
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift (renamed from ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift)14
-rw-r--r--ios/MullvadREST/Transport/APITransport.swift6
-rw-r--r--ios/MullvadRustRuntime/MullvadApiResponse.swift8
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h20
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj26
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift8
-rw-r--r--mullvad-api/src/relay_list.rs54
-rw-r--r--mullvad-ios/src/api_client/api.rs74
-rw-r--r--mullvad-ios/src/api_client/mod.rs25
-rw-r--r--mullvad-ios/src/api_client/response.rs31
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());
}