diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-10-24 18:03:15 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-10-31 13:32:35 +0100 |
| commit | 54cd11a3274784ca103ffd3e05e33093393013e9 (patch) | |
| tree | 43e53f77ed516f9e3ed5a784997e99c0b8beb61c | |
| parent | 87ef07178d9b164bec923d95e80d73d39ec3331b (diff) | |
| download | mullvadvpn-54cd11a3274784ca103ffd3e05e33093393013e9.tar.xz mullvadvpn-54cd11a3274784ca103ffd3e05e33093393013e9.zip | |
AddressCache: add read-only mode
| -rw-r--r-- | ios/MullvadREST/AddressCache.swift | 298 |
1 files changed, 173 insertions, 125 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index 235b02aee2..c18709f0ef 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -12,41 +12,11 @@ import MullvadTypes extension REST { public final class AddressCache { - public static let shared: AddressCache = { - let cacheFilename = "api-ip-address.json" - let cacheDirectoryURL = FileManager.default.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first! - let cacheFileURL = cacheDirectoryURL.appendingPathComponent( - cacheFilename, - isDirectory: false - ) - let prebundledCacheFileURL = Bundle(for: AddressCache.self).url( - forResource: cacheFilename, - withExtension: nil - )! - - return AddressCache( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL - ) - }() - - static var defaultCachedAddresses: CachedAddresses { - return CachedAddresses( - updatedAt: Date(timeIntervalSince1970: 0), - endpoints: [ - REST.defaultAPIEndpoint, - ] - ) - } - /// Logger. - private let logger = Logger(label: "AddressCache.Store") + private let logger = Logger(label: "AddressCache") /// Memory cache. - private var cachedAddresses: CachedAddresses + private var cachedAddresses: CachedAddresses = defaultCachedAddresses /// Cache file location. private let cacheFileURL: URL @@ -57,50 +27,35 @@ extension REST { /// Lock used for synchronizing access to instance members. private let nslock = NSLock() - /// Designated initializer - public init(cacheFileURL: URL, prebundledCacheFileURL: URL) { - self.cacheFileURL = cacheFileURL - self.prebundledCacheFileURL = prebundledCacheFileURL - - do { - let readResult = try Self.readFromCacheLocationWithFallback( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL, - logger: logger - ) + /// Whether address cache is in readonly mode. + private var isReadOnly: Bool - switch readResult.source { - case .disk: - cachedAddresses = readResult.cachedAddresses + private static let defaultCachedAddresses = CachedAddresses( + updatedAt: Date(timeIntervalSince1970: 0), + endpoints: [REST.defaultAPIEndpoint] + ) - case .bundle: - var addresses = readResult.cachedAddresses - addresses.endpoints.shuffle() - cachedAddresses = addresses + /// Designated initializer. + public init?(securityGroupIdentifier: String, isReadOnly: Bool) { + let cacheFilename = "api-ip-address.json" - logger.debug("Persist address list read from bundle.") + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: securityGroupIdentifier + ), let prebundledCacheFileURL = Bundle(for: AddressCache.self).url( + forResource: cacheFilename, + withExtension: nil + ) else { return nil } - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to persist address cache after reading it from bundle." - ) - } - } + let cacheFileURL = containerURL.appendingPathComponent( + cacheFilename, + isDirectory: false + ) - logger.debug( - """ - Initialized cache from \(readResult.source) with \ - \(cachedAddresses.endpoints.count) endpoint(s). - """ - ) - } catch { - logger.debug("Initialized cache with default API endpoint.") + self.cacheFileURL = cacheFileURL + self.prebundledCacheFileURL = prebundledCacheFileURL + self.isReadOnly = isReadOnly - cachedAddresses = Self.defaultCachedAddresses - } + initCache() } public func getCurrentEndpoint() -> AnyIPEndpoint { @@ -122,20 +77,25 @@ extension REST { cachedAddresses.endpoints.removeFirst() cachedAddresses.endpoints.append(failedEndpoint) + if isReadOnly { + refreshAddresses() + } + currentEndpoint = cachedAddresses.endpoints.first! - logger - .debug( - "Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)" - ) + logger.debug( + "Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)" + ) - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to write address cache after selecting next endpoint." - ) + if !isReadOnly { + do { + try writeToDisk() + } catch { + logger.error( + error: error, + message: "Failed to write address cache after selecting next endpoint." + ) + } } return currentEndpoint @@ -168,13 +128,15 @@ extension REST { ) } - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to write address cache after setting new endpoints." - ) + if !isReadOnly { + do { + try writeToDisk() + } catch { + logger.error( + error: error, + message: "Failed to write address cache after setting new endpoints." + ) + } } } @@ -187,20 +149,53 @@ extension REST { // MARK: - Private - private static func readFromCacheLocationWithFallback( - cacheFileURL: URL, - prebundledCacheFileURL: URL, - logger: Logger - ) throws -> ReadResult { + private func initCache() { do { - let readResult = ReadResult( - cachedAddresses: try readFromCacheLocation(cacheFileURL), - source: .disk - ) + try initCacheInner() + } 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." + ) + } + } + } - try checkReadResultContainsEndpoints(readResult) + logger.debug( + """ + Initialized cache from \(readResult.source) with \ + \(cachedAddresses.endpoints.count) endpoint(s). + """ + ) + } - return readResult + private func readFromCacheLocationWithFallback() throws -> ReadResult { + do { + return try readFromCacheLocation() } catch { logger.error( error: error, @@ -208,14 +203,7 @@ extension REST { ) do { - let readResult = ReadResult( - cachedAddresses: try readFromBundle(prebundledCacheFileURL), - source: .bundle - ) - - try checkReadResultContainsEndpoints(readResult) - - return readResult + return try readFromBundle() } catch { logger.error( error: error, @@ -227,41 +215,101 @@ extension REST { } } - private static func checkReadResultContainsEndpoints(_ readResult: ReadResult) throws { - if readResult.cachedAddresses.endpoints.isEmpty { - throw EmptyCacheError(source: readResult.source) + private func readFromCacheLocation() throws -> ReadResult { + var result: Result<ReadResult, Swift.Error>? + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + let accessor = { (fileURL: URL) in + result = Result { + let data = try Data(contentsOf: fileURL) + let cachedAddresses = try JSONDecoder().decode(CachedAddresses.self, from: data) + + if cachedAddresses.endpoints.isEmpty { + throw EmptyCacheError(source: .disk) + } + + return ReadResult(cachedAddresses: cachedAddresses, source: .disk) + } } - } - private static func readFromCacheLocation(_ cacheFileURL: URL) throws -> CachedAddresses { - let data = try Data(contentsOf: cacheFileURL) + var error: NSError? + fileCoordinator.coordinate( + readingItemAt: cacheFileURL, + options: .withoutChanges, + error: &error, + byAccessor: accessor + ) + + if let error = error { + result = .failure(error) + } - return try JSONDecoder().decode(CachedAddresses.self, from: data) + return try result!.get() } - private static func readFromBundle(_ prebundledCacheFileURL: URL) throws - -> CachedAddresses - { + private func readFromBundle() throws -> ReadResult { let data = try Data(contentsOf: prebundledCacheFileURL) let endpoints = try JSONDecoder().decode([AnyIPEndpoint].self, from: data) - return CachedAddresses( + 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 { - let cacheDirectoryURL = cacheFileURL.deletingLastPathComponent() + precondition(!isReadOnly) - try? FileManager.default.createDirectory( - at: cacheDirectoryURL, - withIntermediateDirectories: true, - attributes: nil + var result: Result<Void, Swift.Error>? + 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 ) - let data = try JSONEncoder().encode(cachedAddresses) - try data.write(to: cacheFileURL, options: .atomic) + 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.") + } } } |
