summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST
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/MullvadREST
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/MullvadREST')
-rw-r--r--ios/MullvadREST/AddressCache.swift313
-rw-r--r--ios/MullvadREST/RESTResponseHandler.swift1
2 files changed, 75 insertions, 239 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