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

Implement an FFI to fetch API IP addresses using mullvad-api #7644

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ 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]>
Expand Down
10 changes: 7 additions & 3 deletions ios/MullvadMockData/MullvadREST/MockProxyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import MullvadREST
import MullvadRustRuntime
import MullvadTypes
import WireGuardKitTypes

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

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

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

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

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

import MullvadRustRuntime
import MullvadTypes

enum MullvadApiRequest {
case getAddressList
}

struct MullvadApiRequestFactory {
let apiContext: MullvadApiContext

func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler {
{ completion in
let pointerClass = MullvadApiCompletion { apiResponse in
try? completion?(apiResponse)
}

let rawPointer = Unmanaged.passRetained(pointerClass).toOpaque()

return switch request {
case .getAddressList:
MullvadApiCancellable(handle: mullvad_api_get_addresses(apiContext.context, rawPointer))
}
}
}
}

extension REST {
typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable
}
33 changes: 33 additions & 0 deletions ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@
//

import Foundation
import MullvadRustRuntime
import MullvadTypes
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]>
Expand Down Expand Up @@ -55,6 +62,32 @@ extension REST {
)
}

public func mullvadApiGetAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
) -> Cancellable {
let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList)

let responseHandler = rustResponseHandler(
decoding: [AnyIPEndpoint].self,
with: responseDecoder
)

let networkOperation = MullvadApiNetworkOperation(
name: "get-api-addrs",
dispatchQueue: dispatchQueue,
retryStrategy: retryStrategy,
requestHandler: requestHandler,
responseDecoder: responseDecoder,
responseHandler: responseHandler,
completionHandler: completionHandler
)

operationQueue.addOperation(networkOperation)

return networkOperation
}

public func getAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]>
Expand Down
8 changes: 8 additions & 0 deletions ios/MullvadREST/ApiHandlers/RESTDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import MullvadRustRuntime
import MullvadTypes

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

/// Default network timeout for API requests.
public static let defaultAPINetworkTimeout: Duration = .seconds(10)

/// API context used for API requests via Rust runtime.
// swiftlint:disable:next force_try
public static let apiContext = try! MullvadApiContext(
host: defaultAPIHostname,
address: defaultAPIEndpoint.description
)
}

// swiftlint:enable force_cast
15 changes: 12 additions & 3 deletions ios/MullvadREST/ApiHandlers/RESTProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import MullvadRustRuntime
import MullvadTypes
import Operations

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

let mullvadApiRequestFactory: MullvadApiRequestFactory

/// URL response decoder.
let responseDecoder: JSONDecoder

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

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

Expand Down Expand Up @@ -132,13 +136,16 @@ extension REST {
public class ProxyConfiguration: @unchecked Sendable {
public let transportProvider: RESTTransportProvider
public let addressCacheStore: AddressCache
public let apiContext: MullvadApiContext

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

Expand All @@ -147,13 +154,15 @@ extension REST {

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

super.init(
transportProvider: proxyConfiguration.transportProvider,
addressCacheStore: proxyConfiguration.addressCacheStore
addressCacheStore: proxyConfiguration.addressCacheStore,
apiContext: apiContext
)
}
}
Expand Down
14 changes: 10 additions & 4 deletions ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//

import Foundation
import MullvadRustRuntime

public protocol ProxyFactoryProtocol {
var configuration: REST.AuthProxyConfiguration { get }

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

static func makeProxyFactory(
transportProvider: RESTTransportProvider,
addressCache: REST.AddressCache
addressCache: REST.AddressCache,
apiContext: MullvadApiContext
) -> ProxyFactoryProtocol
}

Expand All @@ -26,11 +29,13 @@ extension REST {

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

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

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

return ProxyFactory(configuration: authConfiguration)
Expand Down
57 changes: 57 additions & 0 deletions ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import MullvadRustRuntime
import MullvadTypes

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

protocol RESTRustResponseHandler<Success> {
associatedtype Success

func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success>
}

extension REST {
// TODO: We could probably remove the `decoding` case when network requests are fully merged to Mullvad API.
/// Responser handler result type.
enum ResponseHandlerResult<Success> {
/// Response handler succeeded and produced a value.
Expand Down Expand Up @@ -66,4 +74,53 @@ extension REST {
}
}
}

final class RustResponseHandler<Success>: RESTRustResponseHandler {
typealias HandlerBlock = (MullvadApiResponse) -> REST.ResponseHandlerResult<Success>

private let handlerBlock: HandlerBlock

init(_ block: @escaping HandlerBlock) {
handlerBlock = block
}

func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> {
handlerBlock(response)
}
}

/// Returns default response handler that parses JSON response into the
/// given `Decodable` type if possible, otherwise attempts to decode
/// the server error.
static func rustResponseHandler<T: Decodable>(
decoding type: T.Type,
with decoder: JSONDecoder
) -> RustResponseHandler<T> {
RustResponseHandler { response in
guard let body = response.body else {
return .unhandledResponse(nil)
}

do {
let decoded = try decoder.decode(type, from: body)
return .decoding { decoded }
} catch {
return .unhandledResponse(
try? decoder.decode(
ServerErrorResponse.self,
from: body
)
)
}
}
}

/// Returns default response handler that parses JSON response into the
/// given `Decodable` type if possible, otherwise attempts to decode
/// the server error.
static func rustEmptyResponseHandler() -> RustResponseHandler<Void> {
RustResponseHandler { _ in
.success(())
}
}
}
Loading