diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-08-25 12:20:29 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-08-25 12:20:29 +0200 |
| commit | c118b96b27c088565f403ea0673f65e0f2174666 (patch) | |
| tree | 2745b5ba0011ca4f4b9a799002cec400a5f48e1a | |
| parent | 082201f0e662b6c2811e5129519d07a527868ad7 (diff) | |
| parent | 2130af52d6430f11a1ff2fa61d5ab748d4fd02b9 (diff) | |
| download | mullvadvpn-c118b96b27c088565f403ea0673f65e0f2174666.tar.xz mullvadvpn-c118b96b27c088565f403ea0673f65e0f2174666.zip | |
Merge branch 'change-address-cache-schema'
| -rw-r--r-- | ios/CHANGELOG.md | 5 | ||||
| -rw-r--r-- | ios/MullvadREST/AddressCache.swift | 102 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/AddressCacheTests.swift | 29 |
3 files changed, 62 insertions, 74 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 5b2571b5fc..1bab652812 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -27,6 +27,11 @@ Line wrap the file at 100 chars. Th - Allow deleting account in account view. - Add new account flow +### Fixed +- Invalidate API IP address cache to fix connectivity issues for some of devices updating from + 2023.2 or earlier. + + ## [2023.3 - 2023-07-15] ### Added - Add search functionality to location selection view. diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index da27798c9d..8db610fc48 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -16,10 +16,10 @@ extension REST { private let logger = Logger(label: "AddressCache") /// Disk cache. - private let fileCache: any FileCacheProtocol<CachedAddresses> + private let fileCache: any FileCacheProtocol<StoredAddressCache> /// Memory cache. - private var cache: CachedAddresses = defaultCachedAddresses + private var cache: StoredAddressCache = defaultStoredCache /// Lock used for synchronizing access to instance members. private let cacheLock = NSLock() @@ -27,10 +27,10 @@ extension REST { /// Whether address cache can be written to. private let canWriteToCache: Bool - /// The default set of endpoints to use as a fallback mechanism - private static let defaultCachedAddresses = CachedAddresses( + /// The default endpoint to use as a fallback mechanism. + private static let defaultStoredCache = StoredAddressCache( updatedAt: Date(timeIntervalSince1970: 0), - endpoints: [REST.defaultAPIEndpoint] + endpoint: REST.defaultAPIEndpoint ) // MARK: - Public API @@ -44,7 +44,7 @@ extension REST { } /// Initializer that accepts a file cache implementation and can be used in tests. - init(canWriteToCache: Bool, fileCache: some FileCacheProtocol<CachedAddresses>) { + init(canWriteToCache: Bool, fileCache: some FileCacheProtocol<StoredAddressCache>) { self.fileCache = fileCache self.canWriteToCache = canWriteToCache } @@ -54,23 +54,19 @@ extension REST { /// 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 { - cacheLock.lock() - defer { cacheLock.unlock() } - var currentEndpoint = cache.endpoints.first ?? REST.defaultAPIEndpoint - - // Reload from disk cache when in the Network Extension as there is no `AddressCacheTracker` running - // there - if canWriteToCache == false { - do { - cache = try fileCache.read() - if let firstEndpoint = cache.endpoints.first { - currentEndpoint = firstEndpoint + cacheLock.withLock { + // Reload from disk cache when in the Network Extension as there is no `AddressCacheTracker` running + // there + if canWriteToCache == false { + do { + cache = try fileCache.read() + } catch { + logger.error(error: error, message: "Failed to read address cache from disk.") } - } catch { - logger.error(error: error, message: "Failed to read address cache from disk.") } + + return cache.endpoint } - return currentEndpoint } /// Updates the available endpoints to use @@ -79,27 +75,20 @@ extension REST { /// 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() } + cacheLock.withLock { + guard let firstEndpoint = endpoints.first else { return } - guard let firstEndpoint = endpoints.first else { return } - if Set(cache.endpoints) == Set(endpoints) { - cache.updatedAt = Date() - } else { - cache = CachedAddresses( - updatedAt: Date(), - endpoints: [firstEndpoint] - ) - } + cache = StoredAddressCache(updatedAt: Date(), endpoint: firstEndpoint) - guard canWriteToCache else { return } - do { - try fileCache.write(cache) - } catch { - logger.error( - error: error, - message: "Failed to write address cache after setting new endpoints." - ) + guard canWriteToCache else { return } + do { + try fileCache.write(cache) + } catch { + logger.error( + error: error, + message: "Failed to write address cache after setting new endpoints." + ) + } } } @@ -107,32 +96,37 @@ extension REST { /// /// - Returns: The `Date` when the cache was last updated at public func getLastUpdateDate() -> Date { - cacheLock.lock() - defer { cacheLock.unlock() } - - return cache.updatedAt + return cacheLock.withLock { cache.updatedAt } } - /// Initializes the cache by reading the a cached file on disk. + /// Initializes cache by reading it from file on disk. /// /// If no cache file is present, a default API endpoint will be selected instead. public func loadFromFile() { - // 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 { - cache = try fileCache.read() - } catch { - logger.debug("Initialized cache with default API endpoint.") - cache = Self.defaultCachedAddresses + cacheLock.withLock { + // 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 { + cache = try fileCache.read() + } catch { + // Log all errors except when file is not on disk. + if (error as? CocoaError)?.code != .fileReadNoSuchFile { + logger.error(error: error, message: "Failed to load address cache from disk.") + } + + logger.debug("Initialized cache with default API endpoint.") + cache = Self.defaultStoredCache + } } } } - public struct CachedAddresses: Codable, Equatable { + /// Serializable address cache representation stored on disk. + struct StoredAddressCache: Codable, Equatable { /// Date when the cached addresses were last updated. var updatedAt: Date - /// API endpoints. - var endpoints: [AnyIPEndpoint] + /// API endpoint. + var endpoint: AnyIPEndpoint } } diff --git a/ios/MullvadVPNTests/AddressCacheTests.swift b/ios/MullvadVPNTests/AddressCacheTests.swift index d0e1d823b1..5a5dedfe1a 100644 --- a/ios/MullvadVPNTests/AddressCacheTests.swift +++ b/ios/MullvadVPNTests/AddressCacheTests.swift @@ -68,18 +68,17 @@ final class AddressCacheTests: XCTestCase { let firstIPEndpoint = try XCTUnwrap(ipAddresses.first) - let fileCache = MockFileCache<REST.CachedAddresses>() + let fileCache = MockFileCache<REST.StoredAddressCache>() let addressCache = REST.AddressCache(canWriteToCache: true, fileCache: fileCache) addressCache.setEndpoints(ipAddresses) let fileState = fileCache.getState() - XCTAssertTrue(fileState.isExists) - guard case let .exists(cachedAddresses) = fileState else { + guard case let .exists(storedAddressCache) = fileState else { XCTFail("State is expected to contain cached addresses.") return } - XCTAssertEqual(cachedAddresses.endpoints.count, 1) + XCTAssertEqual(storedAddressCache.endpoint, firstIPEndpoint) XCTAssertEqual(addressCache.getCurrentEndpoint(), firstIPEndpoint) } @@ -88,7 +87,7 @@ final class AddressCacheTests: XCTestCase { let addressCache = REST.AddressCache( canWriteToCache: true, fileCache: MockFileCache(initialState: .exists( - REST.CachedAddresses(updatedAt: fixedDate, endpoints: [apiEndpoint]) + REST.StoredAddressCache(updatedAt: fixedDate, endpoint: apiEndpoint) )) ) addressCache.loadFromFile() @@ -98,7 +97,7 @@ final class AddressCacheTests: XCTestCase { } func testCacheWritesToDiskWhenSettingNewEndpoints() throws { - let fileCache = MockFileCache<REST.CachedAddresses>() + let fileCache = MockFileCache<REST.StoredAddressCache>() let addressCache = REST.AddressCache(canWriteToCache: true, fileCache: fileCache) XCTAssertEqual(fileCache.getState(), .fileNotFound) @@ -107,32 +106,22 @@ final class AddressCacheTests: XCTestCase { let fileState = fileCache.getState() XCTAssertTrue(fileState.isExists) - guard case let .exists(cachedAddresses) = fileState else { + guard case let .exists(storedAddressCache) = fileState else { XCTFail("State is expected to contain cached addresses.") return } - XCTAssertEqual(cachedAddresses.endpoints, [addressCache.getCurrentEndpoint()]) - XCTAssertEqual(cachedAddresses.updatedAt, addressCache.getLastUpdateDate()) + XCTAssertEqual(storedAddressCache.endpoint, addressCache.getCurrentEndpoint()) + XCTAssertEqual(storedAddressCache.updatedAt, addressCache.getLastUpdateDate()) } func testGetCurrentEndpointReadsFromCacheWhenReadOnly() throws { let addressCache = REST.AddressCache( canWriteToCache: false, fileCache: MockFileCache(initialState: .exists( - REST.CachedAddresses(updatedAt: Date(), endpoints: [apiEndpoint]) + REST.StoredAddressCache(updatedAt: Date(), endpoint: apiEndpoint) )) ) XCTAssertEqual(addressCache.getCurrentEndpoint(), apiEndpoint) } - - func testGetCurrentEndpointHasDefaultEndpointIfCacheIsEmpty() throws { - let addressCache = REST.AddressCache( - canWriteToCache: false, - fileCache: MockFileCache(initialState: .exists(REST.CachedAddresses(updatedAt: Date(), endpoints: []))) - ) - addressCache.loadFromFile() - - XCTAssertEqual(addressCache.getCurrentEndpoint(), REST.defaultAPIEndpoint) - } } |
