diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-10-31 13:40:56 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-10-31 13:40:56 +0100 |
| commit | af73f0bca5751fb34b58930d657b6fe8c035332c (patch) | |
| tree | 84aae1ad50fa06c74eaa0b55e12db4b936690afe | |
| parent | 87ef07178d9b164bec923d95e80d73d39ec3331b (diff) | |
| parent | 8226d65c42b93b1cf1c8c8246bbe68a236030384 (diff) | |
| download | mullvadvpn-af73f0bca5751fb34b58930d657b6fe8c035332c.tar.xz mullvadvpn-af73f0bca5751fb34b58930d657b6fe8c035332c.zip | |
Merge branch 'address-cache-readonly'
| -rw-r--r-- | ios/MullvadREST/AddressCache.swift | 301 | ||||
| -rw-r--r-- | ios/MullvadREST/Info.plist | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 6 |
3 files changed, 191 insertions, 124 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index 235b02aee2..647c495314 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -12,41 +12,16 @@ import MullvadTypes extension REST { public final class AddressCache { - public static let shared: AddressCache = { - let cacheFilename = "api-ip-address.json" - let cacheDirectoryURL = FileManager.default.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first! - let cacheFileURL = cacheDirectoryURL.appendingPathComponent( - cacheFilename, - isDirectory: false - ) - let prebundledCacheFileURL = Bundle(for: AddressCache.self).url( - forResource: cacheFilename, - withExtension: nil - )! - - return AddressCache( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL - ) - }() - - static var defaultCachedAddresses: CachedAddresses { - return CachedAddresses( - updatedAt: Date(timeIntervalSince1970: 0), - endpoints: [ - REST.defaultAPIEndpoint, - ] - ) - } + public static let shared = AddressCache( + securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, + isReadOnly: false + )! /// Logger. - private let logger = Logger(label: "AddressCache.Store") + private let logger = Logger(label: "AddressCache") /// Memory cache. - private var cachedAddresses: CachedAddresses + private var cachedAddresses: CachedAddresses = defaultCachedAddresses /// Cache file location. private let cacheFileURL: URL @@ -57,50 +32,35 @@ extension REST { /// Lock used for synchronizing access to instance members. private let nslock = NSLock() - /// Designated initializer - public init(cacheFileURL: URL, prebundledCacheFileURL: URL) { - self.cacheFileURL = cacheFileURL - self.prebundledCacheFileURL = prebundledCacheFileURL - - do { - let readResult = try Self.readFromCacheLocationWithFallback( - cacheFileURL: cacheFileURL, - prebundledCacheFileURL: prebundledCacheFileURL, - logger: logger - ) + /// Whether address cache is in readonly mode. + private var isReadOnly: Bool - switch readResult.source { - case .disk: - cachedAddresses = readResult.cachedAddresses + private static let defaultCachedAddresses = CachedAddresses( + updatedAt: Date(timeIntervalSince1970: 0), + endpoints: [REST.defaultAPIEndpoint] + ) - case .bundle: - var addresses = readResult.cachedAddresses - addresses.endpoints.shuffle() - cachedAddresses = addresses + /// Designated initializer. + public init?(securityGroupIdentifier: String, isReadOnly: Bool) { + let cacheFilename = "api-ip-address.json" - logger.debug("Persist address list read from bundle.") + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: securityGroupIdentifier + ), let prebundledCacheFileURL = Bundle(for: AddressCache.self).url( + forResource: cacheFilename, + withExtension: nil + ) else { return nil } - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to persist address cache after reading it from bundle." - ) - } - } + let cacheFileURL = containerURL.appendingPathComponent( + cacheFilename, + isDirectory: false + ) - logger.debug( - """ - Initialized cache from \(readResult.source) with \ - \(cachedAddresses.endpoints.count) endpoint(s). - """ - ) - } catch { - logger.debug("Initialized cache with default API endpoint.") + self.cacheFileURL = cacheFileURL + self.prebundledCacheFileURL = prebundledCacheFileURL + self.isReadOnly = isReadOnly - cachedAddresses = Self.defaultCachedAddresses - } + initCache() } public func getCurrentEndpoint() -> AnyIPEndpoint { @@ -122,20 +82,25 @@ extension REST { cachedAddresses.endpoints.removeFirst() cachedAddresses.endpoints.append(failedEndpoint) + if isReadOnly { + refreshAddresses() + } + currentEndpoint = cachedAddresses.endpoints.first! - logger - .debug( - "Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)" - ) + logger.debug( + "Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)" + ) - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to write address cache after selecting next endpoint." - ) + if !isReadOnly { + do { + try writeToDisk() + } catch { + logger.error( + error: error, + message: "Failed to write address cache after selecting next endpoint." + ) + } } return currentEndpoint @@ -168,13 +133,15 @@ extension REST { ) } - do { - try writeToDisk() - } catch { - logger.error( - error: error, - message: "Failed to write address cache after setting new endpoints." - ) + if !isReadOnly { + do { + try writeToDisk() + } catch { + logger.error( + error: error, + message: "Failed to write address cache after setting new endpoints." + ) + } } } @@ -187,20 +154,53 @@ extension REST { // MARK: - Private - private static func readFromCacheLocationWithFallback( - cacheFileURL: URL, - prebundledCacheFileURL: URL, - logger: Logger - ) throws -> ReadResult { + private func initCache() { do { - let readResult = ReadResult( - cachedAddresses: try readFromCacheLocation(cacheFileURL), - source: .disk - ) + try initCacheInner() + } catch { + logger.debug("Initialized cache with default API endpoint.") + + cachedAddresses = Self.defaultCachedAddresses + } + } + + private func initCacheInner() throws { + let readResult = try readFromCacheLocationWithFallback() + + switch readResult.source { + case .disk: + cachedAddresses = readResult.cachedAddresses + + case .bundle: + var addresses = readResult.cachedAddresses + addresses.endpoints.shuffle() + cachedAddresses = addresses + + if !isReadOnly { + logger.debug("Persist address list read from bundle.") + + do { + try writeToDisk() + } catch { + logger.error( + error: error, + message: "Failed to persist address cache after reading it from bundle." + ) + } + } + } - try checkReadResultContainsEndpoints(readResult) + logger.debug( + """ + Initialized cache from \(readResult.source) with \ + \(cachedAddresses.endpoints.count) endpoint(s). + """ + ) + } - return readResult + private func readFromCacheLocationWithFallback() throws -> ReadResult { + do { + return try readFromCacheLocation() } catch { logger.error( error: error, @@ -208,14 +208,7 @@ extension REST { ) do { - let readResult = ReadResult( - cachedAddresses: try readFromBundle(prebundledCacheFileURL), - source: .bundle - ) - - try checkReadResultContainsEndpoints(readResult) - - return readResult + return try readFromBundle() } catch { logger.error( error: error, @@ -227,41 +220,101 @@ extension REST { } } - private static func checkReadResultContainsEndpoints(_ readResult: ReadResult) throws { - if readResult.cachedAddresses.endpoints.isEmpty { - throw EmptyCacheError(source: readResult.source) + private func readFromCacheLocation() throws -> ReadResult { + var result: Result<ReadResult, Swift.Error>? + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + let accessor = { (fileURL: URL) in + result = Result { + let data = try Data(contentsOf: fileURL) + let cachedAddresses = try JSONDecoder().decode(CachedAddresses.self, from: data) + + if cachedAddresses.endpoints.isEmpty { + throw EmptyCacheError(source: .disk) + } + + return ReadResult(cachedAddresses: cachedAddresses, source: .disk) + } } - } - private static func readFromCacheLocation(_ cacheFileURL: URL) throws -> CachedAddresses { - let data = try Data(contentsOf: cacheFileURL) + var error: NSError? + fileCoordinator.coordinate( + readingItemAt: cacheFileURL, + options: .withoutChanges, + error: &error, + byAccessor: accessor + ) + + if let error = error { + result = .failure(error) + } - return try JSONDecoder().decode(CachedAddresses.self, from: data) + return try result!.get() } - private static func readFromBundle(_ prebundledCacheFileURL: URL) throws - -> CachedAddresses - { + private func readFromBundle() throws -> ReadResult { let data = try Data(contentsOf: prebundledCacheFileURL) let endpoints = try JSONDecoder().decode([AnyIPEndpoint].self, from: data) - return CachedAddresses( + let cachedAddresses = CachedAddresses( updatedAt: Date(timeIntervalSince1970: 0), endpoints: endpoints ) + + if cachedAddresses.endpoints.isEmpty { + throw EmptyCacheError(source: .bundle) + } + + return ReadResult(cachedAddresses: cachedAddresses, source: .bundle) } private func writeToDisk() throws { - let cacheDirectoryURL = cacheFileURL.deletingLastPathComponent() + precondition(!isReadOnly) - try? FileManager.default.createDirectory( - at: cacheDirectoryURL, - withIntermediateDirectories: true, - attributes: nil + var result: Result<Void, Swift.Error>? + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + let accessor = { (fileURL: URL) in + result = Result { + let data = try JSONEncoder().encode(self.cachedAddresses) + try data.write(to: fileURL) + } + } + + var error: NSError? + fileCoordinator.coordinate( + writingItemAt: cacheFileURL, + options: [.forReplacing], + error: &error, + byAccessor: accessor ) - let data = try JSONEncoder().encode(cachedAddresses) - try data.write(to: cacheFileURL, options: .atomic) + if let error = error { + result = .failure(error) + } + + return try result!.get() + } + + private func refreshAddresses() { + do { + let readResult = try readFromCacheLocation() + var newCachedAddresses = readResult.cachedAddresses + + guard Set(newCachedAddresses.endpoints) != Set(cachedAddresses.endpoints) + else { return } + + // Move current endpoint to the top of the list + let currentEndpoint = cachedAddresses.endpoints.first! + if let index = newCachedAddresses.endpoints.firstIndex(of: currentEndpoint) { + newCachedAddresses.endpoints.remove(at: index) + newCachedAddresses.endpoints.insert(currentEndpoint, at: 0) + } + + cachedAddresses = newCachedAddresses + } catch { + logger.error(error: error, message: "Failed to refresh address cache from disk.") + } } } diff --git a/ios/MullvadREST/Info.plist b/ios/MullvadREST/Info.plist new file mode 100644 index 0000000000..65663845f1 --- /dev/null +++ b/ios/MullvadREST/Info.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ApplicationSecurityGroupIdentifier</key> + <string>$(SECURITY_GROUP_IDENTIFIER)</string> +</dict> +</plist> diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index e0662fb177..4335c710f4 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; }; 584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; }; 584F99212902CF35001F858D /* libMullvadTypes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 581943F128F8014500B0CB5E /* libMullvadTypes.a */; }; + 58505FFA290A7F0F00118C23 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 5856AD582902BE1A008E5127 /* PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */; }; 5856AD592902BE1A008E5127 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */; }; @@ -585,6 +586,7 @@ 582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; }; 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; }; 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; }; + 582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; }; 5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; }; 583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; }; @@ -928,6 +930,7 @@ 06799ABD28F98E1D00ACD94E /* MullvadREST */ = { isa = PBXGroup; children = ( + 582FFA82290A84E700895745 /* Info.plist */, 062B45A228FD4C0F00746E77 /* Assets */, 06799ABE28F98E1D00ACD94E /* MullvadREST.h */, 06AC114128F8413A0037AF9A /* AddressCache.swift */, @@ -1929,6 +1932,7 @@ 06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */, 06799AED28F98E4800ACD94E /* RESTTransportRegistry.swift in Sources */, 06799AE528F98E4800ACD94E /* HTTP.swift in Sources */, + 58505FFA290A7F0F00118C23 /* ApplicationConfiguration.swift in Sources */, 06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */, 06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */, 06799AF028F98E4800ACD94E /* REST.swift in Sources */, @@ -2416,6 +2420,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MullvadREST/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Mullvad VPN AB. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2448,6 +2453,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MullvadREST/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Mullvad VPN AB. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( |
