Skip to content

Commit

Permalink
Enable testing API access with a given configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Jan 24, 2024
1 parent f4ec9f8 commit a34caac
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 31 deletions.
55 changes: 55 additions & 0 deletions ios/MullvadREST/ApiHandlers/APIAvailabilityTestRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// APIAvailabilityTestRequest.swift
// MullvadREST
//
// Created by Marco Nikic on 2024-01-08.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadTypes

extension REST {
public struct APIAvailabilityTestRequest {
let transport: RESTTransport

public init(transport: RESTTransport) {
self.transport = transport
}

/// Executes an HTTP `HEAD` request to the "api-addrs" endpoint.
///
/// - Parameter completion: Completes with `nil` if the request was successful, and `Error` otherwise.
/// - Returns: A cancellable token to cancel the request inflight.
public func makeRequest(completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
do {
let factory = RequestFactory(
hostname: defaultAPIHostname,
pathPrefix: "/app/v1",
networkTimeout: defaultAPINetworkTimeout,
bodyEncoder: JSONEncoder()
)
var request = try factory.createRequest(
endpoint: defaultAPIEndpoint,
method: .head,
pathTemplate: "api-addrs"
)
request.urlRequest.cachePolicy = .reloadIgnoringLocalCacheData

return transport.sendRequest(request.urlRequest) { _, response, error in
// Any response in the form of `HTTPURLResponse` means that the API was reached successfully
// and implying an HTTP server is running, therefore the test is considered successful.
guard response is HTTPURLResponse else {
completion(error)
return
}
completion(nil)
}

} catch {
completion(error)
}
return AnyCancellable()
}
}
}
1 change: 1 addition & 0 deletions ios/MullvadREST/ApiHandlers/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct HTTPMethod: RawRepresentable {
static let post = HTTPMethod(rawValue: "POST")
static let delete = HTTPMethod(rawValue: "DELETE")
static let put = HTTPMethod(rawValue: "PUT")
static let head = HTTPMethod(rawValue: "HEAD")

let rawValue: String
init(rawValue: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// ProxyConfigurationTransportProvider.swift
// MullvadREST
//
// Created by Marco Nikic on 2024-01-19.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings
import MullvadTypes

/// Allows the creation of `RESTTransport` objects that bypass the network routing logic provided by `TransportProvider`.
public class ProxyConfigurationTransportProvider {
private let shadowsocksLoader: ShadowsocksLoaderProtocol
private let addressCache: REST.AddressCache

public init(shadowsocksLoader: ShadowsocksLoaderProtocol, addressCache: REST.AddressCache) {
self.shadowsocksLoader = shadowsocksLoader
self.addressCache = addressCache
}

public func makeTransport(with configuration: PersistentProxyConfiguration) throws -> RESTTransport {
let urlSession = REST.makeURLSession()
switch configuration {
case .direct:
return URLSessionTransport(urlSession: urlSession)
case .bridges:
let shadowsocksConfiguration = try shadowsocksLoader.load()
return ShadowsocksTransport(
urlSession: urlSession,
configuration: shadowsocksConfiguration,
addressCache: addressCache
)
case let .shadowsocks(shadowSocksConfiguration):
return ShadowsocksTransport(
urlSession: urlSession,
configuration: ShadowsocksConfiguration(
address: shadowSocksConfiguration.server,
port: shadowSocksConfiguration.port,
password: shadowSocksConfiguration.password,
cipher: shadowSocksConfiguration.cipher.rawValue.description
),
addressCache: addressCache
)
case let .socks5(socksConfiguration):
return URLSessionSocks5Transport(
urlSession: urlSession,
configuration: Socks5Configuration(
proxyEndpoint: socksConfiguration.toAnyIPEndpoint,
username: socksConfiguration.credential?.username,
password: socksConfiguration.credential?.password
),
addressCache: addressCache
)
}
}
}
1 change: 1 addition & 0 deletions ios/MullvadREST/Transport/RESTTransportProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import MullvadSettings

public protocol RESTTransportProvider {
func makeTransport() -> RESTTransport?
Expand Down
40 changes: 40 additions & 0 deletions ios/MullvadRESTTests/HeadRequestTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// HeadRequestTests.swift
// MullvadRESTTests
//
// Created by Marco Nikic on 2024-01-22.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

@testable import MullvadREST
import XCTest

class HeadRequestTests: XCTestCase {
func testSuccessfulRequestHasNoError() throws {
let transport = RESTTransportStub(data: Data(), response: HTTPURLResponse())
let request = REST.APIAvailabilityTestRequest(transport: transport)

let successfulRequestExpectation = expectation(description: "HEAD request completed")
_ = request.makeRequest { error in
if error == nil {
successfulRequestExpectation.fulfill()
}
}

wait(for: [successfulRequestExpectation], timeout: 1)
}

func testRequestWithErrors() throws {
let transport = RESTTransportStub(error: URLError(.timedOut))
let request = REST.APIAvailabilityTestRequest(transport: transport)

let failedRequestExpectation = expectation(description: "HEAD request failed")
_ = request.makeRequest { error in
if error != nil {
failedRequestExpectation.fulfill()
}
}

wait(for: [failedRequestExpectation], timeout: 1)
}
}
27 changes: 27 additions & 0 deletions ios/MullvadRESTTests/Mocks/RESTTransportStub.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// RESTTransportStub.swift
// MullvadRESTTests
//
// Created by Marco Nikic on 2024-01-22.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

@testable import MullvadREST
@testable import MullvadTypes
import XCTest

struct RESTTransportStub: RESTTransport {
let name = "transport-stub"

var data: Data?
var response: URLResponse?
var error: Error?

func sendRequest(
_ request: URLRequest,
completion: @escaping (Data?, URLResponse?, Error?) -> Void
) -> Cancellable {
completion(data, response, error)
return AnyCancellable()
}
}
6 changes: 3 additions & 3 deletions ios/MullvadRESTTests/RequestExecutorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//

@testable import MullvadREST
import MullvadTypes
@testable import MullvadTypes
import XCTest

final class RequestExecutorTests: XCTestCase {
Expand All @@ -18,8 +18,8 @@ final class RequestExecutorTests: XCTestCase {
super.setUp()

let transportProvider = REST.AnyTransportProvider {
return AnyTransport {
return Response(delay: 1, statusCode: 200, value: TimeResponse(dateTime: Date()))
AnyTransport {
Response(delay: 1, statusCode: 200, value: TimeResponse(dateTime: Date()))
}
}

Expand Down
16 changes: 16 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; };
A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */; };
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F22B5EB61100999395 /* HeadRequestTests.swift */; };
A932D9F52B5EBB9D00999395 /* RESTTransportStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */; };
A935594C2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */; };
A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; };
A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; };
A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */; };
Expand Down Expand Up @@ -1773,6 +1777,10 @@
A92ECC232A7802520052F1B1 /* StoredAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAccountData.swift; sourceTree = "<group>"; };
A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredDeviceData.swift; sourceTree = "<group>"; };
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = "<group>"; };
A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationTransportProvider.swift; sourceTree = "<group>"; };
A932D9F22B5EB61100999395 /* HeadRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadRequestTests.swift; sourceTree = "<group>"; };
A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStub.swift; sourceTree = "<group>"; };
A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAvailabilityTestRequest.swift; sourceTree = "<group>"; };
A935594D2B4E919F00D5D524 /* Api.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Api.xcconfig; sourceTree = "<group>"; };
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; };
A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5UsernamePasswordCommand.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2834,6 +2842,7 @@
children = (
58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */,
58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */,
A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */,
58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */,
);
path = Mocks;
Expand Down Expand Up @@ -3248,6 +3257,7 @@
children = (
F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */,
58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */,
A932D9F22B5EB61100999395 /* HeadRequestTests.swift */,
58BDEB9E2A98F6B400F578F2 /* Mocks */,
58B4656F2A98C53300467203 /* RequestExecutorTests.swift */,
F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */,
Expand Down Expand Up @@ -3409,6 +3419,7 @@
isa = PBXGroup;
children = (
06AC114128F8413A0037AF9A /* AddressCache.swift */,
A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */,
06FAE67128F83CA40033DD93 /* HTTP.swift */,
06FAE67228F83CA40033DD93 /* RESTAccessTokenManager.swift */,
06FAE66828F83CA30033DD93 /* RESTAccountsProxy.swift */,
Expand Down Expand Up @@ -3459,6 +3470,7 @@
isa = PBXGroup;
children = (
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */,
A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */,
F0DC77A32B2315800087F09D /* Direct */,
F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */,
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */,
Expand Down Expand Up @@ -4391,6 +4403,7 @@
files = (
F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */,
F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */,
A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */,
06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */,
A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */,
A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */,
Expand Down Expand Up @@ -4449,6 +4462,7 @@
06799ADF28F98E4800ACD94E /* RESTDevicesProxy.swift in Sources */,
06799ADA28F98E4800ACD94E /* RESTResponseHandler.swift in Sources */,
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */,
A935594C2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift in Sources */,
A90763C32B2858630045ADF0 /* Socks5Configuration.swift in Sources */,
06799AE428F98E4800ACD94E /* RESTAccountsProxy.swift in Sources */,
5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */,
Expand Down Expand Up @@ -5185,7 +5199,9 @@
F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */,
58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */,
58BDEB9B2A98F58600F578F2 /* TimeServerProxy.swift in Sources */,
A932D9F52B5EBB9D00999395 /* RESTTransportStub.swift in Sources */,
58BDEB992A98F4ED00F578F2 /* AnyTransport.swift in Sources */,
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,38 @@

import Combine
import Foundation
import MullvadREST
import MullvadSettings
import MullvadTypes

/// A concrete implementation of an access method proxy configuration.
class ProxyConfigurationTester: ProxyConfigurationTesterProtocol {
private var cancellable: Cancellable?
private var cancellable: MullvadTypes.Cancellable?
private let transportProvider: ProxyConfigurationTransportProvider
private var headRequest: REST.APIAvailabilityTestRequest?

static let shared = ProxyConfigurationTester()

init() {}
init(transportProvider: ProxyConfigurationTransportProvider) {
self.transportProvider = transportProvider
}

func start(configuration: PersistentProxyConfiguration, completion: @escaping (Error?) -> Void) {
let workItem = DispatchWorkItem {
let randomResult = (0 ... 255).randomElement()?.isMultiple(of: 2) ?? true

completion(randomResult ? nil : URLError(.timedOut))
}

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: workItem)

cancellable = AnyCancellable {
workItem.cancel()
do {
let transport = try transportProvider.makeTransport(with: configuration)
let request = REST.APIAvailabilityTestRequest(transport: transport)
headRequest = request
cancellable = request.makeRequest { error in
DispatchQueue.main.async {
completion(error)
}
}
} catch {
completion(error)
}
}

func cancel() {
cancellable?.cancel()
cancellable = nil
headRequest = nil
}
}
19 changes: 14 additions & 5 deletions ios/MullvadVPN/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private let migrationManager = MigrationManager()

private(set) var accessMethodRepository = AccessMethodRepository()
private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol!
private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider!

// MARK: - Application lifecycle

Expand Down Expand Up @@ -94,14 +96,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD

// This init cannot fail as long as the security group identifier is valid
let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
shadowsocksLoader = ShadowsocksLoader(
shadowsocksCache: shadowsocksCache,
relayCache: relayCache,
constraintsUpdater: constraintsUpdater
)

configuredTransportProvider = ProxyConfigurationTransportProvider(
shadowsocksLoader: shadowsocksLoader,
addressCache: addressCache
)

let transportStrategy = TransportStrategy(
sharedUserDefaults,
datasource: accessMethodRepository,
shadowsocksLoader: ShadowsocksLoader(
shadowsocksCache: shadowsocksCache,
relayCache: relayCache,
constraintsUpdater: constraintsUpdater
)
shadowsocksLoader: shadowsocksLoader
)

let transportProvider = TransportProvider(
Expand Down
Loading

0 comments on commit a34caac

Please sign in to comment.