diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2023-05-30 15:52:26 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-06-13 16:25:10 +0200 |
| commit | e84905793ac54c4768a416f76dd4589e1f66fcf1 (patch) | |
| tree | 7bfffbdb6520bce6fd42c1702fbb8c7cd4e2537d | |
| parent | 7448427f811daf99dd4ab9d4f3676f354beff245 (diff) | |
| download | mullvadvpn-e84905793ac54c4768a416f76dd4589e1f66fcf1.tar.xz mullvadvpn-e84905793ac54c4768a416f76dd4589e1f66fcf1.zip | |
Cache the last used shadowsocks configuration and reuse it subsequently
| -rw-r--r-- | ios/MullvadREST/AddressCache.swift | 89 | ||||
| -rw-r--r-- | ios/MullvadTransport/TransportProvider.swift | 49 | ||||
| -rw-r--r-- | ios/MullvadTransport/URLSessionTransport.swift | 7 | ||||
| -rw-r--r-- | ios/MullvadTypes/Cache.swift | 48 | ||||
| -rw-r--r-- | ios/MullvadTypes/ShadowsocksConfiguration.swift | 24 | ||||
| -rw-r--r-- | ios/MullvadTypes/ShadowsocksConfigurationCache.swift | 57 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 41 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 15 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/AddressCacheTests.swift (renamed from ios/MullvadRESTTests/AddressCacheTests.swift) | 72 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/CachedTests.swift | 56 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/RelayCacheTests.swift | 61 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift | 34 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/TestsCacheFilePresenter.swift | 35 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 14 | ||||
| -rw-r--r-- | ios/RelayCache/CachedRelays.swift | 6 | ||||
| -rw-r--r-- | ios/RelayCache/RelayCache.swift | 81 |
16 files changed, 456 insertions, 233 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index bc54f306b4..0f449f58ff 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -11,15 +11,24 @@ import MullvadLogging import MullvadTypes extension REST { - public final class AddressCache { + 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 /// Logger. private let logger = Logger(label: "AddressCache") /// Memory cache. - private var cachedAddresses: CachedAddresses = defaultCachedAddresses + var cache: CachedAddresses = defaultCachedAddresses /// Cache file location. - private let cacheFileURL: URL + public let cacheFileURL: URL /// Lock used for synchronizing access to instance members. private let cacheLock = NSLock() @@ -28,7 +37,7 @@ extension REST { private let canWriteToCache: Bool /// The name of the cache file on disk - internal static let cacheFileName = "api-ip-address.json" + public static let cacheFileName = "api-ip-address.json" /// The default set of endpoints to use as a fallback mechanism private static let defaultCachedAddresses = CachedAddresses( @@ -49,8 +58,6 @@ extension REST { self.cacheFileURL = cacheFileURL self.canWriteToCache = canWriteToCache - - initCache() } /// Returns the latest available endpoint @@ -60,14 +67,14 @@ extension REST { public func getCurrentEndpoint() -> AnyIPEndpoint { cacheLock.lock() defer { cacheLock.unlock() } - var currentEndpoint = cachedAddresses.endpoints.first ?? REST.defaultAPIEndpoint + 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 { - cachedAddresses = try readFromCache() - if let firstEndpoint = cachedAddresses.endpoints.first { + cache = try readFromDisk() + if let firstEndpoint = cache.endpoints.first { currentEndpoint = firstEndpoint } } catch { @@ -92,10 +99,10 @@ extension REST { defer { cacheLock.unlock() } guard let firstEndpoint = endpoints.first else { return } - if Set(cachedAddresses.endpoints) == Set(endpoints) { - cachedAddresses.updatedAt = Date() + if Set(cache.endpoints) == Set(endpoints) { + cache.updatedAt = Date() } else { - cachedAddresses = CachedAddresses( + cache = CachedAddresses( updatedAt: Date(), endpoints: [firstEndpoint] ) @@ -103,7 +110,7 @@ extension REST { if canWriteToCache { do { - try writeToCache() + try writeToDisk(cache) } catch { logger.error( error: error, @@ -120,69 +127,21 @@ extension REST { cacheLock.lock() defer { cacheLock.unlock() } - return cachedAddresses.updatedAt + return cache.updatedAt } - // MARK: - Private API - /// Initializes the cache by reading the a cached file from disk /// /// If no cache file is present, a default API endpoint will be selected instead - private func initCache() { + public func initCache() { // The first time the application is ran, this statement will fail as there is no cache. This is fine. // The cache will be filled when either `getCurrentEndpoint` or `setEndpoints()` are called. do { - cachedAddresses = try readFromCache() + cache = try readFromDisk() } catch { logger.debug("Initialized cache with default API endpoint.") - cachedAddresses = Self.defaultCachedAddresses - } - } - - /// Reads the cache file from disk - /// - /// - Returns: A list of cached API endpoints in a `CachedAddresses` form - private func readFromCache() throws -> CachedAddresses { - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - - let result = try fileCoordinator - .coordinate(readingItemAt: cacheFileURL, options: [.withoutChanges]) { file in - let data = try Data(contentsOf: file) - let cachedAddresses = try JSONDecoder().decode(CachedAddresses.self, from: data) - - if cachedAddresses.endpoints.isEmpty { - throw EmptyCacheError() - } - - return cachedAddresses - } - - return result - } - - /// Writes the cache file to the disk - private func writeToCache() throws { - precondition(canWriteToCache == true) - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - - try fileCoordinator.coordinate(writingItemAt: cacheFileURL, options: [.forReplacing]) { file in - let data = try JSONEncoder().encode(self.cachedAddresses) - try data.write(to: file) + cache = Self.defaultCachedAddresses } } } - - struct CachedAddresses: Codable { - /// Date when the cached addresses were last updated. - var updatedAt: Date - - /// API endpoints. - var endpoints: [AnyIPEndpoint] - } - - struct EmptyCacheError: LocalizedError { - var errorDescription: String? { - return "Address cache file does not contain any API addresses." - } - } } diff --git a/ios/MullvadTransport/TransportProvider.swift b/ios/MullvadTransport/TransportProvider.swift index 821ea3d059..93274e9edb 100644 --- a/ios/MullvadTransport/TransportProvider.swift +++ b/ios/MullvadTransport/TransportProvider.swift @@ -18,11 +18,18 @@ public final class TransportProvider: RESTTransportProvider { private let relayCache: RelayCache private let logger = Logger(label: "TransportProvider") private let addressCache: REST.AddressCache + private let shadowsocksCache: ShadowsocksConfigurationCache - public init(urlSessionTransport: URLSessionTransport, relayCache: RelayCache, addressCache: REST.AddressCache) { + public init( + urlSessionTransport: URLSessionTransport, + relayCache: RelayCache, + addressCache: REST.AddressCache, + shadowsocksCache: ShadowsocksConfigurationCache + ) { self.urlSessionTransport = urlSessionTransport self.relayCache = relayCache self.addressCache = addressCache + self.shadowsocksCache = shadowsocksCache } public func transport() -> RESTTransport? { @@ -31,22 +38,12 @@ public final class TransportProvider: RESTTransportProvider { public func shadowsocksTransport() -> RESTTransport? { do { - let cachedRelays = try relayCache.read() - let shadowsocksConfiguration = RelaySelector.getShadowsocksTCPBridge(relays: cachedRelays.relays) - let shadowsocksBridgeRelay = RelaySelector.getShadowsocksRelay(relays: cachedRelays.relays) - - guard let shadowsocksConfiguration, - let shadowsocksBridgeRelay - else { - logger.error("Could not get shadow socks bridge information.") - return nil - } + let shadowsocksConfiguration = try shadowsocksConfiguration() let shadowsocksURLSession = urlSessionTransport.urlSession let shadowsocksTransport = URLSessionShadowsocksTransport( urlSession: shadowsocksURLSession, shadowsocksConfiguration: shadowsocksConfiguration, - shadowsocksBridgeRelay: shadowsocksBridgeRelay, addressCache: addressCache ) @@ -56,4 +53,32 @@ public final class TransportProvider: RESTTransportProvider { } 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 + private func shadowsocksConfiguration() throws -> ShadowsocksConfiguration { + // If a previous shadowsocks configuration was in cache, return it directly + if let configuration = shadowsocksCache.configuration { + return configuration + } + + // 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. + let cachedRelays = try relayCache.read() + let bridgeAddress = RelaySelector.getShadowsocksRelay(relays: cachedRelays.relays)?.ipv4AddrIn + let bridgeConfiguration = RelaySelector.getShadowsocksTCPBridge(relays: cachedRelays.relays) + + guard let bridgeAddress, let bridgeConfiguration else { throw POSIXError(.ENOENT) } + + let newConfiguration = ShadowsocksConfiguration( + bridgeAddress: bridgeAddress, + bridgePort: bridgeConfiguration.port, + password: bridgeConfiguration.password, + cipher: bridgeConfiguration.cipher + ) + shadowsocksCache.configuration = newConfiguration + return newConfiguration + } } diff --git a/ios/MullvadTransport/URLSessionTransport.swift b/ios/MullvadTransport/URLSessionTransport.swift index 49bc131895..8556f812ca 100644 --- a/ios/MullvadTransport/URLSessionTransport.swift +++ b/ios/MullvadTransport/URLSessionTransport.swift @@ -47,8 +47,7 @@ public final class URLSessionShadowsocksTransport: RESTTransport { public init( urlSession: URLSession, - shadowsocksConfiguration: REST.ServerShadowsocks, - shadowsocksBridgeRelay: REST.BridgeRelay, + shadowsocksConfiguration: ShadowsocksConfiguration, addressCache: REST.AddressCache ) { self.urlSession = urlSession @@ -57,8 +56,8 @@ public final class URLSessionShadowsocksTransport: RESTTransport { shadowsocksProxy = ShadowsocksProxy( forwardAddress: apiAddress.ip, forwardPort: apiAddress.port, - bridgeAddress: shadowsocksBridgeRelay.ipv4AddrIn, - bridgePort: shadowsocksConfiguration.port, + bridgeAddress: shadowsocksConfiguration.bridgeAddress, + bridgePort: shadowsocksConfiguration.bridgePort, password: shadowsocksConfiguration.password, cipher: shadowsocksConfiguration.cipher ) diff --git a/ios/MullvadTypes/Cache.swift b/ios/MullvadTypes/Cache.swift new file mode 100644 index 0000000000..6238e6c947 --- /dev/null +++ b/ios/MullvadTypes/Cache.swift @@ -0,0 +1,48 @@ +// +// 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/ShadowsocksConfiguration.swift b/ios/MullvadTypes/ShadowsocksConfiguration.swift new file mode 100644 index 0000000000..0a145f0976 --- /dev/null +++ b/ios/MullvadTypes/ShadowsocksConfiguration.swift @@ -0,0 +1,24 @@ +// +// ShadowsocksConfiguration.swift +// MullvadTransport +// +// Created by Marco Nikic on 2023-06-05. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Network + +public struct ShadowsocksConfiguration: Codable { + public let bridgeAddress: IPv4Address + public let bridgePort: UInt16 + public let password: String + public let cipher: String + + public init(bridgeAddress: IPv4Address, bridgePort: UInt16, password: String, cipher: String) { + self.bridgeAddress = bridgeAddress + self.bridgePort = bridgePort + self.password = password + self.cipher = cipher + } +} diff --git a/ios/MullvadTypes/ShadowsocksConfigurationCache.swift b/ios/MullvadTypes/ShadowsocksConfigurationCache.swift new file mode 100644 index 0000000000..d8f035a8db --- /dev/null +++ b/ios/MullvadTypes/ShadowsocksConfigurationCache.swift @@ -0,0 +1,57 @@ +// +// 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 03d65f838e..89c6598405 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -386,6 +386,13 @@ A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */; }; A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; }; 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, ); }; }; @@ -393,8 +400,8 @@ 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 */; }; A9B2CF722A1F64CD0013CC6C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; - A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */; }; A9D99BA52A1F808900DE27D3 /* RelayCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; }; @@ -1109,10 +1116,17 @@ 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; @@ -1359,6 +1373,7 @@ 584D26BE270C550B004EA533 /* AnyIPAddress.swift */, 586A951329013235007BAF2B /* AnyIPEndpoint.swift */, 06AC113628F83FD70037AF9A /* Cancellable.swift */, + A9A8A8EA2A262AB30086D569 /* Cache.swift */, 58E511E328DDDE8900B0BCDE /* CustomErrorDescriptionProtocol.swift */, 586168682976F6BD00EF8598 /* DisplayError.swift */, 58E511EA28DDE18400B0BCDE /* Error+Chain.swift */, @@ -1380,6 +1395,8 @@ 5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */, 581DA2722A1E227D0046ED47 /* RESTTypes.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, + A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */, + A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */, 58E511E028DDB7F100B0BCDE /* WrappingError.swift */, ); path = MullvadTypes; @@ -1863,14 +1880,20 @@ 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( + 58B0A2A4238EE67E00BC001D /* Info.plist */, + F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, + 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */, + A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, + A9467E832A29E69F000DC21F /* CachedTests.swift */, 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */, 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */, 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */, - 58B0A2A4238EE67E00BC001D /* Info.plist */, - F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, + A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, + A9467E852A29E9F3000DC21F /* ServerRelaysResponse+Mocks.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, + A9467E812A29E0F8000DC21F /* TestsCacheFilePresenter.swift */, ); path = MullvadVPNTests; sourceTree = "<group>"; @@ -2120,7 +2143,6 @@ 58FBFBE7291622580020E046 /* MullvadRESTTests */ = { isa = PBXGroup; children = ( - A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, 58FBFBF0291630700020E046 /* DurationTests.swift */, 58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */, A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */, @@ -2131,10 +2153,10 @@ A97F1F422A1F4E1A00ECEFDE /* MullvadTransport */ = { isa = PBXGroup; children = ( + A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */, 586F2BE129F6916F009E6924 /* shadowsocks.h */, 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */, 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */, - A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */, A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */, ); path = MullvadTransport; @@ -2854,16 +2876,21 @@ 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 */, 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; }; @@ -3156,6 +3183,9 @@ 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 */, 58D2240D294C90210029F5F8 /* CustomErrorDescriptionProtocol.swift in Sources */, 58D2240E294C90210029F5F8 /* Error+Chain.swift in Sources */, 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */, @@ -3192,7 +3222,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A9CF11FD2A0518E7001D9565 /* AddressCacheTests.swift in Sources */, A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, 58FBFBF1291630700020E046 /* DurationTests.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 0272c27b8a..2bdda88201 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -10,6 +10,7 @@ import BackgroundTasks import MullvadLogging import MullvadREST import MullvadTransport +import MullvadTypes import Operations import RelayCache import StoreKit @@ -50,7 +51,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD logger = Logger(label: "AppDelegate") - addressCache = REST.AddressCache(canWriteToCache: true, cacheFolder: ApplicationConfiguration.containerURL) + let containerURL = ApplicationConfiguration.containerURL + + addressCache = REST.AddressCache(canWriteToCache: true, cacheFolder: containerURL) + addressCache.initCache() proxyFactory = REST.ProxyFactory.makeProxyFactory( transportProvider: { [weak self] in self?.transportMonitor }, @@ -61,10 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy = proxyFactory.createAccountsProxy() devicesProxy = proxyFactory.createDevicesProxy() - let relayCache = RelayCache( - securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier - )! - + let relayCache = RelayCache(cacheFolder: containerURL) relayCacheTracker = RelayCacheTracker(relayCache: relayCache, application: application, apiProxy: apiProxy) addressCacheTracker = AddressCacheTracker( @@ -91,10 +92,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession()) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheFolder: containerURL) let transportProvider = TransportProvider( urlSessionTransport: urlSessionTransport, relayCache: relayCache, - addressCache: addressCache + addressCache: addressCache, + shadowsocksCache: shadowsocksCache ) transportMonitor = TransportMonitor( diff --git a/ios/MullvadRESTTests/AddressCacheTests.swift b/ios/MullvadVPNTests/AddressCacheTests.swift index 85387adee7..4b9091d28c 100644 --- a/ios/MullvadRESTTests/AddressCacheTests.swift +++ b/ios/MullvadVPNTests/AddressCacheTests.swift @@ -10,36 +10,19 @@ import MullvadTypes import XCTest -final class AddressCacheTests: XCTestCase { - static var testsCacheDirectory: URL! +final class AddressCacheTests: CachedTests { var apiEndpoint: AnyIPEndpoint! - var cacheFilePresenter: AddressCacheFilePresenter! - let defaultExpectationTimeout = REST.Duration.milliseconds(200).timeInterval // MARK: Tests Setup - override class func setUp() { - super.setUp() - let temporaryDirectory = FileManager.default.temporaryDirectory - testsCacheDirectory = temporaryDirectory.appendingPathComponent("AddressCacheTests") - } + 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")) - let cacheFileURL = Self.testsCacheDirectory.appendingPathComponent(REST.AddressCache.cacheFileName) - cacheFilePresenter = AddressCacheFilePresenter(presentedItemURL: cacheFileURL) - NSFileCoordinator.addFilePresenter(cacheFilePresenter) - } - - override func tearDownWithError() throws { - NSFileCoordinator.removeFilePresenter(cacheFilePresenter) - try super.tearDownWithError() } - // MARK: - - - // MARK: Tests + // MARK: - Tests func testAddressCacheHasDefaultEndpoint() { let cache = REST.AddressCache(canWriteToCache: false, cacheFolder: Self.testsCacheDirectory) @@ -100,7 +83,7 @@ final class AddressCacheTests: XCTestCase { } } - func testCacheReadsFromCachedFileAtInit() throws { + func testCacheReadsFromCachedFileWithInitCache() throws { let didReadFromCache = expectation(description: "Cache was read") cacheFilePresenter.onReaderAction = { didReadFromCache.fulfill() @@ -110,6 +93,7 @@ final class AddressCacheTests: XCTestCase { let fixedDate = Date() try prepopulateCache(at: cacheFileURL, fixedDate: fixedDate, with: [apiEndpoint]) let cache = REST.AddressCache(canWriteToCache: true, cacheFolder: cacheDirectory) + cache.initCache() XCTAssertEqual(cache.getCurrentEndpoint(), apiEndpoint) XCTAssertEqual(cache.getLastUpdateDate(), fixedDate) @@ -141,8 +125,6 @@ final class AddressCacheTests: XCTestCase { func testGetCurrentEndpointReadsFromCacheWhenReadOnly() throws { let didReadFromCache = expectation(description: "Cache was read") - // Cache will be read from twice. Once during init, once when getting current endpoint - didReadFromCache.expectedFulfillmentCount = 2 cacheFilePresenter.onReaderAction = { didReadFromCache.fulfill() } @@ -159,8 +141,6 @@ final class AddressCacheTests: XCTestCase { func testGetCurrentEndpointHasDefaultEndpointIfCacheIsEmpty() throws { let didReadFromCache = expectation(description: "Cache was read") - // Cache will be read from twice. Once during init, once when getting current endpoint - didReadFromCache.expectedFulfillmentCount = 2 cacheFilePresenter.onReaderAction = { didReadFromCache.fulfill() } @@ -179,20 +159,6 @@ final class AddressCacheTests: XCTestCase { // MARK: - extension AddressCacheTests { - /// 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) - } - /// Populates a JSON cache file containing a `Date` and `[AnyIPEndpoint]` /// /// - Parameters: @@ -205,31 +171,3 @@ extension AddressCacheTests { try encodedCache.write(to: cacheFileURL) } } - -class AddressCacheFilePresenter: NSObject, NSFilePresenter { - var presentedItemURL: URL? - let operationQueue: OperationQueue - let dispatchQueue = DispatchQueue(label: "com.MullvadVPN.AddressCacheTests") - 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) { - print(#function) - onReaderAction?() - reader(nil) - } - - func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) { - print(#function) - onWriterAction?() - writer(nil) - } -} diff --git a/ios/MullvadVPNTests/CachedTests.swift b/ios/MullvadVPNTests/CachedTests.swift new file mode 100644 index 0000000000..48b8f9a50d --- /dev/null +++ b/ios/MullvadVPNTests/CachedTests.swift @@ -0,0 +1,56 @@ +// +// 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/RelayCacheTests.swift b/ios/MullvadVPNTests/RelayCacheTests.swift new file mode 100644 index 0000000000..05f1b33180 --- /dev/null +++ b/ios/MullvadVPNTests/RelayCacheTests.swift @@ -0,0 +1,61 @@ +// +// RelayCacheTests.swift +// MullvadVPNTests +// +// Created by Marco Nikic on 2023-06-02. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +import MullvadTransport +@testable import RelayCache +import XCTest + +final class RelayCacheTests: CachedTests { + override class var cacheFileName: String { RelayCache.cacheFileName } + + 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) + } + + 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)) + + let cachedContent = try Data(contentsOf: cacheFileURL) + let cachedRelays = try JSONDecoder().decode(CachedRelays.self, from: cachedContent) + + XCTAssertEqual(cachedRelays.updatedAt, .distantPast) + } + + waitForExpectations(timeout: defaultExpectationTimeout) + } +} + +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) + } +} diff --git a/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift b/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift new file mode 100644 index 0000000000..3eaa173d39 --- /dev/null +++ b/ios/MullvadVPNTests/ServerRelaysResponse+Mocks.swift @@ -0,0 +1,34 @@ +// +// 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 new file mode 100644 index 0000000000..27a67b6d28 --- /dev/null +++ b/ios/MullvadVPNTests/TestsCacheFilePresenter.swift @@ -0,0 +1,35 @@ +// +// 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 820b59e7e6..3691fecebc 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -65,9 +65,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { private var tunnelStartupFailureRecoveryTimer: DispatchSourceTimer? /// Relay cache. - private let relayCache = RelayCache( - securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier - )! + private let relayCache: RelayCache /// Current selector result. private var selectorResult: RelaySelectorResult? @@ -136,14 +134,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger = Logger(label: "PacketTunnelProvider") tunnelLogger = Logger(label: "WireGuard") - let addressCache = REST.AddressCache(canWriteToCache: false, cacheFolder: ApplicationConfiguration.containerURL) + let containerURL = ApplicationConfiguration.containerURL + let addressCache = REST.AddressCache(canWriteToCache: false, cacheFolder: containerURL) + addressCache.initCache() + + relayCache = RelayCache(cacheFolder: containerURL) let urlSession = REST.makeURLSession() let urlSessionTransport = URLSessionTransport(urlSession: urlSession) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheFolder: containerURL) let transportProvider = TransportProvider( urlSessionTransport: urlSessionTransport, relayCache: relayCache, - addressCache: addressCache + addressCache: addressCache, + shadowsocksCache: shadowsocksCache ) let proxyFactory = REST.ProxyFactory.makeProxyFactory( diff --git a/ios/RelayCache/CachedRelays.swift b/ios/RelayCache/CachedRelays.swift index 6ff9c36091..75a775c650 100644 --- a/ios/RelayCache/CachedRelays.swift +++ b/ios/RelayCache/CachedRelays.swift @@ -12,13 +12,13 @@ import MullvadREST /// A struct that represents the relay cache on disk public struct CachedRelays: Codable { /// E-tag returned by server - public var etag: String? + public let etag: String? /// The relay list stored within the cache entry - public var relays: REST.ServerRelaysResponse + public let relays: REST.ServerRelaysResponse /// The date when this cache was last updated - public var updatedAt: Date + public let updatedAt: Date public init(etag: String? = nil, relays: REST.ServerRelaysResponse, updatedAt: Date) { self.etag = etag diff --git a/ios/RelayCache/RelayCache.swift b/ios/RelayCache/RelayCache.swift index 3c5ceb3a20..ed1cbf4c9e 100644 --- a/ios/RelayCache/RelayCache.swift +++ b/ios/RelayCache/RelayCache.swift @@ -8,30 +8,28 @@ import Foundation import MullvadREST +import MullvadTypes -public final class RelayCache { - /// Cache file location. - let cacheFileURL: URL - - /// Location of pre-bundled relays file. - let prebundledRelaysFileURL: URL +public final class RelayCache: Caching { + public typealias CacheType = CachedRelays - /// Initialize cache with default cache file location in app group container. - public init?(securityGroupIdentifier: String) { - guard let containerURL = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: securityGroupIdentifier - ), let prebundledRelaysFileURL = Bundle(for: Self.self) - .url(forResource: "relays", withExtension: "json") else { return nil } + /// Cache file location. + public let cacheFileURL: URL + public static let cacheFileName = "relays.json" - cacheFileURL = containerURL.appendingPathComponent("relays.json", isDirectory: false) - self.prebundledRelaysFileURL = prebundledRelaysFileURL + public init(cacheFolder: URL) { + let cacheFileURL = cacheFolder.appendingPathComponent( + Self.cacheFileName, + isDirectory: false + ) + self.cacheFileURL = cacheFileURL } /// 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 readDiskCache() + return try readFromDisk() } catch { if error is DecodingError || (error as? CocoaError)?.code == .fileReadNoSuchFile { return try readPrebundledRelays() @@ -43,60 +41,13 @@ public final class RelayCache { /// Safely write the cache file on disk using file coordinator. public func write(record: CachedRelays) throws { - var result: Result<Void, Error>? - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - - let accessor = { (fileURLForWriting: URL) in - result = Result { - let data = try JSONEncoder().encode(record) - try data.write(to: fileURLForWriting) - } - } - - var error: NSError? - fileCoordinator.coordinate( - writingItemAt: cacheFileURL, - options: [.forReplacing], - error: &error, - byAccessor: accessor - ) - - if let error { - result = .failure(error) - } - - try result?.get() - } - - /// Safely read the cache file from disk using file coordinator. - private func readDiskCache() throws -> CachedRelays { - var result: Result<CachedRelays, Error>? - let fileCoordinator = NSFileCoordinator(filePresenter: nil) - - let accessor = { (fileURLForReading: URL) in - result = Result { - let data = try Data(contentsOf: fileURLForReading) - return try JSONDecoder().decode(CachedRelays.self, from: data) - } - } - - var error: NSError? - fileCoordinator.coordinate( - readingItemAt: cacheFileURL, - options: [.withoutChanges], - error: &error, - byAccessor: accessor - ) - - if let error { - result = .failure(error) - } - - return try result!.get() + try writeToDisk(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) } let data = try Data(contentsOf: prebundledRelaysFileURL) let relays = try REST.Coding.makeJSONDecoder() .decode(REST.ServerRelaysResponse.self, from: data) |
