summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-10-31 13:40:56 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-10-31 13:40:56 +0100
commitaf73f0bca5751fb34b58930d657b6fe8c035332c (patch)
tree84aae1ad50fa06c74eaa0b55e12db4b936690afe
parent87ef07178d9b164bec923d95e80d73d39ec3331b (diff)
parent8226d65c42b93b1cf1c8c8246bbe68a236030384 (diff)
downloadmullvadvpn-af73f0bca5751fb34b58930d657b6fe8c035332c.tar.xz
mullvadvpn-af73f0bca5751fb34b58930d657b6fe8c035332c.zip
Merge branch 'address-cache-readonly'
-rw-r--r--ios/MullvadREST/AddressCache.swift301
-rw-r--r--ios/MullvadREST/Info.plist8
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj6
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 = (