diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-03-02 14:40:35 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-03-02 14:40:35 +0100 |
| commit | 0d7d8c6e6c504d0a3065bfa79aba5c89bb037bc5 (patch) | |
| tree | a4ad1d4b63eb5af22da657f98b403e0b225c3ece | |
| parent | 8c2d907f30b68abda7833072ff1204bd84289225 (diff) | |
| parent | a175c2886e04bfdc63d11bcebf8d32159ea6ffac (diff) | |
| download | mullvadvpn-0d7d8c6e6c504d0a3065bfa79aba5c89bb037bc5.tar.xz mullvadvpn-0d7d8c6e6c504d0a3065bfa79aba5c89bb037bc5.zip | |
Merge branch 'fix-ci'
| -rw-r--r-- | ios/.swiftformat | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/AddressCache/AddressCache.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPN/AddressCache/AddressCacheStore.swift | 302 | ||||
| -rw-r--r-- | ios/MullvadVPN/AddressCache/AddressCacheTracker.swift | 201 |
4 files changed, 1 insertions, 515 deletions
diff --git a/ios/.swiftformat b/ios/.swiftformat index 7ff7f3916a..4a2eebe95e 100644 --- a/ios/.swiftformat +++ b/ios/.swiftformat @@ -12,4 +12,4 @@ --wrapternary before-operators --redundanttype inferred --ifdef no-indent ---disable initCoderUnavailable, redundantReturn, unusedArguments, redundantRawValues, preferKeyPath, extensionAccessControl +--disable initCoderUnavailable, redundantReturn, unusedArguments, redundantRawValues, preferKeyPath, extensionAccessControl, hoistTry diff --git a/ios/MullvadVPN/AddressCache/AddressCache.swift b/ios/MullvadVPN/AddressCache/AddressCache.swift deleted file mode 100644 index 77fb8fc5c4..0000000000 --- a/ios/MullvadVPN/AddressCache/AddressCache.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// AddressCache.swift -// MullvadVPN -// -// Created by pronebird on 26/11/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -enum AddressCache {} diff --git a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift b/ios/MullvadVPN/AddressCache/AddressCacheStore.swift deleted file mode 100644 index aed04fd59b..0000000000 --- a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift +++ /dev/null @@ -1,302 +0,0 @@ -// -// AddressCacheStore.swift -// MullvadVPN -// -// Created by pronebird on 08/12/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadLogging - -extension AddressCache { - struct CachedAddresses: Codable { - /// Date when the cached addresses were last updated. - var updatedAt: Date - - /// API endpoints. - 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." - } - } - - class Store { - static let shared: Store = { - 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.main.url( - forResource: cacheFilename, - withExtension: nil - )! - - return Store( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL - ) - }() - - static var defaultCachedAddresses: CachedAddresses { - return CachedAddresses( - updatedAt: Date(timeIntervalSince1970: 0), - endpoints: [ - ApplicationConfiguration.defaultAPIEndpoint, - ] - ) - } - - /// Logger. - private let logger = Logger(label: "AddressCache.Store") - - /// Memory cache. - private var cachedAddresses: CachedAddresses - - /// 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() - - /// Designated initializer - init(cacheFileURL: URL, prebundledCacheFileURL: URL) { - self.cacheFileURL = cacheFileURL - self.prebundledCacheFileURL = prebundledCacheFileURL - - do { - let readResult = try Self.readFromCacheLocationWithFallback( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL, - logger: logger - ) - - switch readResult.source { - case .disk: - cachedAddresses = readResult.cachedAddresses - - case .bundle: - var addresses = readResult.cachedAddresses - addresses.endpoints.shuffle() - cachedAddresses = addresses - - 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). - """ - ) - } catch { - logger.debug("Initialized cache with default API endpoint.") - - cachedAddresses = Self.defaultCachedAddresses - } - } - - func getCurrentEndpoint() -> AnyIPEndpoint { - nslock.lock() - defer { nslock.unlock() } - return cachedAddresses.endpoints.first! - } - - 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) - - currentEndpoint = cachedAddresses.endpoints.first! - - 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." - ) - } - - return currentEndpoint - } - - func setEndpoints(_ endpoints: [AnyIPEndpoint]) { - nslock.lock() - defer { nslock.unlock() } - - guard !endpoints.isEmpty 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 - ) - } - - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to write address cache after setting new endpoints." - ) - } - } - - func getLastUpdateDate() -> Date { - nslock.lock() - defer { nslock.unlock() } - - return cachedAddresses.updatedAt - } - - private static func readFromCacheLocationWithFallback( - cacheFileURL: URL, - prebundledCacheFileURL: URL, - logger: Logger - ) throws -> ReadResult { - do { - let readResult = ReadResult( - cachedAddresses: try readFromCacheLocation(cacheFileURL), - source: .disk - ) - - try checkReadResultContainsEndpoints(readResult) - - return readResult - } catch { - logger.error( - error: error, - message: "Failed to read address cache from disk. Fallback to pre-bundled cache." - ) - - do { - let readResult = ReadResult( - cachedAddresses: try readFromBundle(prebundledCacheFileURL), - source: .bundle - ) - - try checkReadResultContainsEndpoints(readResult) - - return readResult - } catch { - logger.error( - error: error, - message: "Failed to read address cache from bundle." - ) - - throw error - } - } - } - - private static func checkReadResultContainsEndpoints(_ readResult: ReadResult) throws { - if readResult.cachedAddresses.endpoints.isEmpty { - throw EmptyCacheError(source: readResult.source) - } - } - - private static func readFromCacheLocation(_ cacheFileURL: URL) throws -> CachedAddresses { - let data = try Data(contentsOf: cacheFileURL) - - return try JSONDecoder().decode(CachedAddresses.self, from: data) - } - - private static func readFromBundle(_ prebundledCacheFileURL: URL) throws - -> CachedAddresses - { - let data = try Data(contentsOf: prebundledCacheFileURL) - let endpoints = try JSONDecoder().decode([AnyIPEndpoint].self, from: data) - - return CachedAddresses( - updatedAt: Date(timeIntervalSince1970: 0), - endpoints: endpoints - ) - } - - private func writeToDisk() throws { - let cacheDirectoryURL = cacheFileURL.deletingLastPathComponent() - - try? FileManager.default.createDirectory( - at: cacheDirectoryURL, - withIntermediateDirectories: true, - attributes: nil - ) - - let data = try JSONEncoder().encode(cachedAddresses) - try data.write(to: cacheFileURL, options: .atomic) - } - } -} diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift deleted file mode 100644 index 56b92d06e3..0000000000 --- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// AddressCacheTracker.swift -// MullvadVPN -// -// Created by pronebird on 08/12/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import MullvadLogging -import Operations -import UIKit - -extension AddressCache { - class Tracker { - /// Shared instance. - static let shared = AddressCache.Tracker( - apiProxy: REST.ProxyFactory.shared.createAPIProxy(), - store: AddressCache.Store.shared - ) - - /// Update interval (in seconds). - private static let updateInterval: TimeInterval = 60 * 60 * 24 - - /// Retry interval (in seconds). - private static let retryInterval: TimeInterval = 60 * 15 - - /// Logger. - private let logger = Logger(label: "AddressCache.Tracker") - - /// REST API proxy. - private let apiProxy: REST.APIProxy - - /// Store. - private let store: AddressCache.Store - - /// A flag that indicates whether periodic updates are running - private var isPeriodicUpdatesEnabled = false - - /// The date of last failed attempt. - private var lastFailureAttemptDate: Date? - - /// Timer used for scheduling periodic updates. - private var timer: DispatchSourceTimer? - - /// Operation queue. - private let operationQueue: AsyncOperationQueue = { - let operationQueue = AsyncOperationQueue() - operationQueue.maxConcurrentOperationCount = 1 - return operationQueue - }() - - /// Lock used for synchronizing member access. - private let nslock = NSLock() - - /// Designated initializer - private init(apiProxy: REST.APIProxy, store: AddressCache.Store) { - self.apiProxy = apiProxy - self.store = store - } - - func startPeriodicUpdates() { - nslock.lock() - defer { nslock.unlock() } - - guard !isPeriodicUpdatesEnabled else { - return - } - - logger.debug("Start periodic address cache updates.") - - isPeriodicUpdatesEnabled = true - - let scheduleDate = _nextScheduleDate() - - logger.debug("Schedule address cache update at \(scheduleDate.logFormatDate()).") - - scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow) - } - - func stopPeriodicUpdates() { - nslock.lock() - defer { nslock.unlock() } - - guard isPeriodicUpdatesEnabled else { return } - - logger.debug("Stop periodic address cache updates.") - - isPeriodicUpdatesEnabled = false - - timer?.cancel() - timer = nil - } - - func updateEndpoints( - completionHandler: ((OperationCompletion<Bool, Error>) -> Void)? = nil - ) -> Cancellable { - let operation = ResultBlockOperation<Bool, Error> { operation in - guard self.nextScheduleDate() <= Date() else { - operation.finish(completion: .success(false)) - return - } - - let task = self.apiProxy.getAddressList(retryStrategy: .default) { completion in - self.setEndpoints(from: completion) - - let mappedCompletion = completion.map { _ in true } - .eraseFailureType() - - operation.finish(completion: mappedCompletion) - } - - operation.addCancellationBlock { - task.cancel() - } - } - - operation.completionQueue = .main - operation.completionHandler = completionHandler - - operation.addObserver( - BackgroundObserver(name: "Update endpoints", cancelUponExpiration: true) - ) - - operationQueue.addOperation(operation) - - return operation - } - - func nextScheduleDate() -> Date { - nslock.lock() - defer { nslock.unlock() } - - return _nextScheduleDate() - } - - private func setEndpoints(from completion: OperationCompletion<[AnyIPEndpoint], REST.Error>) - { - nslock.lock() - defer { nslock.unlock() } - - switch completion { - case let .success(endpoints): - store.setEndpoints(endpoints) - - case let .failure(error): - logger.error( - error: error, - message: "Failed to update address cache." - ) - - case .cancelled: - break - } - - lastFailureAttemptDate = completion.isSuccess ? nil : Date() - } - - private func scheduleEndpointsUpdate(startTime: DispatchWallTime) { - let newTimer = DispatchSource.makeTimerSource() - newTimer.setEventHandler { [weak self] in - self?.handleTimer() - } - - newTimer.schedule(wallDeadline: startTime) - newTimer.activate() - - timer?.cancel() - timer = newTimer - } - - private func handleTimer() { - _ = updateEndpoints { _ in - self.nslock.lock() - defer { self.nslock.unlock() } - - guard self.isPeriodicUpdatesEnabled else { return } - - let scheduleDate = self._nextScheduleDate() - - self.logger - .debug("Schedule next address cache update at \(scheduleDate.logFormatDate()).") - - self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow) - } - } - - private func _nextScheduleDate() -> Date { - let nextDate = lastFailureAttemptDate.map { date in - return Date( - timeInterval: Self.retryInterval, - since: date - ) - } ?? Date( - timeInterval: Self.updateInterval, - since: store.getLastUpdateDate() - ) - - return max(nextDate, Date()) - } - } -} |
