Skip to content

Commit 9a7866e

Browse files
committed
Send API requests across app/packet tunnel boundary
1 parent 8ceda7a commit 9a7866e

30 files changed

+773
-100
lines changed

ios/MullvadMockData/MullvadREST/MockProxyFactory.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ public struct MockProxyFactory: ProxyFactoryProtocol {
2929

3030
public static func makeProxyFactory(
3131
transportProvider: any RESTTransportProvider,
32-
addressCache: REST.AddressCache,
33-
apiContext: MullvadApiContext
32+
apiTransportProvider: any APITransportProviderProtocol,
33+
addressCache: REST.AddressCache
3434
) -> any ProxyFactoryProtocol {
3535
let basicConfiguration = REST.ProxyConfiguration(
3636
transportProvider: transportProvider,
37-
addressCacheStore: addressCache,
38-
apiContext: apiContext
37+
apiTransportProvider: apiTransportProvider,
38+
addressCacheStore: addressCache
3939
)
4040

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

4848
let authConfiguration = REST.AuthProxyConfiguration(
4949
proxyConfiguration: basicConfiguration,
50-
accessTokenManager: accessTokenManager,
51-
apiContext: apiContext
50+
accessTokenManager: accessTokenManager
5251
)
5352

5453
return MockProxyFactory(configuration: authConfiguration)
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// APIError.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2025-02-24.
6+
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
public struct APIError: Error, Codable, Sendable {
10+
public let statusCode: Int
11+
public let errorDescription: String
12+
public let serverResponseCode: String?
13+
14+
public init(statusCode: Int, errorDescription: String, serverResponseCode: String?) {
15+
self.statusCode = statusCode
16+
self.errorDescription = errorDescription
17+
self.serverResponseCode = serverResponseCode
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// APIRequest.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2025-02-24.
6+
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
public enum APIRequest: Codable, Sendable {
10+
case getAddressList(_ retryStrategy: REST.RetryStrategy)
11+
12+
var retryStrategy: REST.RetryStrategy {
13+
switch self {
14+
case let .getAddressList(strategy):
15+
return strategy
16+
}
17+
}
18+
}
19+
20+
public struct ProxyAPIRequest: Codable, Sendable {
21+
public let id: UUID
22+
public let request: APIRequest
23+
24+
public init(id: UUID, request: APIRequest) {
25+
self.id = id
26+
self.request = request
27+
}
28+
}
29+
30+
public struct ProxyAPIResponse: Codable, Sendable {
31+
public let data: Data?
32+
public let error: APIError?
33+
34+
public init(data: Data?, error: APIError?) {
35+
self.data = data
36+
self.error = error
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// APIRequestProxy.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2025-02-13.
6+
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import MullvadRustRuntime
10+
import MullvadTypes
11+
12+
public protocol APIRequestProxyProtocol {
13+
func sendRequest(_ proxyRequest: ProxyAPIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void)
14+
func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse
15+
func cancelRequest(identifier: UUID)
16+
}
17+
18+
/// Network request proxy capable of passing serializable requests and responses over the given transport provider.
19+
public final class APIRequestProxy: APIRequestProxyProtocol, @unchecked Sendable {
20+
/// Serial queue used for synchronizing access to class members.
21+
private let dispatchQueue: DispatchQueue
22+
23+
private let transportProvider: APITransportProviderProtocol
24+
25+
/// List of all proxied network requests bypassing VPN.
26+
private var proxiedRequests: [UUID: Cancellable] = [:]
27+
28+
public init(
29+
dispatchQueue: DispatchQueue,
30+
transportProvider: APITransportProviderProtocol
31+
) {
32+
self.dispatchQueue = dispatchQueue
33+
self.transportProvider = transportProvider
34+
}
35+
36+
public func sendRequest(
37+
_ proxyRequest: ProxyAPIRequest,
38+
completion: @escaping @Sendable (ProxyAPIResponse) -> Void
39+
) {
40+
dispatchQueue.async {
41+
guard let transport = self.transportProvider.makeTransport() else {
42+
// Cancel old task, if there's one scheduled.
43+
self.cancelRequest(identifier: proxyRequest.id)
44+
45+
completion(ProxyAPIResponse(data: nil, error: nil))
46+
return
47+
}
48+
49+
let cancellable = transport.sendRequest(proxyRequest.request) { [weak self] response in
50+
guard let self else { return }
51+
52+
// Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
53+
dispatchQueue.async {
54+
_ = self.removeRequest(identifier: proxyRequest.id)
55+
completion(response)
56+
}
57+
}
58+
59+
// Cancel old task, if there's one scheduled.
60+
let oldTask = self.addRequest(identifier: proxyRequest.id, task: cancellable)
61+
oldTask?.cancel()
62+
}
63+
}
64+
65+
public func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse {
66+
return await withCheckedContinuation { continuation in
67+
sendRequest(proxyRequest) { proxyResponse in
68+
continuation.resume(returning: proxyResponse)
69+
}
70+
}
71+
}
72+
73+
public func cancelRequest(identifier: UUID) {
74+
dispatchQueue.async {
75+
let task = self.removeRequest(identifier: identifier)
76+
task?.cancel()
77+
}
78+
}
79+
80+
private func addRequest(identifier: UUID, task: Cancellable) -> Cancellable? {
81+
dispatchPrecondition(condition: .onQueue(dispatchQueue))
82+
return proxiedRequests.updateValue(task, forKey: identifier)
83+
}
84+
85+
private func removeRequest(identifier: UUID) -> Cancellable? {
86+
dispatchPrecondition(condition: .onQueue(dispatchQueue))
87+
return proxiedRequests.removeValue(forKey: identifier)
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//
2+
// MullvadApiNetworkOperation.swift
3+
// MullvadREST
4+
//
5+
// Created by Jon Petersson on 2025-01-29.
6+
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import MullvadLogging
11+
import MullvadRustRuntime
12+
import MullvadTypes
13+
import Operations
14+
15+
private enum MullvadApiTransportError: Error {
16+
case connectionFailed(description: String?)
17+
}
18+
19+
extension REST {
20+
class MullvadApiNetworkOperation<Success: Sendable>: ResultOperation<Success>, @unchecked Sendable {
21+
private let logger: Logger
22+
23+
private let request: APIRequest
24+
private let transportProvider: APITransportProviderProtocol
25+
private var responseDecoder: JSONDecoder
26+
private let responseHandler: any RESTRustResponseHandler<Success>
27+
private var networkTask: Cancellable?
28+
29+
init(
30+
name: String,
31+
dispatchQueue: DispatchQueue,
32+
request: APIRequest,
33+
transportProvider: APITransportProviderProtocol,
34+
responseDecoder: JSONDecoder,
35+
responseHandler: some RESTRustResponseHandler<Success>,
36+
completionHandler: CompletionHandler? = nil
37+
) {
38+
self.request = request
39+
self.transportProvider = transportProvider
40+
self.responseDecoder = responseDecoder
41+
self.responseHandler = responseHandler
42+
43+
var logger = Logger(label: "REST.RustNetworkOperation")
44+
45+
logger[metadataKey: "name"] = .string(name)
46+
self.logger = logger
47+
48+
super.init(
49+
dispatchQueue: dispatchQueue,
50+
completionQueue: .main,
51+
completionHandler: completionHandler
52+
)
53+
}
54+
55+
override public func operationDidCancel() {
56+
networkTask?.cancel()
57+
networkTask = nil
58+
}
59+
60+
override public func main() {
61+
startRequest()
62+
}
63+
64+
func startRequest() {
65+
dispatchPrecondition(condition: .onQueue(dispatchQueue))
66+
67+
guard !isCancelled else {
68+
finish(result: .failure(OperationError.cancelled))
69+
return
70+
}
71+
72+
let transport = transportProvider.makeTransport()
73+
networkTask = transport?.sendRequest(request) { [weak self] response in
74+
guard let self else { return }
75+
76+
if let apiError = response.error {
77+
finish(result: .failure(restError(apiError: apiError)))
78+
return
79+
}
80+
81+
let decodedResponse = responseHandler.handleResponse(response.data)
82+
83+
switch decodedResponse {
84+
case let .success(value):
85+
finish(result: .success(value))
86+
case let .decoding(block):
87+
do {
88+
finish(result: .success(try block()))
89+
} catch {
90+
finish(result: .failure(REST.Error.unhandledResponse(0, nil)))
91+
}
92+
case let .unhandledResponse(error):
93+
finish(result: .failure(REST.Error.unhandledResponse(0, error)))
94+
}
95+
}
96+
}
97+
98+
private func restError(apiError: APIError) -> Error {
99+
guard let serverResponseCode = apiError.serverResponseCode else {
100+
return .transport(MullvadApiTransportError.connectionFailed(description: apiError.errorDescription))
101+
}
102+
103+
let response = REST.ServerErrorResponse(
104+
code: REST.ServerResponseCode(rawValue: serverResponseCode),
105+
detail: apiError.errorDescription
106+
)
107+
return .unhandledResponse(apiError.statusCode, response)
108+
}
109+
}
110+
}

ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
import MullvadRustRuntime
1010
import MullvadTypes
1111

12-
enum MullvadApiRequest {
13-
case getAddressList(retryStrategy: REST.RetryStrategy)
14-
}
12+
public struct MullvadApiRequestFactory: Sendable {
13+
public let apiContext: MullvadApiContext
1514

16-
struct MullvadApiRequestFactory {
17-
let apiContext: MullvadApiContext
15+
public init(apiContext: MullvadApiContext) {
16+
self.apiContext = apiContext
17+
}
1818

19-
func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler {
19+
public func makeRequest(_ request: APIRequest) -> REST.MullvadApiRequestHandler {
2020
{ completion in
2121
let pointerClass = MullvadApiCompletion { apiResponse in
2222
try? completion?(apiResponse)
@@ -37,5 +37,5 @@ struct MullvadApiRequestFactory {
3737
}
3838

3939
extension REST {
40-
typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
40+
public typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
4141
}

ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ extension REST {
6666
retryStrategy: REST.RetryStrategy,
6767
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
6868
) -> Cancellable {
69-
let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList(retryStrategy: retryStrategy))
70-
7169
let responseHandler = rustResponseHandler(
7270
decoding: [AnyIPEndpoint].self,
7371
with: responseDecoder
@@ -76,7 +74,8 @@ extension REST {
7674
let networkOperation = MullvadApiNetworkOperation(
7775
name: "get-api-addrs",
7876
dispatchQueue: dispatchQueue,
79-
requestHandler: requestHandler,
77+
request: .getAddressList(retryStrategy),
78+
transportProvider: configuration.apiTransportProvider,
8079
responseDecoder: responseDecoder,
8180
responseHandler: responseHandler,
8281
completionHandler: completionHandler

ios/MullvadREST/ApiHandlers/RESTProxy.swift

+7-11
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ extension REST {
2727
/// URL request factory.
2828
let requestFactory: REST.RequestFactory
2929

30-
let mullvadApiRequestFactory: MullvadApiRequestFactory
31-
3230
/// URL response decoder.
3331
let responseDecoder: JSONDecoder
3432

@@ -43,7 +41,6 @@ extension REST {
4341

4442
self.configuration = configuration
4543
self.requestFactory = requestFactory
46-
self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext)
4744
self.responseDecoder = responseDecoder
4845
}
4946

@@ -135,17 +132,17 @@ extension REST {
135132

136133
public class ProxyConfiguration: @unchecked Sendable {
137134
public let transportProvider: RESTTransportProvider
135+
public let apiTransportProvider: APITransportProviderProtocol
138136
public let addressCacheStore: AddressCache
139-
public let apiContext: MullvadApiContext
140137

141138
public init(
142139
transportProvider: RESTTransportProvider,
143-
addressCacheStore: AddressCache,
144-
apiContext: MullvadApiContext
140+
apiTransportProvider: APITransportProviderProtocol,
141+
addressCacheStore: AddressCache
145142
) {
146143
self.transportProvider = transportProvider
144+
self.apiTransportProvider = apiTransportProvider
147145
self.addressCacheStore = addressCacheStore
148-
self.apiContext = apiContext
149146
}
150147
}
151148

@@ -154,15 +151,14 @@ extension REST {
154151

155152
public init(
156153
proxyConfiguration: ProxyConfiguration,
157-
accessTokenManager: RESTAccessTokenManagement,
158-
apiContext: MullvadApiContext
154+
accessTokenManager: RESTAccessTokenManagement
159155
) {
160156
self.accessTokenManager = accessTokenManager
161157

162158
super.init(
163159
transportProvider: proxyConfiguration.transportProvider,
164-
addressCacheStore: proxyConfiguration.addressCacheStore,
165-
apiContext: apiContext
160+
apiTransportProvider: proxyConfiguration.apiTransportProvider,
161+
addressCacheStore: proxyConfiguration.addressCacheStore
166162
)
167163
}
168164
}

0 commit comments

Comments
 (0)