diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-06-12 11:52:10 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-06-13 16:25:10 +0200 |
| commit | b6a59eb7d162c97dd8827eeed46cfca7d2eef171 (patch) | |
| tree | e527fc18d4eff5699bad03b7acbb685972a2b5fd | |
| parent | e84905793ac54c4768a416f76dd4589e1f66fcf1 (diff) | |
| download | mullvadvpn-b6a59eb7d162c97dd8827eeed46cfca7d2eef171.tar.xz mullvadvpn-b6a59eb7d162c97dd8827eeed46cfca7d2eef171.zip | |
Replace Caching type with FileCache<Content: Codable>
22 files changed, 481 insertions, 540 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index 0f449f58ff..8279b1c263 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -11,24 +11,15 @@ import MullvadLogging import MullvadTypes extension REST { - public struct CachedAddresses: Codable { - /// Date when the cached addresses were last updated. - var updatedAt: Date - - /// API endpoints. - var endpoints: [AnyIPEndpoint] - } - - public final class AddressCache: Caching { - public typealias CacheType = CachedAddresses + public final class AddressCache { /// Logger. private let logger = Logger(label: "AddressCache") - /// Memory cache. - var cache: CachedAddresses = defaultCachedAddresses + /// Disk cache. + private let fileCache: any FileCacheProtocol<CachedAddresses> - /// Cache file location. - public let cacheFileURL: URL + /// Memory cache. + private var cache: CachedAddresses = defaultCachedAddresses /// Lock used for synchronizing access to instance members. private let cacheLock = NSLock() @@ -36,27 +27,25 @@ extension REST { /// Whether address cache can be written to. private let canWriteToCache: Bool - /// The name of the cache file on disk - public 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] ) - // MARK: - - - // MARK: Public API + // MARK: - Public API /// Designated initializer. - public init(canWriteToCache: Bool, cacheFolder: URL) { - let cacheFileURL = cacheFolder.appendingPathComponent( - Self.cacheFileName, - isDirectory: false + public init(canWriteToCache: Bool, cacheDirectory: URL) { + fileCache = FileCache( + fileURL: cacheDirectory.appendingPathComponent("api-ip-address.json", isDirectory: false) ) + self.canWriteToCache = canWriteToCache + } - self.cacheFileURL = cacheFileURL + /// Initializer that accepts a file cache implementation and can be used in tests. + init(canWriteToCache: Bool, fileCache: some FileCacheProtocol<CachedAddresses>) { + self.fileCache = fileCache self.canWriteToCache = canWriteToCache } @@ -73,12 +62,12 @@ extension REST { // there if canWriteToCache == false { do { - cache = try readFromDisk() + cache = try fileCache.read() if let firstEndpoint = cache.endpoints.first { currentEndpoint = firstEndpoint } } catch { - logger.error(error: error) + logger.error(error: error, message: "Failed to read address cache from disk.") } } return currentEndpoint @@ -108,15 +97,14 @@ extension REST { ) } - if canWriteToCache { - do { - try writeToDisk(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." + ) } } @@ -130,18 +118,26 @@ extension REST { return cache.updatedAt } - /// Initializes the cache by reading the a cached file from disk + /// Initializes the cache by reading the a cached file on disk. /// - /// If no cache file is present, a default API endpoint will be selected instead - public func initCache() { + /// 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 readFromDisk() + cache = try fileCache.read() } catch { logger.debug("Initialized cache with default API endpoint.") cache = Self.defaultCachedAddresses } } } + + public struct CachedAddresses: Codable, Equatable { + /// Date when the cached addresses were last updated. + var updatedAt: Date + + /// API endpoints. + var endpoints: [AnyIPEndpoint] + } } diff --git a/ios/MullvadREST/ServerRelaysResponse.swift b/ios/MullvadREST/ServerRelaysResponse.swift index d91d05baee..081802196b 100644 --- a/ios/MullvadREST/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ServerRelaysResponse.swift @@ -12,7 +12,7 @@ import struct Network.IPv4Address import struct Network.IPv6Address extension REST { - public struct ServerLocation: Codable { + public struct ServerLocation: Codable, Equatable { public let country: String public let city: String public let latitude: Double @@ -26,7 +26,7 @@ extension REST { } } - public struct BridgeRelay: Codable { + public struct BridgeRelay: Codable, Equatable { public let hostname: String public let active: Bool public let owned: Bool @@ -37,7 +37,7 @@ extension REST { public let includeInCountry: Bool } - public struct ServerRelay: Codable { + public struct ServerRelay: Codable, Equatable { public let hostname: String public let active: Bool public let owned: Bool @@ -74,7 +74,7 @@ extension REST { } } - public struct ServerWireguardTunnels: Codable { + public struct ServerWireguardTunnels: Codable, Equatable { public let ipv4Gateway: IPv4Address public let ipv6Gateway: IPv6Address public let portRanges: [[UInt16]] @@ -93,7 +93,7 @@ extension REST { } } - public struct ServerShadowsocks: Codable { + public struct ServerShadowsocks: Codable, Equatable { public let `protocol`: String public let port: UInt16 public let cipher: String @@ -107,7 +107,7 @@ extension REST { } } - public struct ServerBridges: Codable { + public struct ServerBridges: Codable, Equatable { public let shadowsocks: [ServerShadowsocks] public let relays: [BridgeRelay] @@ -117,7 +117,7 @@ extension REST { } } - public struct ServerRelaysResponse: Codable { + public struct ServerRelaysResponse: Codable, Equatable { public let locations: [String: ServerLocation] public let wireguard: ServerWireguardTunnels public let bridge: ServerBridges diff --git a/ios/MullvadTypes/ShadowsocksConfiguration.swift b/ios/MullvadTransport/ShadowsocksConfiguration.swift index 0a145f0976..0a145f0976 100644 --- a/ios/MullvadTypes/ShadowsocksConfiguration.swift +++ b/ios/MullvadTransport/ShadowsocksConfiguration.swift diff --git a/ios/MullvadTransport/ShadowsocksConfigurationCache.swift b/ios/MullvadTransport/ShadowsocksConfigurationCache.swift new file mode 100644 index 0000000000..111215138e --- /dev/null +++ b/ios/MullvadTransport/ShadowsocksConfigurationCache.swift @@ -0,0 +1,47 @@ +// +// ShadowsocksConfigurationCache.swift +// MullvadTransport +// +// Created by Marco Nikic on 2023-06-05. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +/// Holds a shadowsocks configuration object backed by a caching mechanism shared across processes +public final class ShadowsocksConfigurationCache { + private let configurationLock = NSLock() + private var cachedConfiguration: ShadowsocksConfiguration? + private let fileCache: FileCache<ShadowsocksConfiguration> + + public init(cacheDirectory: URL) { + fileCache = FileCache( + fileURL: cacheDirectory.appendingPathComponent("shadowsocks-cache.json", isDirectory: false) + ) + } + + /// Returns configration from memory cache if available, otherwise attempts to load it from disk cache before + /// returning. + public func read() throws -> ShadowsocksConfiguration { + configurationLock.lock() + defer { configurationLock.unlock() } + + if let cachedConfiguration { + return cachedConfiguration + } else { + let readConfiguration = try fileCache.read() + cachedConfiguration = readConfiguration + return readConfiguration + } + } + + /// Replace memory cache with new configuration and attempt to persist it on disk. + public func write(_ configuration: ShadowsocksConfiguration) throws { + configurationLock.lock() + defer { configurationLock.unlock() } + + cachedConfiguration = configuration + try fileCache.write(configuration) + } +} diff --git a/ios/MullvadTransport/TransportProvider.swift b/ios/MullvadTransport/TransportProvider.swift index 93274e9edb..786f34a2b2 100644 --- a/ios/MullvadTransport/TransportProvider.swift +++ b/ios/MullvadTransport/TransportProvider.swift @@ -49,23 +49,27 @@ public final class TransportProvider: RESTTransportProvider { return shadowsocksTransport } catch { - logger.error(error: error) + logger.error(error: error, message: "Failed to produce shadowsocks configuration.") + return nil } - return nil } - /// The last used shadowsocks configuration - /// - /// The last used shadowsocks configuration if any, otherwise a random one selected by `RelaySelector` - /// - Returns: A shadowsocks configuration + /// Returns the last used shadowsocks configuration, otherwise a new randomized configuration. private func shadowsocksConfiguration() throws -> ShadowsocksConfiguration { - // If a previous shadowsocks configuration was in cache, return it directly - if let configuration = shadowsocksCache.configuration { - return configuration - } - + // If a previous shadowsocks configuration was in cache, return it directly. // There is no previous configuration either if this is the first time this code ran // Or because the previous shadowsocks configuration was invalid, therefore generate a new one. + do { + return try shadowsocksCache.read() + } catch { + // There is no previous configuration either if this is the first time this code ran + // Or because the previous shadowsocks configuration was invalid, therefore generate a new one. + return try makeNewShadowsocksConfiguration() + } + } + + /// Returns a randomly selected shadowsocks configuration. + private func makeNewShadowsocksConfiguration() throws -> ShadowsocksConfiguration { let cachedRelays = try relayCache.read() let bridgeAddress = RelaySelector.getShadowsocksRelay(relays: cachedRelays.relays)?.ipv4AddrIn let bridgeConfiguration = RelaySelector.getShadowsocksTCPBridge(relays: cachedRelays.relays) @@ -78,7 +82,13 @@ public final class TransportProvider: RESTTransportProvider { password: bridgeConfiguration.password, cipher: bridgeConfiguration.cipher ) - shadowsocksCache.configuration = newConfiguration + + do { + try shadowsocksCache.write(newConfiguration) + } catch { + logger.error(error: error, message: "Failed to persist shadowsocks cache.") + } + return newConfiguration } } diff --git a/ios/MullvadTransport/URLSessionShadowsocksTransport.swift b/ios/MullvadTransport/URLSessionShadowsocksTransport.swift new file mode 100644 index 0000000000..a4e7e34403 --- /dev/null +++ b/ios/MullvadTransport/URLSessionShadowsocksTransport.swift @@ -0,0 +1,65 @@ +// +// ShadowsocksTransport.swift +// MullvadTransport +// +// Created by pronebird on 12/06/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST +import MullvadTypes + +public final class URLSessionShadowsocksTransport: RESTTransport { + /// The Shadowsocks proxy instance that proxies all the traffic it receives + private let shadowsocksProxy: ShadowsocksProxy + + /// The IPv4 representation of the loopback address used by `shadowsocksProxy` + private let localhost = "127.0.0.1" + + /// The `URLSession` used to send requests via `shadowsocksProxy` + public let urlSession: URLSession + + public var name: String { + return "shadow-socks-url-session" + } + + public init( + urlSession: URLSession, + shadowsocksConfiguration: ShadowsocksConfiguration, + addressCache: REST.AddressCache + ) { + self.urlSession = urlSession + let apiAddress = addressCache.getCurrentEndpoint() + + shadowsocksProxy = ShadowsocksProxy( + forwardAddress: apiAddress.ip, + forwardPort: apiAddress.port, + bridgeAddress: shadowsocksConfiguration.bridgeAddress, + bridgePort: shadowsocksConfiguration.bridgePort, + password: shadowsocksConfiguration.password, + cipher: shadowsocksConfiguration.cipher + ) + } + + public func sendRequest( + _ request: URLRequest, + completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void + ) -> Cancellable { + // Start the Shadowsocks proxy in order to get a local port + shadowsocksProxy.start() + + // Copy the URL request and rewrite the host and port to point to the Shadowsocks proxy instance + var urlRequestCopy = request + urlRequestCopy.url = request.url.flatMap { url in + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.host = localhost + components?.port = Int(shadowsocksProxy.localPort()) + return components?.url + } + + let dataTask = urlSession.dataTask(with: urlRequestCopy, completionHandler: completion) + dataTask.resume() + return dataTask + } +} diff --git a/ios/MullvadTransport/URLSessionTransport.swift b/ios/MullvadTransport/URLSessionTransport.swift index 8556f812ca..4ecef05870 100644 --- a/ios/MullvadTransport/URLSessionTransport.swift +++ b/ios/MullvadTransport/URLSessionTransport.swift @@ -31,56 +31,3 @@ public final class URLSessionTransport: RESTTransport { return dataTask } } - -public final class URLSessionShadowsocksTransport: RESTTransport { - /// The Shadowsocks proxy instance that proxies all the traffic it receives - private let shadowsocksProxy: ShadowsocksProxy - /// The IPv4 representation of the loopback address used by `shadowsocksProxy` - private let localhost = "127.0.0.1" - - /// The `URLSession` used to send requests via `shadowsocksProxy` - public let urlSession: URLSession - - public var name: String { - return "shadow-socks-url-session" - } - - public init( - urlSession: URLSession, - shadowsocksConfiguration: ShadowsocksConfiguration, - addressCache: REST.AddressCache - ) { - self.urlSession = urlSession - let apiAddress = addressCache.getCurrentEndpoint() - - shadowsocksProxy = ShadowsocksProxy( - forwardAddress: apiAddress.ip, - forwardPort: apiAddress.port, - bridgeAddress: shadowsocksConfiguration.bridgeAddress, - bridgePort: shadowsocksConfiguration.bridgePort, - password: shadowsocksConfiguration.password, - cipher: shadowsocksConfiguration.cipher - ) - } - - public func sendRequest( - _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void - ) -> Cancellable { - // Start the Shadowsocks proxy in order to get a local port - shadowsocksProxy.start() - - // Copy the URL request and rewrite the host and port to point to the Shadowsocks proxy instance - var urlRequestCopy = request - urlRequestCopy.url = request.url.flatMap { url in - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - components?.host = localhost - components?.port = Int(shadowsocksProxy.localPort()) - return components?.url - } - - let dataTask = urlSession.dataTask(with: urlRequestCopy, completionHandler: completion) - dataTask.resume() - return dataTask - } -} diff --git a/ios/MullvadTypes/Cache.swift b/ios/MullvadTypes/Cache.swift deleted file mode 100644 index 6238e6c947..0000000000 --- a/ios/MullvadTypes/Cache.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Cache.swift -// MullvadTypes -// -// Created by Marco Nikic on 2023-05-30. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// A protocol for reading and writing to a cache file using `NSFileCoordinator` for cross process protection. -/// -/// Uses `JSONDecoder` for reading and `JSONEncoder` for writing. `CacheType` must conform to `Codable` -public protocol Caching<CacheType> where CacheType: Codable { - associatedtype CacheType - - /// The name of the cache file - static var cacheFileName: String { get } - /// The location of the cache file - var cacheFileURL: URL { get } - - func readFromDisk() throws -> CacheType - func writeToDisk(_: CacheType) throws -} - -public extension Caching { - func readFromDisk() throws -> CacheType { - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - let result = try fileCoordinator - .coordinate(readingItemAt: cacheFileURL, options: [.withoutChanges]) { file in - let data = try Data(contentsOf: file) - let cachedFile = try JSONDecoder().decode(CacheType.self, from: data) - - return cachedFile - } - - return result - } - - func writeToDisk(_ cache: CacheType) throws { - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - - try fileCoordinator.coordinate(writingItemAt: cacheFileURL, options: [.forReplacing]) { file in - let data = try JSONEncoder().encode(cache) - try data.write(to: file) - } - } -} diff --git a/ios/MullvadTypes/FileCache.swift b/ios/MullvadTypes/FileCache.swift new file mode 100644 index 0000000000..4e16a73c41 --- /dev/null +++ b/ios/MullvadTypes/FileCache.swift @@ -0,0 +1,42 @@ +// +// FileCache.swift +// MullvadTypes +// +// Created by Marco Nikic on 2023-05-30. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// File cache implementation that can read and write any `Codable` content and uses file coordinator to coordinate I/O. +public struct FileCache<Content: Codable>: FileCacheProtocol { + public let fileURL: URL + + public init(fileURL: URL) { + self.fileURL = fileURL + } + + public func read() throws -> Content { + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + return try fileCoordinator.coordinate(readingItemAt: fileURL, options: [.withoutChanges]) { fileURL in + return try JSONDecoder().decode(Content.self, from: Data(contentsOf: fileURL)) + } + } + + public func write(_ content: Content) throws { + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + try fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forReplacing]) { fileURL in + try JSONEncoder().encode(content).write(to: fileURL) + } + } +} + +/// Protocol describing file cache that's able to read and write serializable content. +public protocol FileCacheProtocol<Content> { + associatedtype Content: Codable + + func read() throws -> Content + func write(_ content: Content) throws +} diff --git a/ios/MullvadTypes/ShadowsocksConfigurationCache.swift b/ios/MullvadTypes/ShadowsocksConfigurationCache.swift deleted file mode 100644 index d8f035a8db..0000000000 --- a/ios/MullvadTypes/ShadowsocksConfigurationCache.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// ShadowsocksConfigurationCache.swift -// MullvadTypes -// -// Created by Marco Nikic on 2023-06-05. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Holds a shadowsocks configuration object backed by a caching mechanism shared across processes -public class ShadowsocksConfigurationCache: Caching { - public typealias CacheType = ShadowsocksConfiguration - - public static var cacheFileName: String { "shadowsocks-cache.json" } - public let cacheFileURL: URL - - private var _configuration: ShadowsocksConfiguration? - private let cacheLock = NSLock() - - public init(cacheFolder: URL) { - let cacheFileURL = cacheFolder.appendingPathComponent( - Self.cacheFileName, - isDirectory: false - ) - - self.cacheFileURL = cacheFileURL - } - - /// The cached shadowsocks configuration object - /// If there is no cache available, a configuration will be read from disk - public var configuration: ShadowsocksConfiguration? { - get { - cacheLock.lock() - defer { cacheLock.unlock() } - - if let _configuration { - return _configuration - } - do { - let diskCache = try readFromDisk() - return diskCache - } catch { - return nil - } - } - set { - cacheLock.lock() - defer { cacheLock.unlock() } - - _configuration = newValue - if let _configuration { - try? writeToDisk(_configuration) - } - } - } -} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 89c6598405..f16f22ba27 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; }; 5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */; }; 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */; }; + 5822C0042A3724A800A3A5FB /* ShadowsocksConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */; }; + 5822C0052A3724A800A3A5FB /* ShadowsocksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */; }; 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; }; 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; @@ -251,6 +253,8 @@ 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; 58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4F82964B08300D72515 /* MapViewController.swift */; }; 58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */; }; + 58C3FA662A38549D006A450A /* MockFileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA652A38549D006A450A /* MockFileCache.swift */; }; + 58C3FA682A385C89006A450A /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3FA672A385C89006A450A /* FileCacheTests.swift */; }; 58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 58C76A092A33850E00100D75 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 58C76A0B2A338E4300100D75 /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; }; @@ -340,6 +344,7 @@ 58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */; }; 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */; }; 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; + 58E0E2842A3718CE002E3420 /* URLSessionShadowsocksTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */; }; 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */; }; 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; }; 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; }; @@ -388,11 +393,6 @@ A93D13782A1F60A6001EB0B1 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 586F2BE129F6916F009E6924 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; }; A9467E7F2A29DEFE000DC21F /* RelayCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */; }; A9467E802A29E0A6000DC21F /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; }; - A9467E822A29E0F8000DC21F /* TestsCacheFilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E812A29E0F8000DC21F /* TestsCacheFilePresenter.swift */; }; - A9467E842A29E69F000DC21F /* CachedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E832A29E69F000DC21F /* CachedTests.swift */; }; - A9467E862A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E852A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift */; }; - A9467E892A2DD688000DC21F /* ShadowsocksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */; }; - A9467E8B2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */; }; A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */; }; A95F86B82A1F547000245DAC /* ShadowsocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */; }; A97F1F442A1F4E1A00ECEFDE /* MullvadTransport.h in Headers */ = {isa = PBXBuildFile; fileRef = A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -400,7 +400,7 @@ A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A97FF54B2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54A2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift */; }; A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; }; - A9A8A8EB2A262AB30086D569 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* Cache.swift */; }; + A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* FileCache.swift */; }; A9B2CF722A1F64CD0013CC6C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */; }; @@ -1027,6 +1027,8 @@ 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; }; 58C3F4F82964B08300D72515 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; }; 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; }; + 58C3FA652A38549D006A450A /* MockFileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileCache.swift; sourceTree = "<group>"; }; + 58C3FA672A385C89006A450A /* FileCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheTests.swift; sourceTree = "<group>"; }; 58C76A072A33850E00100D75 /* ApplicationTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTarget.swift; sourceTree = "<group>"; }; 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTask.swift; sourceTree = "<group>"; }; 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationController.swift; sourceTree = "<group>"; }; @@ -1069,6 +1071,7 @@ 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = "<group>"; }; 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorDelegate.swift; sourceTree = "<group>"; }; 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; + 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionShadowsocksTransport.swift; sourceTree = "<group>"; }; 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationUIHandler.swift; sourceTree = "<group>"; }; 58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; @@ -1117,16 +1120,13 @@ A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; }; A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; }; A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; }; - A9467E812A29E0F8000DC21F /* TestsCacheFilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsCacheFilePresenter.swift; sourceTree = "<group>"; }; - A9467E832A29E69F000DC21F /* CachedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedTests.swift; sourceTree = "<group>"; }; - A9467E852A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Mocks.swift"; sourceTree = "<group>"; }; A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfiguration.swift; sourceTree = "<group>"; }; A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = "<group>"; }; A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadTransport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadTransport.h; sourceTree = "<group>"; }; A97FF54A2A0B7AD000900996 /* SimulatorTunnelTransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelTransportProvider.swift; sourceTree = "<group>"; }; A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; }; - A9A8A8EA2A262AB30086D569 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; }; + A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; }; A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; }; A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = "<group>"; }; E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; }; @@ -1373,7 +1373,7 @@ 584D26BE270C550B004EA533 /* AnyIPAddress.swift */, 586A951329013235007BAF2B /* AnyIPEndpoint.swift */, 06AC113628F83FD70037AF9A /* Cancellable.swift */, - A9A8A8EA2A262AB30086D569 /* Cache.swift */, + A9A8A8EA2A262AB30086D569 /* FileCache.swift */, 58E511E328DDDE8900B0BCDE /* CustomErrorDescriptionProtocol.swift */, 586168682976F6BD00EF8598 /* DisplayError.swift */, 58E511EA28DDE18400B0BCDE /* Error+Chain.swift */, @@ -1395,8 +1395,6 @@ 5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */, 581DA2722A1E227D0046ED47 /* RESTTypes.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, - A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */, - A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */, 58E511E028DDB7F100B0BCDE /* WrappingError.swift */, ); path = MullvadTypes; @@ -1882,18 +1880,16 @@ children = ( 58B0A2A4238EE67E00BC001D /* Info.plist */, F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, - 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */, A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, - A9467E832A29E69F000DC21F /* CachedTests.swift */, 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */, 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */, + 58C3FA672A385C89006A450A /* FileCacheTests.swift */, 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, - A9467E852A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, - A9467E812A29E0F8000DC21F /* TestsCacheFilePresenter.swift */, + 58C3FA652A38549D006A450A /* MockFileCache.swift */, ); path = MullvadVPNTests; sourceTree = "<group>"; @@ -2156,8 +2152,11 @@ A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */, 586F2BE129F6916F009E6924 /* shadowsocks.h */, 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */, + 58E0E2832A3718CE002E3420 /* URLSessionShadowsocksTransport.swift */, 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */, A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */, + A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */, + A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */, ); path = MullvadTransport; sourceTree = "<group>"; @@ -2873,23 +2872,22 @@ 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */, 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */, 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */, + 58C3FA662A38549D006A450A /* MockFileCache.swift in Sources */, 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */, 58915D652A25F9E20066445B /* TunnelSettingsV2.swift in Sources */, 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */, A9467E7F2A29DEFE000DC21F /* RelayCacheTests.swift in Sources */, - A9467E822A29E0F8000DC21F /* TestsCacheFilePresenter.swift in Sources */, 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */, 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */, - A9467E842A29E69F000DC21F /* CachedTests.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, + 58C3FA682A385C89006A450A /* FileCacheTests.swift in Sources */, 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */, 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */, F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, - A9467E862A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift in Sources */, A9467E802A29E0A6000DC21F /* AddressCacheTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3183,9 +3181,7 @@ 58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */, 58D2240B294C90210029F5F8 /* Cancellable.swift in Sources */, 58D2240C294C90210029F5F8 /* WrappingError.swift in Sources */, - A9A8A8EB2A262AB30086D569 /* Cache.swift in Sources */, - A9467E892A2DD688000DC21F /* ShadowsocksConfiguration.swift in Sources */, - A9467E8B2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift in Sources */, + A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */, 58D2240D294C90210029F5F8 /* CustomErrorDescriptionProtocol.swift in Sources */, 58D2240E294C90210029F5F8 /* Error+Chain.swift in Sources */, 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */, @@ -3232,9 +3228,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5822C0052A3724A800A3A5FB /* ShadowsocksConfiguration.swift in Sources */, A95F86B82A1F547000245DAC /* ShadowsocksProxy.swift in Sources */, A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */, + 5822C0042A3724A800A3A5FB /* ShadowsocksConfigurationCache.swift in Sources */, A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */, + 58E0E2842A3718CE002E3420 /* URLSessionShadowsocksTransport.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 2bdda88201..0906f376ed 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -53,8 +53,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let containerURL = ApplicationConfiguration.containerURL - addressCache = REST.AddressCache(canWriteToCache: true, cacheFolder: containerURL) - addressCache.initCache() + addressCache = REST.AddressCache(canWriteToCache: true, cacheDirectory: containerURL) + addressCache.loadFromFile() proxyFactory = REST.ProxyFactory.makeProxyFactory( transportProvider: { [weak self] in self?.transportMonitor }, @@ -65,7 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy = proxyFactory.createAccountsProxy() devicesProxy = proxyFactory.createDevicesProxy() - let relayCache = RelayCache(cacheFolder: containerURL) + let relayCache = RelayCache(cacheDirectory: containerURL) relayCacheTracker = RelayCacheTracker(relayCache: relayCache, application: application, apiProxy: apiProxy) addressCacheTracker = AddressCacheTracker( @@ -92,7 +92,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession()) - let shadowsocksCache = ShadowsocksConfigurationCache(cacheFolder: containerURL) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) let transportProvider = TransportProvider( urlSessionTransport: urlSessionTransport, relayCache: relayCache, diff --git a/ios/MullvadVPNTests/AddressCacheTests.swift b/ios/MullvadVPNTests/AddressCacheTests.swift index 4b9091d28c..d0e1d823b1 100644 --- a/ios/MullvadVPNTests/AddressCacheTests.swift +++ b/ios/MullvadVPNTests/AddressCacheTests.swift @@ -8,60 +8,57 @@ @testable import MullvadREST import MullvadTypes +import struct Network.IPv4Address import XCTest -final class AddressCacheTests: CachedTests { - var apiEndpoint: AnyIPEndpoint! - - // MARK: Tests Setup - - override class var cacheFileName: String { REST.AddressCache.cacheFileName } - - override func setUpWithError() throws { - try super.setUpWithError() - apiEndpoint = try XCTUnwrap(AnyIPEndpoint(string: "127.0.0.1:80")) - } +final class AddressCacheTests: XCTestCase { + let apiEndpoint: AnyIPEndpoint = .ipv4(IPv4Endpoint(ip: IPv4Address.loopback, port: 80)) // MARK: - Tests func testAddressCacheHasDefaultEndpoint() { - let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory) - XCTAssertEqual(cache.getCurrentEndpoint(), REST.defaultAPIEndpoint) + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .fileNotFound) + ) + XCTAssertEqual(addressCache.getCurrentEndpoint(), REST.defaultAPIEndpoint) } func testSetEndpoints() throws { - let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory) + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .fileNotFound) + ) - cache.setEndpoints([apiEndpoint]) - XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint) + addressCache.setEndpoints([apiEndpoint]) + XCTAssertEqual(addressCache.getCurrentEndpoint(), apiEndpoint) } func testSetEndpointsUpdatesDateWhenSettingSameAddress() throws { - let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory) - cache.setEndpoints([apiEndpoint]) + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .fileNotFound) + ) + addressCache.setEndpoints([apiEndpoint]) - let dateBeforeSettingEndpoint = Date() - cache.setEndpoints([apiEndpoint]) - let dateAfterSettingEndpoint = Date() + let dateBeforeUpdate = addressCache.getLastUpdateDate() + addressCache.setEndpoints([apiEndpoint]) + let dateAfterUpdate = addressCache.getLastUpdateDate() - let dateIntervalRange = dateBeforeSettingEndpoint ... dateAfterSettingEndpoint - XCTAssertTrue(dateIntervalRange.contains(cache.getLastUpdateDate())) + XCTAssertNotEqual(dateBeforeUpdate, dateAfterUpdate) } func testSetEndpointsDoesNotDoAnythingIfSettingEmptyEndpoints() throws { - let didNotWriteToCache = expectation(description: "Did not write to cache") - didNotWriteToCache.isInverted = true + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .fileNotFound) + ) + addressCache.loadFromFile() - cacheFilePresenter.onWriterAction = { - didNotWriteToCache.fulfill() - } - - try withCachefolders { cacheDirectory, _ in - let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory) - cache.setEndpoints([]) - } + let currentEndpoint = addressCache.getCurrentEndpoint() + addressCache.setEndpoints([]) - waitForExpectations(timeout: defaultExpectationTimeout) + XCTAssertEqual(addressCache.getCurrentEndpoint(), currentEndpoint) } func testSetEndpointsOnlyAcceptsTheFirstEndpoint() throws { @@ -71,103 +68,71 @@ final class AddressCacheTests: CachedTests { let firstIPEndpoint = try XCTUnwrap(ipAddresses.first) - try withCachefolders { cacheDirectory, cacheFileURL in - let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory) - cache.setEndpoints(ipAddresses) + let fileCache = MockFileCache<REST.CachedAddresses>() + let addressCache = REST.AddressCache(canWriteToCache: true, fileCache: fileCache) + addressCache.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 testCacheReadsFromCachedFileWithInitCache() throws { - let didReadFromCache = expectation(description: "Cache was read") - cacheFilePresenter.onReaderAction = { - didReadFromCache.fulfill() + let fileState = fileCache.getState() + XCTAssertTrue(fileState.isExists) + guard case let .exists(cachedAddresses) = fileState else { + XCTFail("State is expected to contain cached addresses.") + return } - try withCachefolders { cacheDirectory, cacheFileURL in - let fixedDate = Date() - try prepopulateCache(at: cacheFileURL, fixedDate: fixedDate, with: [apiEndpoint]) - let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory) - cache.initCache() + XCTAssertEqual(cachedAddresses.endpoints.count, 1) + XCTAssertEqual(addressCache.getCurrentEndpoint(), firstIPEndpoint) + } - XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint) - XCTAssertEqual(cache.getLastUpdateDate(), fixedDate) - } + func testCacheReadsFromFile() throws { + let fixedDate = Date() + let addressCache = REST.AddressCache( + canWriteToCache: true, + fileCache: MockFileCache(initialState: .exists( + REST.CachedAddresses(updatedAt: fixedDate, endpoints: [apiEndpoint]) + )) + ) + addressCache.loadFromFile() - waitForExpectations(timeout: defaultExpectationTimeout) + XCTAssertEqual(addressCache.getCurrentEndpoint(), apiEndpoint) + XCTAssertEqual(addressCache.getLastUpdateDate(), fixedDate) } func testCacheWritesToDiskWhenSettingNewEndpoints() throws { - let didWriteToCache = expectation(description: "Cache was written to") - cacheFilePresenter.onWriterAction = { - didWriteToCache.fulfill() - } + let fileCache = MockFileCache<REST.CachedAddresses>() + let addressCache = REST.AddressCache(canWriteToCache: true, fileCache: fileCache) - try withCachefolders { cacheDirectory, cacheFileURL in + XCTAssertEqual(fileCache.getState(), .fileNotFound) + addressCache.setEndpoints([apiEndpoint]) - 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) + let fileState = fileCache.getState() + XCTAssertTrue(fileState.isExists) - XCTAssertEqual(cachedAddress, cache.getCurrentEndpoint()) - XCTAssertEqual(cachedAddresses.updatedAt, cache.getLastUpdateDate()) + guard case let .exists(cachedAddresses) = fileState else { + XCTFail("State is expected to contain cached addresses.") + return } - waitForExpectations(timeout: defaultExpectationTimeout) + XCTAssertEqual(cachedAddresses.endpoints, [addressCache.getCurrentEndpoint()]) + XCTAssertEqual(cachedAddresses.updatedAt, addressCache.getLastUpdateDate()) } func testGetCurrentEndpointReadsFromCacheWhenReadOnly() throws { - let didReadFromCache = expectation(description: "Cache was read") - 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) + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .exists( + REST.CachedAddresses(updatedAt: Date(), endpoints: [apiEndpoint]) + )) + ) + XCTAssertEqual(addressCache.getCurrentEndpoint(), apiEndpoint) } func testGetCurrentEndpointHasDefaultEndpointIfCacheIsEmpty() throws { - let didReadFromCache = expectation(description: "Cache was read") - 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: - + let addressCache = REST.AddressCache( + canWriteToCache: false, + fileCache: MockFileCache(initialState: .exists(REST.CachedAddresses(updatedAt: Date(), endpoints: []))) + ) + addressCache.loadFromFile() -extension AddressCacheTests { - /// 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) + XCTAssertEqual(addressCache.getCurrentEndpoint(), REST.defaultAPIEndpoint) } } diff --git a/ios/MullvadVPNTests/CachedTests.swift b/ios/MullvadVPNTests/CachedTests.swift deleted file mode 100644 index 48b8f9a50d..0000000000 --- a/ios/MullvadVPNTests/CachedTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// CachedTests.swift -// MullvadVPNTests -// -// Created by Marco Nikic on 2023-06-02. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import MullvadREST -import XCTest - -class CachedTests: XCTestCase { - static var testsCacheDirectory: URL! - var cacheFilePresenter: TestsCacheFilePresenter! - let defaultExpectationTimeout = REST.Duration.milliseconds(200).timeInterval - - open class var cacheFileName: String { - XCTFail("Do not use this class directly, inherit from it instead") - return "" - } - - override class func setUp() { - super.setUp() - let temporaryDirectory = FileManager.default.temporaryDirectory - testsCacheDirectory = temporaryDirectory.appendingPathComponent("\(self)") - } - - override func setUpWithError() throws { - try super.setUpWithError() - let cacheFileURL = Self.testsCacheDirectory.appendingPathComponent(Self.cacheFileName) - cacheFilePresenter = TestsCacheFilePresenter(presentedItemURL: cacheFileURL) - NSFileCoordinator.addFilePresenter(cacheFilePresenter) - } - - override func tearDownWithError() throws { - NSFileCoordinator.removeFilePresenter(cacheFilePresenter) - try super.tearDownWithError() - } -} - -extension CachedTests { - /// 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) - } -} diff --git a/ios/MullvadVPNTests/FileCacheTests.swift b/ios/MullvadVPNTests/FileCacheTests.swift new file mode 100644 index 0000000000..dc3b250ed8 --- /dev/null +++ b/ios/MullvadVPNTests/FileCacheTests.swift @@ -0,0 +1,43 @@ +// +// FileCacheTests.swift +// MullvadVPNTests +// +// Created by pronebird on 13/06/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadTypes +import XCTest + +class FileCacheTests: XCTestCase { + var testFileURL: URL! + + override func setUp() { + testFileURL = FileManager.default.temporaryDirectory + .appendingPathComponent("FileCacheTest-\(UUID().uuidString)", isDirectory: true) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: testFileURL) + } + + func testRead() throws { + let stringData = UUID().uuidString + try JSONEncoder().encode(stringData).write(to: testFileURL) + + let fileCache = FileCache<String>(fileURL: testFileURL) + XCTAssertEqual(try fileCache.read(), stringData) + } + + func testWrite() throws { + let fileCache = FileCache<String>(fileURL: testFileURL) + + let stringData = UUID().uuidString + let serializedData = try JSONEncoder().encode(stringData) + + try fileCache.write(stringData) + + XCTAssertEqual(try Data(contentsOf: testFileURL), serializedData) + } +} diff --git a/ios/MullvadVPNTests/MockFileCache.swift b/ios/MullvadVPNTests/MockFileCache.swift new file mode 100644 index 0000000000..0b07c788e2 --- /dev/null +++ b/ios/MullvadVPNTests/MockFileCache.swift @@ -0,0 +1,63 @@ +// +// MockFileCache.swift +// MullvadVPNTests +// +// Created by pronebird on 13/06/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +/// File cache implementation that simulates file state and uses internal lock to synchronize access to it. +final class MockFileCache<Content: Codable & Equatable>: FileCacheProtocol { + private var state: State + private let stateLock = NSLock() + + init(initialState: State = .fileNotFound) { + state = initialState + } + + /// Returns internal state. + func getState() -> State { + stateLock.lock() + defer { stateLock.unlock() } + + return state + } + + func read() throws -> Content { + stateLock.lock() + defer { stateLock.unlock() } + + switch state { + case .fileNotFound: + throw CocoaError(.fileReadNoSuchFile) + case let .exists(content): + return content + } + } + + func write(_ content: Content) throws { + stateLock.lock() + defer { stateLock.unlock() } + + state = .exists(content) + } + + enum State: Equatable { + /// File does not exist yet. + case fileNotFound + + /// File exists with the given contents. + case exists(Content) + + var isExists: Bool { + if case .exists = self { + return true + } else { + return false + } + } + } +} diff --git a/ios/MullvadVPNTests/RelayCacheTests.swift b/ios/MullvadVPNTests/RelayCacheTests.swift index 05f1b33180..1a5d4804d4 100644 --- a/ios/MullvadVPNTests/RelayCacheTests.swift +++ b/ios/MullvadVPNTests/RelayCacheTests.swift @@ -11,51 +11,47 @@ import MullvadTransport @testable import RelayCache import XCTest -final class RelayCacheTests: CachedTests { - override class var cacheFileName: String { RelayCache.cacheFileName } +final class RelayCacheTests: XCTestCase { + func testCanReadCache() throws { + let fileCache = MockFileCache( + initialState: .exists(CachedRelays(relays: .mock(), updatedAt: .distantPast)) + ) + let cache = RelayCache(fileCache: fileCache) + let relays = try XCTUnwrap(cache.read()) - func testReadReadsFromCache() throws { - let didReadFromCache = expectation(description: "Cache was read") - cacheFilePresenter.onReaderAction = { - didReadFromCache.fulfill() - } - - try withCachefolders { cacheDirectory, cacheFileURL in - try prepopulateCache(at: cacheFileURL, fixedDate: .distantPast) - - let cache = RelayCache(cacheFolder: cacheDirectory) - let relays = try cache.read() - - XCTAssertEqual(relays.updatedAt, .distantPast) - } - - waitForExpectations(timeout: defaultExpectationTimeout) + XCTAssertEqual(fileCache.getState(), .exists(relays)) } - func testWriteWritesToCache() throws { - let didWriteToCache = expectation(description: "Cache was written to") - cacheFilePresenter.onWriterAction = { - didWriteToCache.fulfill() - } - - try withCachefolders { cacheDirectory, cacheFileURL in - let cache = RelayCache(cacheFolder: cacheDirectory) - try cache.write(record: CachedRelays(relays: .empty, updatedAt: .distantPast)) + func testCanWriteCache() throws { + let fileCache = MockFileCache( + initialState: .exists(CachedRelays(relays: .mock(), updatedAt: .distantPast)) + ) + let cache = RelayCache(fileCache: fileCache) + let newCachedRelays = CachedRelays(relays: .mock(), updatedAt: Date()) - let cachedContent = try Data(contentsOf: cacheFileURL) - let cachedRelays = try JSONDecoder().decode(CachedRelays.self, from: cachedContent) + try cache.write(record: newCachedRelays) + XCTAssertEqual(fileCache.getState(), .exists(newCachedRelays)) + } - XCTAssertEqual(cachedRelays.updatedAt, .distantPast) - } + func testCanReadPrebundledRelaysWhenNoCacheIsStored() throws { + let fileCache = MockFileCache<CachedRelays>(initialState: .fileNotFound) + let cache = RelayCache(fileCache: fileCache) - waitForExpectations(timeout: defaultExpectationTimeout) + XCTAssertNoThrow(try cache.read()) } } -extension RelayCacheTests { - func prepopulateCache(at cacheFileURL: URL, fixedDate: Date = .init()) throws { - let prepopulatedCache = CachedRelays(relays: .empty, updatedAt: fixedDate) - let encodedCache = try JSONEncoder().encode(prepopulatedCache) - try encodedCache.write(to: cacheFileURL) +private extension REST.ServerRelaysResponse { + static func mock() -> Self { + return REST.ServerRelaysResponse( + locations: [:], + wireguard: REST.ServerWireguardTunnels( + ipv4Gateway: .loopback, + ipv6Gateway: .loopback, + portRanges: [], + relays: [] + ), + bridge: REST.ServerBridges(shadowsocks: [], relays: []) + ) } } diff --git a/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift b/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift deleted file mode 100644 index 3eaa173d39..0000000000 --- a/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ServerRelaysResponse+Mocks.swift -// MullvadVPNTests -// -// Created by Marco Nikic on 2023-06-02. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import MullvadREST -import Network - -extension REST.ServerRelaysResponse { - static var empty: Self { - REST.ServerRelaysResponse(locations: [:], wireguard: .empty, bridge: .empty) - } -} - -extension REST.ServerLocation { - static var empty: Self { - .init(country: "", city: "", latitude: 0, longitude: 0) - } -} - -extension REST.ServerWireguardTunnels { - static var empty: Self { - .init(ipv4Gateway: .loopback, ipv6Gateway: .loopback, portRanges: [], relays: []) - } -} - -extension REST.ServerBridges { - static var empty: Self { - .init(shadowsocks: [], relays: []) - } -} diff --git a/ios/MullvadVPNTests/TestsCacheFilePresenter.swift b/ios/MullvadVPNTests/TestsCacheFilePresenter.swift deleted file mode 100644 index 27a67b6d28..0000000000 --- a/ios/MullvadVPNTests/TestsCacheFilePresenter.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TestsCacheFilePresenter.swift -// MullvadVPNTests -// -// Created by Marco Nikic on 2023-06-02. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class TestsCacheFilePresenter: NSObject, NSFilePresenter { - var presentedItemURL: URL? - let operationQueue: OperationQueue - let dispatchQueue = DispatchQueue(label: "com.MullvadVPN.TestsCacheFilePresenter") - 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) { - onReaderAction?() - reader(nil) - } - - func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) { - onWriterAction?() - writer(nil) - } -} diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 3691fecebc..e3bb561067 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -135,14 +135,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { tunnelLogger = Logger(label: "WireGuard") let containerURL = ApplicationConfiguration.containerURL - let addressCache = REST.AddressCache(canWriteToCache: false, cacheFolder: containerURL) - addressCache.initCache() + let addressCache = REST.AddressCache(canWriteToCache: false, cacheDirectory: containerURL) + addressCache.loadFromFile() - relayCache = RelayCache(cacheFolder: containerURL) + relayCache = RelayCache(cacheDirectory: containerURL) let urlSession = REST.makeURLSession() let urlSessionTransport = URLSessionTransport(urlSession: urlSession) - let shadowsocksCache = ShadowsocksConfigurationCache(cacheFolder: containerURL) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) let transportProvider = TransportProvider( urlSessionTransport: urlSessionTransport, relayCache: relayCache, diff --git a/ios/RelayCache/CachedRelays.swift b/ios/RelayCache/CachedRelays.swift index 75a775c650..499eb9cde3 100644 --- a/ios/RelayCache/CachedRelays.swift +++ b/ios/RelayCache/CachedRelays.swift @@ -10,7 +10,7 @@ import Foundation import MullvadREST /// A struct that represents the relay cache on disk -public struct CachedRelays: Codable { +public struct CachedRelays: Codable, Equatable { /// E-tag returned by server public let etag: String? diff --git a/ios/RelayCache/RelayCache.swift b/ios/RelayCache/RelayCache.swift index ed1cbf4c9e..abce823786 100644 --- a/ios/RelayCache/RelayCache.swift +++ b/ios/RelayCache/RelayCache.swift @@ -10,26 +10,24 @@ import Foundation import MullvadREST import MullvadTypes -public final class RelayCache: Caching { - public typealias CacheType = CachedRelays +public final class RelayCache { + private let fileCache: any FileCacheProtocol<CachedRelays> - /// Cache file location. - public let cacheFileURL: URL - public static let cacheFileName = "relays.json" + /// Designated initializer + public init(cacheDirectory: URL) { + fileCache = FileCache(fileURL: cacheDirectory.appendingPathComponent("relays.json", isDirectory: false)) + } - public init(cacheFolder: URL) { - let cacheFileURL = cacheFolder.appendingPathComponent( - Self.cacheFileName, - isDirectory: false - ) - self.cacheFileURL = cacheFileURL + /// Initializer that accepts a custom FileCache implementation. Used in tests. + init(fileCache: some FileCacheProtocol<CachedRelays>) { + self.fileCache = fileCache } /// Safely read the cache file from disk using file coordinator and fallback to prebundled /// relays in case if the relay cache file is missing. public func read() throws -> CachedRelays { do { - return try readFromDisk() + return try fileCache.read() } catch { if error is DecodingError || (error as? CocoaError)?.code == .fileReadNoSuchFile { return try readPrebundledRelays() @@ -41,16 +39,16 @@ public final class RelayCache: Caching { /// Safely write the cache file on disk using file coordinator. public func write(record: CachedRelays) throws { - try writeToDisk(record) + try fileCache.write(record) } /// Read pre-bundled relays file from disk. private func readPrebundledRelays() throws -> CachedRelays { - guard let prebundledRelaysFileURL = Bundle(for: Self.self) - .url(forResource: "relays", withExtension: "json") else { throw POSIXError(.ENOENT) } + guard let prebundledRelaysFileURL = Bundle(for: Self.self).url(forResource: "relays", withExtension: "json") + else { throw CocoaError(.fileNoSuchFile) } + let data = try Data(contentsOf: prebundledRelaysFileURL) - let relays = try REST.Coding.makeJSONDecoder() - .decode(REST.ServerRelaysResponse.self, from: data) + let relays = try REST.Coding.makeJSONDecoder().decode(REST.ServerRelaysResponse.self, from: data) return CachedRelays( relays: relays, |
