Skip to content

Commit 41d474b

Browse files
committed
Implement an FFI to fetch API IP addresses using mullvad-api
1 parent d3eb91e commit 41d474b

31 files changed

+1039
-30
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import MullvadTypes
1212
import WireGuardKitTypes
1313

1414
struct APIProxyStub: APIQuerying {
15+
func mullvadApiGetAddressList(
16+
retryStrategy: REST.RetryStrategy,
17+
completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]>
18+
) -> Cancellable {
19+
AnyCancellable()
20+
}
21+
1522
func getAddressList(
1623
retryStrategy: REST.RetryStrategy,
1724
completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]>

ios/MullvadMockData/MullvadREST/MockProxyFactory.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import Foundation
1010
import MullvadREST
11+
import MullvadRustRuntime
1112
import MullvadTypes
1213
import WireGuardKitTypes
1314

@@ -28,11 +29,13 @@ public struct MockProxyFactory: ProxyFactoryProtocol {
2829

2930
public static func makeProxyFactory(
3031
transportProvider: any RESTTransportProvider,
31-
addressCache: REST.AddressCache
32+
addressCache: REST.AddressCache,
33+
apiContext: MullvadApiContext
3234
) -> any ProxyFactoryProtocol {
3335
let basicConfiguration = REST.ProxyConfiguration(
3436
transportProvider: transportProvider,
35-
addressCacheStore: addressCache
37+
addressCacheStore: addressCache,
38+
apiContext: apiContext
3639
)
3740

3841
let authenticationProxy = REST.AuthenticationProxy(
@@ -44,7 +47,8 @@ public struct MockProxyFactory: ProxyFactoryProtocol {
4447

4548
let authConfiguration = REST.AuthProxyConfiguration(
4649
proxyConfiguration: basicConfiguration,
47-
accessTokenManager: accessTokenManager
50+
accessTokenManager: accessTokenManager,
51+
apiContext: apiContext
4852
)
4953

5054
return MockProxyFactory(configuration: authConfiguration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// MullvadApiRequestFactory.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2025-02-07.
6+
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import MullvadRustRuntime
10+
import MullvadTypes
11+
12+
enum MullvadApiRequest {
13+
case getAddressList
14+
}
15+
16+
struct MullvadApiRequestFactory {
17+
let apiContext: MullvadApiContext
18+
19+
func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler {
20+
{ completion in
21+
let pointerClass = MullvadApiCompletion { apiResponse in
22+
try? completion?(apiResponse)
23+
}
24+
25+
let rawPointer = Unmanaged.passRetained(pointerClass).toOpaque()
26+
27+
return switch request {
28+
case .getAddressList:
29+
MullvadApiCancellable(handle: mullvad_api_get_addresses(apiContext.context, rawPointer))
30+
}
31+
}
32+
}
33+
}
34+
35+
extension REST {
36+
typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
37+
}

ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift

+33
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@
77
//
88

99
import Foundation
10+
import MullvadRustRuntime
1011
import MullvadTypes
12+
import Operations
1113
import WireGuardKitTypes
1214

1315
public protocol APIQuerying: Sendable {
16+
func mullvadApiGetAddressList(
17+
retryStrategy: REST.RetryStrategy,
18+
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
19+
) -> Cancellable
20+
1421
func getAddressList(
1522
retryStrategy: REST.RetryStrategy,
1623
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
@@ -55,6 +62,32 @@ extension REST {
5562
)
5663
}
5764

65+
public func mullvadApiGetAddressList(
66+
retryStrategy: REST.RetryStrategy,
67+
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
68+
) -> Cancellable {
69+
let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList)
70+
71+
let responseHandler = rustResponseHandler(
72+
decoding: [AnyIPEndpoint].self,
73+
with: responseDecoder
74+
)
75+
76+
let networkOperation = MullvadApiNetworkOperation(
77+
name: "get-api-addrs",
78+
dispatchQueue: dispatchQueue,
79+
retryStrategy: retryStrategy,
80+
requestHandler: requestHandler,
81+
responseDecoder: responseDecoder,
82+
responseHandler: responseHandler,
83+
completionHandler: completionHandler
84+
)
85+
86+
operationQueue.addOperation(networkOperation)
87+
88+
return networkOperation
89+
}
90+
5891
public func getAddressList(
5992
retryStrategy: REST.RetryStrategy,
6093
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>

ios/MullvadREST/ApiHandlers/RESTDefaults.swift

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Foundation
10+
import MullvadRustRuntime
1011
import MullvadTypes
1112

1213
// swiftlint:disable force_cast
@@ -28,6 +29,13 @@ extension REST {
2829

2930
/// Default network timeout for API requests.
3031
public static let defaultAPINetworkTimeout: Duration = .seconds(10)
32+
33+
/// API context used for API requests via Rust runtime.
34+
// swiftlint:disable:next force_try
35+
public static let apiContext = try! MullvadApiContext(
36+
host: defaultAPIHostname,
37+
address: defaultAPIEndpoint.description
38+
)
3139
}
3240

3341
// swiftlint:enable force_cast

ios/MullvadREST/ApiHandlers/RESTProxy.swift

+12-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Foundation
10+
import MullvadRustRuntime
1011
import MullvadTypes
1112
import Operations
1213

@@ -26,6 +27,8 @@ extension REST {
2627
/// URL request factory.
2728
let requestFactory: REST.RequestFactory
2829

30+
let mullvadApiRequestFactory: MullvadApiRequestFactory
31+
2932
/// URL response decoder.
3033
let responseDecoder: JSONDecoder
3134

@@ -40,6 +43,7 @@ extension REST {
4043

4144
self.configuration = configuration
4245
self.requestFactory = requestFactory
46+
self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext)
4347
self.responseDecoder = responseDecoder
4448
}
4549

@@ -132,13 +136,16 @@ extension REST {
132136
public class ProxyConfiguration: @unchecked Sendable {
133137
public let transportProvider: RESTTransportProvider
134138
public let addressCacheStore: AddressCache
139+
public let apiContext: MullvadApiContext
135140

136141
public init(
137142
transportProvider: RESTTransportProvider,
138-
addressCacheStore: AddressCache
143+
addressCacheStore: AddressCache,
144+
apiContext: MullvadApiContext
139145
) {
140146
self.transportProvider = transportProvider
141147
self.addressCacheStore = addressCacheStore
148+
self.apiContext = apiContext
142149
}
143150
}
144151

@@ -147,13 +154,15 @@ extension REST {
147154

148155
public init(
149156
proxyConfiguration: ProxyConfiguration,
150-
accessTokenManager: RESTAccessTokenManagement
157+
accessTokenManager: RESTAccessTokenManagement,
158+
apiContext: MullvadApiContext
151159
) {
152160
self.accessTokenManager = accessTokenManager
153161

154162
super.init(
155163
transportProvider: proxyConfiguration.transportProvider,
156-
addressCacheStore: proxyConfiguration.addressCacheStore
164+
addressCacheStore: proxyConfiguration.addressCacheStore,
165+
apiContext: apiContext
157166
)
158167
}
159168
}

ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift

+10-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//
88

99
import Foundation
10+
import MullvadRustRuntime
11+
1012
public protocol ProxyFactoryProtocol {
1113
var configuration: REST.AuthProxyConfiguration { get }
1214

@@ -16,7 +18,8 @@ public protocol ProxyFactoryProtocol {
1618

1719
static func makeProxyFactory(
1820
transportProvider: RESTTransportProvider,
19-
addressCache: REST.AddressCache
21+
addressCache: REST.AddressCache,
22+
apiContext: MullvadApiContext
2023
) -> ProxyFactoryProtocol
2124
}
2225

@@ -26,11 +29,13 @@ extension REST {
2629

2730
public static func makeProxyFactory(
2831
transportProvider: any RESTTransportProvider,
29-
addressCache: REST.AddressCache
32+
addressCache: REST.AddressCache,
33+
apiContext: MullvadApiContext
3034
) -> any ProxyFactoryProtocol {
3135
let basicConfiguration = REST.ProxyConfiguration(
3236
transportProvider: transportProvider,
33-
addressCacheStore: addressCache
37+
addressCacheStore: addressCache,
38+
apiContext: apiContext
3439
)
3540

3641
let authenticationProxy = REST.AuthenticationProxy(
@@ -42,7 +47,8 @@ extension REST {
4247

4348
let authConfiguration = REST.AuthProxyConfiguration(
4449
proxyConfiguration: basicConfiguration,
45-
accessTokenManager: accessTokenManager
50+
accessTokenManager: accessTokenManager,
51+
apiContext: apiContext
4652
)
4753

4854
return ProxyFactory(configuration: authConfiguration)

ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift

+57
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Foundation
10+
import MullvadRustRuntime
1011
import MullvadTypes
1112

1213
protocol RESTResponseHandler<Success> {
@@ -15,7 +16,14 @@ protocol RESTResponseHandler<Success> {
1516
func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success>
1617
}
1718

19+
protocol RESTRustResponseHandler<Success> {
20+
associatedtype Success
21+
22+
func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
23+
}
24+
1825
extension REST {
26+
// TODO: We could probably remove the `decoding` case when network requests are fully merged to Mullvad API.
1927
/// Responser handler result type.
2028
enum ResponseHandlerResult<Success> {
2129
/// Response handler succeeded and produced a value.
@@ -66,4 +74,53 @@ extension REST {
6674
}
6775
}
6876
}
77+
78+
final class RustResponseHandler<Success>: RESTRustResponseHandler {
79+
typealias HandlerBlock = (MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
80+
81+
private let handlerBlock: HandlerBlock
82+
83+
init(_ block: @escaping HandlerBlock) {
84+
handlerBlock = block
85+
}
86+
87+
func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> {
88+
handlerBlock(response)
89+
}
90+
}
91+
92+
/// Returns default response handler that parses JSON response into the
93+
/// given `Decodable` type if possible, otherwise attempts to decode
94+
/// the server error.
95+
static func rustResponseHandler<T: Decodable>(
96+
decoding type: T.Type,
97+
with decoder: JSONDecoder
98+
) -> RustResponseHandler<T> {
99+
RustResponseHandler { response in
100+
guard let body = response.body else {
101+
return .unhandledResponse(nil)
102+
}
103+
104+
do {
105+
let decoded = try decoder.decode(type, from: body)
106+
return .decoding { decoded }
107+
} catch {
108+
return .unhandledResponse(
109+
try? decoder.decode(
110+
ServerErrorResponse.self,
111+
from: body
112+
)
113+
)
114+
}
115+
}
116+
}
117+
118+
/// Returns default response handler that parses JSON response into the
119+
/// given `Decodable` type if possible, otherwise attempts to decode
120+
/// the server error.
121+
static func rustEmptyResponseHandler() -> RustResponseHandler<Void> {
122+
RustResponseHandler { _ in
123+
.success(())
124+
}
125+
}
69126
}

0 commit comments

Comments
 (0)