Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transport api requests across app/packet tunnel boundary #7698

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions ios/MullvadMockData/MullvadREST/MockProxyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ public struct MockProxyFactory: ProxyFactoryProtocol {

public static func makeProxyFactory(
transportProvider: any RESTTransportProvider,
addressCache: REST.AddressCache,
apiContext: MullvadApiContext
apiTransportProvider: any APITransportProviderProtocol,
addressCache: REST.AddressCache
) -> any ProxyFactoryProtocol {
let basicConfiguration = REST.ProxyConfiguration(
transportProvider: transportProvider,
addressCacheStore: addressCache,
apiContext: apiContext
apiTransportProvider: apiTransportProvider,
addressCacheStore: addressCache
)

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

let authConfiguration = REST.AuthProxyConfiguration(
proxyConfiguration: basicConfiguration,
accessTokenManager: accessTokenManager,
apiContext: apiContext
accessTokenManager: accessTokenManager
)

return MockProxyFactory(configuration: authConfiguration)
Expand Down
19 changes: 19 additions & 0 deletions ios/MullvadREST/APIRequest/APIError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// APIError.swift
// MullvadVPN
//
// Created by Jon Petersson on 2025-02-24.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

public struct APIError: Error, Codable, Sendable {
public let statusCode: Int
public let errorDescription: String
public let serverResponseCode: String?

public init(statusCode: Int, errorDescription: String, serverResponseCode: String?) {
self.statusCode = statusCode
self.errorDescription = errorDescription
self.serverResponseCode = serverResponseCode
}
}
38 changes: 38 additions & 0 deletions ios/MullvadREST/APIRequest/APIRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// APIRequest.swift
// MullvadVPN
//
// Created by Jon Petersson on 2025-02-24.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

public enum APIRequest: Codable, Sendable {
case getAddressList(_ retryStrategy: REST.RetryStrategy)

var retryStrategy: REST.RetryStrategy {
switch self {
case let .getAddressList(strategy):
return strategy
}
}
}

public struct ProxyAPIRequest: Codable, Sendable {
public let id: UUID
public let request: APIRequest

public init(id: UUID, request: APIRequest) {
self.id = id
self.request = request
}
}

public struct ProxyAPIResponse: Codable, Sendable {
public let data: Data?
public let error: APIError?

public init(data: Data?, error: APIError?) {
self.data = data
self.error = error
}
}
89 changes: 89 additions & 0 deletions ios/MullvadREST/APIRequest/APIRequestProxy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// APIRequestProxy.swift
// MullvadVPN
//
// Created by Jon Petersson on 2025-02-13.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadRustRuntime
import MullvadTypes

public protocol APIRequestProxyProtocol {
func sendRequest(_ proxyRequest: ProxyAPIRequest, completion: @escaping @Sendable (ProxyAPIResponse) -> Void)
func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse
func cancelRequest(identifier: UUID)
}

/// Network request proxy capable of passing serializable requests and responses over the given transport provider.
public final class APIRequestProxy: APIRequestProxyProtocol, @unchecked Sendable {
/// Serial queue used for synchronizing access to class members.
private let dispatchQueue: DispatchQueue

private let transportProvider: APITransportProviderProtocol

/// List of all proxied network requests bypassing VPN.
private var proxiedRequests: [UUID: Cancellable] = [:]

public init(
dispatchQueue: DispatchQueue,
transportProvider: APITransportProviderProtocol
) {
self.dispatchQueue = dispatchQueue
self.transportProvider = transportProvider
}

public func sendRequest(
_ proxyRequest: ProxyAPIRequest,
completion: @escaping @Sendable (ProxyAPIResponse) -> Void
) {
dispatchQueue.async {
guard let transport = self.transportProvider.makeTransport() else {
// Cancel old task, if there's one scheduled.
self.cancelRequest(identifier: proxyRequest.id)

completion(ProxyAPIResponse(data: nil, error: nil))
return
}

let cancellable = transport.sendRequest(proxyRequest.request) { [weak self] response in
guard let self else { return }

// Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests`
dispatchQueue.async {
_ = self.removeRequest(identifier: proxyRequest.id)
completion(response)
}
}

// Cancel old task, if there's one scheduled.
let oldTask = self.addRequest(identifier: proxyRequest.id, task: cancellable)
oldTask?.cancel()
}
}

public func sendRequest(_ proxyRequest: ProxyAPIRequest) async -> ProxyAPIResponse {
return await withCheckedContinuation { continuation in
sendRequest(proxyRequest) { proxyResponse in
continuation.resume(returning: proxyResponse)
}
}
}

public func cancelRequest(identifier: UUID) {
dispatchQueue.async {
let task = self.removeRequest(identifier: identifier)
task?.cancel()
}
}

private func addRequest(identifier: UUID, task: Cancellable) -> Cancellable? {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
return proxiedRequests.updateValue(task, forKey: identifier)
}

private func removeRequest(identifier: UUID) -> Cancellable? {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
return proxiedRequests.removeValue(forKey: identifier)
}
}
110 changes: 110 additions & 0 deletions ios/MullvadREST/ApiHandlers/MullvadApiNetworkOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// MullvadApiNetworkOperation.swift
// MullvadREST
//
// Created by Jon Petersson on 2025-01-29.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import MullvadRustRuntime
import MullvadTypes
import Operations

private enum MullvadApiTransportError: Error {
case connectionFailed(description: String?)
}

extension REST {
class MullvadApiNetworkOperation<Success: Sendable>: ResultOperation<Success>, @unchecked Sendable {
private let logger: Logger

private let request: APIRequest
private let transportProvider: APITransportProviderProtocol
private var responseDecoder: JSONDecoder
private let responseHandler: any RESTRustResponseHandler<Success>
private var networkTask: Cancellable?

init(
name: String,
dispatchQueue: DispatchQueue,
request: APIRequest,
transportProvider: APITransportProviderProtocol,
responseDecoder: JSONDecoder,
responseHandler: some RESTRustResponseHandler<Success>,
completionHandler: CompletionHandler? = nil
) {
self.request = request
self.transportProvider = transportProvider
self.responseDecoder = responseDecoder
self.responseHandler = responseHandler

var logger = Logger(label: "REST.RustNetworkOperation")

logger[metadataKey: "name"] = .string(name)
self.logger = logger

super.init(
dispatchQueue: dispatchQueue,
completionQueue: .main,
completionHandler: completionHandler
)
}

override public func operationDidCancel() {
networkTask?.cancel()
networkTask = nil
}

override public func main() {
startRequest()
}

func startRequest() {
dispatchPrecondition(condition: .onQueue(dispatchQueue))

guard !isCancelled else {
finish(result: .failure(OperationError.cancelled))
return
}

let transport = transportProvider.makeTransport()
networkTask = transport?.sendRequest(request) { [weak self] response in
guard let self else { return }

if let apiError = response.error {
finish(result: .failure(restError(apiError: apiError)))
return
}

let decodedResponse = responseHandler.handleResponse(response.data)

switch decodedResponse {
case let .success(value):
finish(result: .success(value))
case let .decoding(block):
do {
finish(result: .success(try block()))
} catch {
finish(result: .failure(REST.Error.unhandledResponse(0, nil)))
}
case let .unhandledResponse(error):
finish(result: .failure(REST.Error.unhandledResponse(0, error)))
}
}
}

private func restError(apiError: APIError) -> Error {
guard let serverResponseCode = apiError.serverResponseCode else {
return .transport(MullvadApiTransportError.connectionFailed(description: apiError.errorDescription))
}

let response = REST.ServerErrorResponse(
code: REST.ServerResponseCode(rawValue: serverResponseCode),
detail: apiError.errorDescription
)
return .unhandledResponse(apiError.statusCode, response)
}
}
}
14 changes: 7 additions & 7 deletions ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
import MullvadRustRuntime
import MullvadTypes

enum MullvadApiRequest {
case getAddressList(retryStrategy: REST.RetryStrategy)
}
public struct MullvadApiRequestFactory: Sendable {
public let apiContext: MullvadApiContext

struct MullvadApiRequestFactory {
let apiContext: MullvadApiContext
public init(apiContext: MullvadApiContext) {
self.apiContext = apiContext
}

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

extension REST {
typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
public typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
}
5 changes: 2 additions & 3 deletions ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ extension REST {
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
) -> Cancellable {
let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList(retryStrategy: retryStrategy))

let responseHandler = rustResponseHandler(
decoding: [AnyIPEndpoint].self,
with: responseDecoder
Expand All @@ -76,7 +74,8 @@ extension REST {
let networkOperation = MullvadApiNetworkOperation(
name: "get-api-addrs",
dispatchQueue: dispatchQueue,
requestHandler: requestHandler,
request: .getAddressList(retryStrategy),
transportProvider: configuration.apiTransportProvider,
responseDecoder: responseDecoder,
responseHandler: responseHandler,
completionHandler: completionHandler
Expand Down
18 changes: 7 additions & 11 deletions ios/MullvadREST/ApiHandlers/RESTProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ extension REST {
/// URL request factory.
let requestFactory: REST.RequestFactory

let mullvadApiRequestFactory: MullvadApiRequestFactory

/// URL response decoder.
let responseDecoder: JSONDecoder

Expand All @@ -43,7 +41,6 @@ extension REST {

self.configuration = configuration
self.requestFactory = requestFactory
self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext)
self.responseDecoder = responseDecoder
}

Expand Down Expand Up @@ -135,17 +132,17 @@ extension REST {

public class ProxyConfiguration: @unchecked Sendable {
public let transportProvider: RESTTransportProvider
public let apiTransportProvider: APITransportProviderProtocol
public let addressCacheStore: AddressCache
public let apiContext: MullvadApiContext

public init(
transportProvider: RESTTransportProvider,
addressCacheStore: AddressCache,
apiContext: MullvadApiContext
apiTransportProvider: APITransportProviderProtocol,
addressCacheStore: AddressCache
) {
self.transportProvider = transportProvider
self.apiTransportProvider = apiTransportProvider
self.addressCacheStore = addressCacheStore
self.apiContext = apiContext
}
}

Expand All @@ -154,15 +151,14 @@ extension REST {

public init(
proxyConfiguration: ProxyConfiguration,
accessTokenManager: RESTAccessTokenManagement,
apiContext: MullvadApiContext
accessTokenManager: RESTAccessTokenManagement
) {
self.accessTokenManager = accessTokenManager

super.init(
transportProvider: proxyConfiguration.transportProvider,
addressCacheStore: proxyConfiguration.addressCacheStore,
apiContext: apiContext
apiTransportProvider: proxyConfiguration.apiTransportProvider,
addressCacheStore: proxyConfiguration.addressCacheStore
)
}
}
Expand Down
Loading
Loading