summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-05-04 09:56:21 +0200
committerBug Magnet <marco.nikic@mullvad.net>2023-05-12 14:51:59 +0200
commita08ceebb971669bcd35992f744f98a4e0f5ecc05 (patch)
tree8bd5ca47f322fe90537ef42f3bdd2e71f73b67a4 /ios
parentbdd6590031354943f392326a1d951f26d42240b7 (diff)
downloadmullvadvpn-a08ceebb971669bcd35992f744f98a4e0f5ecc05.tar.xz
mullvadvpn-a08ceebb971669bcd35992f744f98a4e0f5ecc05.zip
Simplify the AddressCache logic, it now filters results to only keep the first one, does not rotate addresses anymore, and has tests written for.
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadREST/AddressCache.swift313
-rw-r--r--ios/MullvadREST/RESTResponseHandler.swift1
-rw-r--r--ios/MullvadRESTTests/AddressCacheTests.swift235
-rw-r--r--ios/MullvadTypes/NSFileCoordinator+Extensions.swift51
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj32
-rw-r--r--ios/MullvadVPN/AppDelegate.swift8
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift7
-rwxr-xr-xios/rest-prebuild.sh22
8 files changed, 378 insertions, 291 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift
index c18709f0ef..bc54f306b4 100644
--- a/ios/MullvadREST/AddressCache.swift
+++ b/ios/MullvadREST/AddressCache.swift
@@ -21,116 +21,89 @@ extension REST {
/// Cache file location.
private let cacheFileURL: URL
- /// The location of pre-bundled address cache file.
- private let prebundledCacheFileURL: URL
-
/// Lock used for synchronizing access to instance members.
- private let nslock = NSLock()
+ private let cacheLock = NSLock()
+
+ /// Whether address cache can be written to.
+ private let canWriteToCache: Bool
- /// Whether address cache is in readonly mode.
- private var isReadOnly: Bool
+ /// The name of the cache file on disk
+ internal static let cacheFileName = "api-ip-address.json"
+ /// The default set of endpoints to use as a fallback mechanism
private static let defaultCachedAddresses = CachedAddresses(
updatedAt: Date(timeIntervalSince1970: 0),
endpoints: [REST.defaultAPIEndpoint]
)
- /// Designated initializer.
- public init?(securityGroupIdentifier: String, isReadOnly: Bool) {
- let cacheFilename = "api-ip-address.json"
+ // MARK: -
- guard let containerURL = FileManager.default.containerURL(
- forSecurityApplicationGroupIdentifier: securityGroupIdentifier
- ), let prebundledCacheFileURL = Bundle(for: AddressCache.self).url(
- forResource: cacheFilename,
- withExtension: nil
- ) else { return nil }
+ // MARK: Public API
- let cacheFileURL = containerURL.appendingPathComponent(
- cacheFilename,
+ /// Designated initializer.
+ public init(canWriteToCache: Bool, cacheFolder: URL) {
+ let cacheFileURL = cacheFolder.appendingPathComponent(
+ Self.cacheFileName,
isDirectory: false
)
self.cacheFileURL = cacheFileURL
- self.prebundledCacheFileURL = prebundledCacheFileURL
- self.isReadOnly = isReadOnly
+ self.canWriteToCache = canWriteToCache
initCache()
}
+ /// Returns the latest available endpoint
+ ///
+ /// When running from the Network Extension, this method will read from the cache before returning.
+ /// - Returns: The latest available endpoint, or a default endpoint if no endpoints are available
public func getCurrentEndpoint() -> AnyIPEndpoint {
- nslock.lock()
- defer { nslock.unlock() }
- return cachedAddresses.endpoints.first!
- }
-
- public func selectNextEndpoint(_ failedEndpoint: AnyIPEndpoint) -> AnyIPEndpoint {
- nslock.lock()
- defer { nslock.unlock() }
-
- var currentEndpoint = cachedAddresses.endpoints.first!
-
- guard failedEndpoint == currentEndpoint else {
- return currentEndpoint
- }
-
- cachedAddresses.endpoints.removeFirst()
- cachedAddresses.endpoints.append(failedEndpoint)
+ cacheLock.lock()
+ defer { cacheLock.unlock() }
+ var currentEndpoint = cachedAddresses.endpoints.first ?? REST.defaultAPIEndpoint
- if isReadOnly {
- refreshAddresses()
- }
-
- currentEndpoint = cachedAddresses.endpoints.first!
-
- logger.debug(
- "Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)"
- )
-
- if !isReadOnly {
+ // Reload from disk cache when in the Network Extension as there is no `AddressCacheTracker` running
+ // there
+ if canWriteToCache == false {
do {
- try writeToDisk()
+ cachedAddresses = try readFromCache()
+ if let firstEndpoint = cachedAddresses.endpoints.first {
+ currentEndpoint = firstEndpoint
+ }
} catch {
- logger.error(
- error: error,
- message: "Failed to write address cache after selecting next endpoint."
- )
+ logger.error(error: error)
}
}
-
return currentEndpoint
}
- public func setEndpoints(_ endpoints: [AnyIPEndpoint]) {
- nslock.lock()
- defer { nslock.unlock() }
+ public func selectNextEndpoint(_ failedEndpoint: AnyIPEndpoint) -> AnyIPEndpoint {
+ // This function currently acts as a convoluted no-op. It will be soon deleted.
+ return getCurrentEndpoint()
+ }
- guard !endpoints.isEmpty else {
- return
- }
+ /// Updates the available endpoints to use
+ ///
+ /// Only the first available endpoint is kept, the rest are discarded.
+ /// This method will only modify the on disk cache when running from the UI process.
+ /// - Parameter endpoints: The new endpoints to use for API requests
+ public func setEndpoints(_ endpoints: [AnyIPEndpoint]) {
+ cacheLock.lock()
+ defer { cacheLock.unlock() }
+ guard let firstEndpoint = endpoints.first else { return }
if Set(cachedAddresses.endpoints) == Set(endpoints) {
cachedAddresses.updatedAt = Date()
} else {
- // Shuffle new endpoints
- var newEndpoints = endpoints.shuffled()
-
- // Move current endpoint to the top of the list
- let currentEndpoint = cachedAddresses.endpoints.first!
- if let index = newEndpoints.firstIndex(of: currentEndpoint) {
- newEndpoints.remove(at: index)
- newEndpoints.insert(currentEndpoint, at: 0)
- }
-
cachedAddresses = CachedAddresses(
updatedAt: Date(),
- endpoints: newEndpoints
+ endpoints: [firstEndpoint]
)
}
- if !isReadOnly {
+ if canWriteToCache {
do {
- try writeToDisk()
+ try writeToCache()
} catch {
logger.error(
error: error,
@@ -140,175 +113,61 @@ extension REST {
}
}
+ /// The `Date` when the cache was last updated at
+ ///
+ /// - Returns: The `Date` when the cache was last updated at
public func getLastUpdateDate() -> Date {
- nslock.lock()
- defer { nslock.unlock() }
+ cacheLock.lock()
+ defer { cacheLock.unlock() }
return cachedAddresses.updatedAt
}
- // MARK: - Private
+ // MARK: - Private API
+ /// Initializes the cache by reading the a cached file from disk
+ ///
+ /// If no cache file is present, a default API endpoint will be selected instead
private func initCache() {
+ // The first time the application is ran, this statement will fail as there is no cache. This is fine.
+ // The cache will be filled when either `getCurrentEndpoint` or `setEndpoints()` are called.
do {
- try initCacheInner()
+ cachedAddresses = try readFromCache()
} catch {
logger.debug("Initialized cache with default API endpoint.")
-
cachedAddresses = Self.defaultCachedAddresses
}
}
- private func initCacheInner() throws {
- let readResult = try readFromCacheLocationWithFallback()
-
- switch readResult.source {
- case .disk:
- cachedAddresses = readResult.cachedAddresses
-
- case .bundle:
- var addresses = readResult.cachedAddresses
- addresses.endpoints.shuffle()
- cachedAddresses = addresses
-
- if !isReadOnly {
- logger.debug("Persist address list read from bundle.")
-
- do {
- try writeToDisk()
- } catch {
- logger.error(
- error: error,
- message: "Failed to persist address cache after reading it from bundle."
- )
- }
- }
- }
-
- logger.debug(
- """
- Initialized cache from \(readResult.source) with \
- \(cachedAddresses.endpoints.count) endpoint(s).
- """
- )
- }
-
- private func readFromCacheLocationWithFallback() throws -> ReadResult {
- do {
- return try readFromCacheLocation()
- } catch {
- logger.error(
- error: error,
- message: "Failed to read address cache from disk. Fallback to pre-bundled cache."
- )
-
- do {
- return try readFromBundle()
- } catch {
- logger.error(
- error: error,
- message: "Failed to read address cache from bundle."
- )
-
- throw error
- }
- }
- }
-
- private func readFromCacheLocation() throws -> ReadResult {
- var result: Result<ReadResult, Swift.Error>?
+ /// Reads the cache file from disk
+ ///
+ /// - Returns: A list of cached API endpoints in a `CachedAddresses` form
+ private func readFromCache() throws -> CachedAddresses {
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
- let accessor = { (fileURL: URL) in
- result = Result {
- let data = try Data(contentsOf: fileURL)
+ let result = try fileCoordinator
+ .coordinate(readingItemAt: cacheFileURL, options: [.withoutChanges]) { file in
+ let data = try Data(contentsOf: file)
let cachedAddresses = try JSONDecoder().decode(CachedAddresses.self, from: data)
if cachedAddresses.endpoints.isEmpty {
- throw EmptyCacheError(source: .disk)
+ throw EmptyCacheError()
}
- return ReadResult(cachedAddresses: cachedAddresses, source: .disk)
+ return cachedAddresses
}
- }
-
- var error: NSError?
- fileCoordinator.coordinate(
- readingItemAt: cacheFileURL,
- options: .withoutChanges,
- error: &error,
- byAccessor: accessor
- )
- if let error = error {
- result = .failure(error)
- }
-
- return try result!.get()
+ return result
}
- private func readFromBundle() throws -> ReadResult {
- let data = try Data(contentsOf: prebundledCacheFileURL)
- let endpoints = try JSONDecoder().decode([AnyIPEndpoint].self, from: data)
-
- let cachedAddresses = CachedAddresses(
- updatedAt: Date(timeIntervalSince1970: 0),
- endpoints: endpoints
- )
-
- if cachedAddresses.endpoints.isEmpty {
- throw EmptyCacheError(source: .bundle)
- }
-
- return ReadResult(cachedAddresses: cachedAddresses, source: .bundle)
- }
-
- private func writeToDisk() throws {
- precondition(!isReadOnly)
-
- var result: Result<Void, Swift.Error>?
+ /// Writes the cache file to the disk
+ private func writeToCache() throws {
+ precondition(canWriteToCache == true)
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
- let accessor = { (fileURL: URL) in
- result = Result {
- let data = try JSONEncoder().encode(self.cachedAddresses)
- try data.write(to: fileURL)
- }
- }
-
- var error: NSError?
- fileCoordinator.coordinate(
- writingItemAt: cacheFileURL,
- options: [.forReplacing],
- error: &error,
- byAccessor: accessor
- )
-
- if let error = error {
- result = .failure(error)
- }
-
- return try result!.get()
- }
-
- private func refreshAddresses() {
- do {
- let readResult = try readFromCacheLocation()
- var newCachedAddresses = readResult.cachedAddresses
-
- guard Set(newCachedAddresses.endpoints) != Set(cachedAddresses.endpoints)
- else { return }
-
- // Move current endpoint to the top of the list
- let currentEndpoint = cachedAddresses.endpoints.first!
- if let index = newCachedAddresses.endpoints.firstIndex(of: currentEndpoint) {
- newCachedAddresses.endpoints.remove(at: index)
- newCachedAddresses.endpoints.insert(currentEndpoint, at: 0)
- }
-
- cachedAddresses = newCachedAddresses
- } catch {
- logger.error(error: error, message: "Failed to refresh address cache from disk.")
+ try fileCoordinator.coordinate(writingItemAt: cacheFileURL, options: [.forReplacing]) { file in
+ let data = try JSONEncoder().encode(self.cachedAddresses)
+ try data.write(to: file)
}
}
}
@@ -321,33 +180,9 @@ extension REST {
var endpoints: [AnyIPEndpoint]
}
- enum CacheSource: CustomStringConvertible {
- /// Cache file originates from disk location.
- case disk
-
- /// Cache file originates from application bundle.
- case bundle
-
- var description: String {
- switch self {
- case .disk:
- return "disk"
- case .bundle:
- return "bundle"
- }
- }
- }
-
- struct ReadResult {
- var cachedAddresses: CachedAddresses
- var source: CacheSource
- }
-
struct EmptyCacheError: LocalizedError {
- let source: CacheSource
-
var errorDescription: String? {
- return "Address cache file from \(source) does not contain any API addresses."
+ return "Address cache file does not contain any API addresses."
}
}
}
diff --git a/ios/MullvadREST/RESTResponseHandler.swift b/ios/MullvadREST/RESTResponseHandler.swift
index 1a33931f31..fd3b7d2e2b 100644
--- a/ios/MullvadREST/RESTResponseHandler.swift
+++ b/ios/MullvadREST/RESTResponseHandler.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadTypes
protocol RESTResponseHandler {
associatedtype Success
diff --git a/ios/MullvadRESTTests/AddressCacheTests.swift b/ios/MullvadRESTTests/AddressCacheTests.swift
new file mode 100644
index 0000000000..85387adee7
--- /dev/null
+++ b/ios/MullvadRESTTests/AddressCacheTests.swift
@@ -0,0 +1,235 @@
+//
+// AddressCacheTests.swift
+// MullvadRESTTests
+//
+// Created by Marco Nikic on 2023-05-05.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadREST
+import MullvadTypes
+import XCTest
+
+final class AddressCacheTests: XCTestCase {
+ static var testsCacheDirectory: URL!
+ var apiEndpoint: AnyIPEndpoint!
+ var cacheFilePresenter: AddressCacheFilePresenter!
+ let defaultExpectationTimeout = REST.Duration.milliseconds(200).timeInterval
+
+ // MARK: Tests Setup
+
+ override class func setUp() {
+ super.setUp()
+ let temporaryDirectory = FileManager.default.temporaryDirectory
+ testsCacheDirectory = temporaryDirectory.appendingPathComponent("AddressCacheTests")
+ }
+
+ override func setUpWithError() throws {
+ try super.setUpWithError()
+ apiEndpoint = try XCTUnwrap(AnyIPEndpoint(string: "127.0.0.1:80"))
+ let cacheFileURL = Self.testsCacheDirectory.appendingPathComponent(REST.AddressCache.cacheFileName)
+ cacheFilePresenter = AddressCacheFilePresenter(presentedItemURL: cacheFileURL)
+ NSFileCoordinator.addFilePresenter(cacheFilePresenter)
+ }
+
+ override func tearDownWithError() throws {
+ NSFileCoordinator.removeFilePresenter(cacheFilePresenter)
+ try super.tearDownWithError()
+ }
+
+ // MARK: -
+
+ // MARK: Tests
+
+ func testAddressCacheHasDefaultEndpoint() {
+ let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory)
+ XCTAssertEqual(cache.getCurrentEndpoint(), REST.defaultAPIEndpoint)
+ }
+
+ func testSetEndpoints() throws {
+ let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory)
+
+ cache.setEndpoints([apiEndpoint])
+ XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint)
+ }
+
+ func testSetEndpointsUpdatesDateWhenSettingSameAddress() throws {
+ let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory)
+ cache.setEndpoints([apiEndpoint])
+
+ let dateBeforeSettingEndpoint = Date()
+ cache.setEndpoints([apiEndpoint])
+ let dateAfterSettingEndpoint = Date()
+
+ let dateIntervalRange = dateBeforeSettingEndpoint ... dateAfterSettingEndpoint
+ XCTAssertTrue(dateIntervalRange.contains(cache.getLastUpdateDate()))
+ }
+
+ func testSetEndpointsDoesNotDoAnythingIfSettingEmptyEndpoints() throws {
+ let didNotWriteToCache = expectation(description: "Did not write to cache")
+ didNotWriteToCache.isInverted = true
+
+ cacheFilePresenter.onWriterAction = {
+ didNotWriteToCache.fulfill()
+ }
+
+ try withCachefolders { cacheDirectory, _ in
+ let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory)
+ cache.setEndpoints([])
+ }
+
+ waitForExpectations(timeout: defaultExpectationTimeout)
+ }
+
+ func testSetEndpointsOnlyAcceptsTheFirstEndpoint() throws {
+ let ipAddresses = (1 ... 10)
+ .map { "\($0).\($0).\($0).\($0):80" }
+ .compactMap { AnyIPEndpoint(string: $0) }
+
+ let firstIPEndpoint = try XCTUnwrap(ipAddresses.first)
+
+ try withCachefolders { cacheDirectory, cacheFileURL in
+ let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory)
+ cache.setEndpoints(ipAddresses)
+
+ let cachedContent = try Data(contentsOf: cacheFileURL)
+ let cachedAddresses = try JSONDecoder().decode(REST.CachedAddresses.self, from: cachedContent)
+
+ XCTAssertEqual(cachedAddresses.endpoints.count, 1)
+ XCTAssertEqual(cache.getCurrentEndpoint(), firstIPEndpoint)
+ }
+ }
+
+ func testCacheReadsFromCachedFileAtInit() throws {
+ let didReadFromCache = expectation(description: "Cache was read")
+ cacheFilePresenter.onReaderAction = {
+ didReadFromCache.fulfill()
+ }
+
+ try withCachefolders { cacheDirectory, cacheFileURL in
+ let fixedDate = Date()
+ try prepopulateCache(at: cacheFileURL, fixedDate: fixedDate, with: [apiEndpoint])
+ let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory)
+
+ XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint)
+ XCTAssertEqual(cache.getLastUpdateDate(), fixedDate)
+ }
+
+ waitForExpectations(timeout: defaultExpectationTimeout)
+ }
+
+ func testCacheWritesToDiskWhenSettingNewEndpoints() throws {
+ let didWriteToCache = expectation(description: "Cache was written to")
+ cacheFilePresenter.onWriterAction = {
+ didWriteToCache.fulfill()
+ }
+
+ try withCachefolders { cacheDirectory, cacheFileURL in
+
+ let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory)
+ cache.setEndpoints([apiEndpoint])
+ let cachedContent = try Data(contentsOf: cacheFileURL)
+ let cachedAddresses = try JSONDecoder().decode(REST.CachedAddresses.self, from: cachedContent)
+ let cachedAddress = try XCTUnwrap(cachedAddresses.endpoints.first)
+
+ XCTAssertEqual(cachedAddress, cache.getCurrentEndpoint())
+ XCTAssertEqual(cachedAddresses.updatedAt, cache.getLastUpdateDate())
+ }
+
+ waitForExpectations(timeout: defaultExpectationTimeout)
+ }
+
+ func testGetCurrentEndpointReadsFromCacheWhenReadOnly() throws {
+ let didReadFromCache = expectation(description: "Cache was read")
+ // Cache will be read from twice. Once during init, once when getting current endpoint
+ didReadFromCache.expectedFulfillmentCount = 2
+ cacheFilePresenter.onReaderAction = {
+ didReadFromCache.fulfill()
+ }
+
+ try withCachefolders { cacheDirectory, cacheFileURL in
+ let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: cacheDirectory)
+ try prepopulateCache(at: cacheFileURL, with: [apiEndpoint])
+
+ XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint)
+ }
+
+ waitForExpectations(timeout: defaultExpectationTimeout)
+ }
+
+ func testGetCurrentEndpointHasDefaultEndpointIfCacheIsEmpty() throws {
+ let didReadFromCache = expectation(description: "Cache was read")
+ // Cache will be read from twice. Once during init, once when getting current endpoint
+ didReadFromCache.expectedFulfillmentCount = 2
+ cacheFilePresenter.onReaderAction = {
+ didReadFromCache.fulfill()
+ }
+
+ try withCachefolders { cacheDirectory, cacheFileURL in
+ try prepopulateCache(at: cacheFileURL, with: [])
+
+ let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: cacheDirectory)
+ XCTAssertEqual(cache.getCurrentEndpoint(), REST.defaultAPIEndpoint)
+ }
+
+ waitForExpectations(timeout: defaultExpectationTimeout)
+ }
+}
+
+// MARK: -
+
+extension AddressCacheTests {
+ /// Prepares a cache folder that is expected to be present during the `runTest` closure
+ /// - Parameter runTest: A closure that expects a `cacheDirectory` encapsulating `cacheFileURL` to be present when
+ /// it runs
+ func withCachefolders(_ runTest: (_ cacheDirectory: URL, _ cacheFileURL: URL) throws -> Void) throws {
+ let cacheFileURL = try XCTUnwrap(cacheFilePresenter.presentedItemURL)
+ let fileManager = FileManager.default
+ let cacheDirectory = try XCTUnwrap(Self.testsCacheDirectory)
+ try fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
+
+ try runTest(cacheDirectory, cacheFileURL)
+
+ try fileManager.removeItem(at: cacheDirectory)
+ }
+
+ /// Populates a JSON cache file containing a `Date` and `[AnyIPEndpoint]`
+ ///
+ /// - Parameters:
+ /// - cacheFileURL: The cache file destination
+ /// - fixedDate: The `Date` the cache file was written to
+ /// - endpoints: A list of `AnyIPEndpoint` to write in the cache
+ func prepopulateCache(at cacheFileURL: URL, fixedDate: Date = Date(), with endpoints: [AnyIPEndpoint]) throws {
+ let prepopulatedCache = REST.CachedAddresses(updatedAt: fixedDate, endpoints: endpoints)
+ let encodedCache = try JSONEncoder().encode(prepopulatedCache)
+ try encodedCache.write(to: cacheFileURL)
+ }
+}
+
+class AddressCacheFilePresenter: NSObject, NSFilePresenter {
+ var presentedItemURL: URL?
+ let operationQueue: OperationQueue
+ let dispatchQueue = DispatchQueue(label: "com.MullvadVPN.AddressCacheTests")
+ var presentedItemOperationQueue: OperationQueue { operationQueue }
+
+ var onReaderAction: (() -> Void)?
+ var onWriterAction: (() -> Void)?
+
+ init(presentedItemURL: URL) {
+ operationQueue = OperationQueue()
+ self.presentedItemURL = presentedItemURL
+ operationQueue.underlyingQueue = dispatchQueue
+ }
+
+ func relinquishPresentedItem(toReader reader: @escaping ((() -> Void)?) -> Void) {
+ print(#function)
+ onReaderAction?()
+ reader(nil)
+ }
+
+ func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) {
+ print(#function)
+ onWriterAction?()
+ writer(nil)
+ }
+}
diff --git a/ios/MullvadTypes/NSFileCoordinator+Extensions.swift b/ios/MullvadTypes/NSFileCoordinator+Extensions.swift
new file mode 100644
index 0000000000..242381ccb4
--- /dev/null
+++ b/ios/MullvadTypes/NSFileCoordinator+Extensions.swift
@@ -0,0 +1,51 @@
+//
+// NSFileCoordinator+Extensions.swift
+// MullvadTypes
+//
+// Created by Marco Nikic on 2023-05-11.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension NSFileCoordinator {
+ public func coordinate<R>(
+ readingItemAt itemURL: URL,
+ options: ReadingOptions = [],
+ accessor: (URL) throws -> R
+ ) throws -> R {
+ var error: NSError?
+ var result: Result<R, Error> = .failure(CocoaError(.fileReadUnknown))
+
+ coordinate(readingItemAt: itemURL, options: options, error: &error) { url in
+ result = Result { try accessor(url) }
+ }
+
+ if let error {
+ throw error
+ }
+
+ return try result.get()
+ }
+
+ public func coordinate(
+ writingItemAt itemURL: URL,
+ options: WritingOptions = [],
+ accessor: (URL) throws -> Void
+ ) throws {
+ var error: NSError?
+ var accessorError: Error?
+
+ coordinate(writingItemAt: itemURL, options: options, error: &error) { url in
+ do {
+ try accessor(url)
+ } catch {
+ accessorError = error
+ }
+ }
+
+ if let e = error ?? accessorError {
+ throw e
+ }
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index c687145711..3bab2cdcf9 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -10,7 +10,6 @@
062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; };
062B45AE28FD503000746E77 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 062B45AD28FD503000746E77 /* WireGuardKit */; };
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; };
- 062B45C228FE980000746E77 /* api-ip-address.json in Resources */ = {isa = PBXBuildFile; fileRef = 062B45C128FE97FF00746E77 /* api-ip-address.json */; };
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */; };
063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
063F02762902B63F001FA09F /* RelayCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 063F02752902B63F001FA09F /* RelayCache.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -368,6 +367,8 @@
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; };
7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */; };
+ A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; };
+ A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -631,7 +632,6 @@
/* Begin PBXFileReference section */
062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDefaults.swift; sourceTree = "<group>"; };
- 062B45C128FE97FF00746E77 /* api-ip-address.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "api-ip-address.json"; sourceTree = "<group>"; };
063687AF28EB083800BE7161 /* ProxyURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLRequest.swift; sourceTree = "<group>"; };
063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelTransport.swift; sourceTree = "<group>"; };
063F02732902B63F001FA09F /* RelayCache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RelayCache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -969,6 +969,8 @@
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; };
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; };
+ A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; };
+ A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
@@ -1104,7 +1106,6 @@
062B45A228FD4C0F00746E77 /* Assets */ = {
isa = PBXGroup;
children = (
- 062B45C128FE97FF00746E77 /* api-ip-address.json */,
06799AB428F98CE700ACD94E /* le_root_cert.cer */,
);
path = Assets;
@@ -1208,6 +1209,7 @@
58A1AA8623F43901009F7EA6 /* Location.swift */,
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */,
58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */,
+ A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */,
06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */,
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */,
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */,
@@ -1935,6 +1937,7 @@
58FBFBE7291622580020E046 /* MullvadRESTTests */ = {
isa = PBXGroup;
children = (
+ A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */,
58FBFBF0291630700020E046 /* DurationTests.swift */,
58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */,
);
@@ -2031,7 +2034,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 06799AD328F98E1D00ACD94E /* Build configuration list for PBXNativeTarget "MullvadREST" */;
buildPhases = (
- 588E4EB028FEF1CA008046E3 /* Run prebuild script */,
06799AB728F98E1D00ACD94E /* Headers */,
06799AB828F98E1D00ACD94E /* Sources */,
06799AB928F98E1D00ACD94E /* Frameworks */,
@@ -2400,7 +2402,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 062B45C228FE980000746E77 /* api-ip-address.json in Resources */,
062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2495,25 +2496,6 @@
shellPath = /bin/sh;
shellScript = "exec > $PROJECT_DIR/relays-prebuild.log 2>&1\n\n$PROJECT_DIR/relays-prebuild.sh\n";
};
- 588E4EB028FEF1CA008046E3 /* Run prebuild script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- name = "Run prebuild script";
- outputFileListPaths = (
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "exec > $PROJECT_DIR/rest-prebuild.log 2>&1\n\n$PROJECT_DIR/rest-prebuild.sh\n";
- };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -2881,6 +2863,7 @@
58D22406294C90210029F5F8 /* IPv4Endpoint.swift in Sources */,
58D22407294C90210029F5F8 /* IPv6Endpoint.swift in Sources */,
58CAFA032985367600BE19F7 /* Promise.swift in Sources */,
+ A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */,
58D22408294C90210029F5F8 /* AnyIPEndpoint.swift in Sources */,
58D22409294C90210029F5F8 /* AnyIPAddress.swift in Sources */,
58D2240A294C90210029F5F8 /* IPAddress+Codable.swift in Sources */,
@@ -2922,6 +2905,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */,
58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */,
58FBFBF1291630700020E046 /* DurationTests.swift in Sources */,
);
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 748742ee76..26ac51cccf 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -51,10 +51,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
logger = Logger(label: "AppDelegate")
+ let containerURL = FileManager.default
+ .containerURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
+
addressCache = REST.AddressCache(
- securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier,
- isReadOnly: false
- )!
+ canWriteToCache: true, cacheFolder: containerURL
+ )
proxyFactory = REST.ProxyFactory.makeProxyFactory(
transportProvider: { [weak self] in
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index 52220cdf49..4a0a341aba 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -144,10 +144,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
providerLogger = Logger(label: "PacketTunnelProvider")
tunnelLogger = Logger(label: "WireGuard")
+ let containerURL = FileManager.default
+ .containerURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
let addressCache = REST.AddressCache(
- securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier,
- isReadOnly: true
- )!
+ canWriteToCache: false, cacheFolder: containerURL
+ )
let urlSession = REST.makeURLSession()
let urlSessionTransport = REST.URLSessionTransport(urlSession: urlSession)
diff --git a/ios/rest-prebuild.sh b/ios/rest-prebuild.sh
deleted file mode 100755
index 59fde4aa56..0000000000
--- a/ios/rest-prebuild.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-
-if [ -z "$PROJECT_DIR" ]; then
- echo "This script is intended to be executed by Xcode"
- exit 1
-fi
-
-API_IP_ADDRESS_LIST_FILE="$PROJECT_DIR/MullvadREST/Assets/api-ip-address.json"
-
-if [ $CONFIGURATION == "Release" ]; then
- echo "Remove API address list file"
- if [ -f "$API_IP_ADDRESS_LIST_FILE" ]; then
- rm "$API_IP_ADDRESS_LIST_FILE"
- else
- echo "API IP address list file does not exist"
- fi
-fi
-
-if [ ! -f "$API_IP_ADDRESS_LIST_FILE" ]; then
- echo "Download API address list"
- curl https://api.mullvad.net/app/v1/api-addrs -s -o "$API_IP_ADDRESS_LIST_FILE"
-fi