summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-03-19 16:32:36 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-03-31 11:39:29 +0200
commitb6a47bc377db48f39414d7587995b41bff4f8901 (patch)
tree9d5eadcd15c4391e95f223a57db98c5228507d42
parenteb72686c74607872ee510b432a442ea10baa1b86 (diff)
downloadmullvadvpn-b6a47bc377db48f39414d7587995b41bff4f8901.tar.xz
mullvadvpn-b6a47bc377db48f39414d7587995b41bff4f8901.zip
Move Mullvad API functions to new file
-rw-r--r--ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift15
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift130
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTError.swift1
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift9
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift218
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift4
-rw-r--r--ios/MullvadREST/Transport/APITransport.swift3
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h14
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift9
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/View controllers/Filter/RelayFilterViewModelTests.swift8
-rw-r--r--mullvad-api/src/relay_list.rs48
-rw-r--r--mullvad-ios/src/api_client/api.rs64
-rw-r--r--mullvad-ios/src/api_client/mod.rs25
-rw-r--r--mullvad-ios/src/api_client/response.rs2
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(),
};