diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/ApplicationConfiguration.swift | 18 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache.swift | 228 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayList.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/SelectLocationController.swift | 87 |
5 files changed, 337 insertions, 36 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index df975f7b3c..974b90f555 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -42,6 +42,11 @@ 58BFA5C022A7C8A900A6173D /* MullvadAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */; }; 58BFA5C122A7C92400A6173D /* JsonRequestProcedure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E30228B2AEB00C7710B /* JsonRequestProcedure.swift */; }; 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; }; + 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; + 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; + 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; + 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; 58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */; }; 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */; }; @@ -130,6 +135,8 @@ 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAPI.swift; sourceTree = "<group>"; }; 58ADDB3F227B1E7100FAFEA7 /* Optional+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Unwrap.swift"; sourceTree = "<group>"; }; 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; }; + 58BFA5C522A7C97F00A6173D /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; + 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; }; 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; }; 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; }; 58CCA00F224249A1004F3011 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = "<group>"; }; @@ -223,6 +230,7 @@ 58F19E32228B383300C7710B /* AccountVerificationProcedure.swift */, 58CCA01722426713004F3011 /* AccountViewController.swift */, 58CE5E63224146200008646E /* AppDelegate.swift */, + 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */, 58CE5E6A224146210008646E /* Assets.xcassets */, 589AB4F6227B64450039131E /* BasicTableViewCell.swift */, 58CCA00F224249A1004F3011 /* ConnectViewController.swift */, @@ -241,6 +249,7 @@ 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */, 58ADDB3F227B1E7100FAFEA7 /* Optional+Unwrap.swift */, 58781CC522AE5F4B009B9D8E /* ProcedureKit+Patches.swift */, + 58BFA5C522A7C97F00A6173D /* RelayCache.swift */, 5888AD88227B18C40051EB06 /* RelayList.swift */, 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, @@ -529,11 +538,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5840250122B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, 581CBCE62296B97300727D7F /* ViewControllerIdentifier.swift in Sources */, + 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 58461AD3228D622E00B72ECB /* Account.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */, @@ -579,13 +590,15 @@ files = ( 58D06EAA2302E1B0000C75C6 /* AccountVerificationProcedure.swift in Sources */, 58781CC722AE602B009B9D8E /* ProcedureKit+Patches.swift in Sources */, - 58D06EA92302E1A8000C75C6 /* RelayList.swift in Sources */, 58D06EA82302E1A1000C75C6 /* WireguardAssociatedAddresses.swift in Sources */, + 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */, 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */, + 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */, 58BFA5C122A7C92400A6173D /* JsonRequestProcedure.swift in Sources */, 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, + 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */, 58BFA5C022A7C8A900A6173D /* MullvadAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -800,6 +813,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = G7CDBEG477; + ENABLE_BITCODE = NO; INFOPLIST_FILE = PacketTunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -824,6 +838,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = G7CDBEG477; + ENABLE_BITCODE = NO; INFOPLIST_FILE = PacketTunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift new file mode 100644 index 0000000000..3324d8d66f --- /dev/null +++ b/ios/MullvadVPN/ApplicationConfiguration.swift @@ -0,0 +1,18 @@ +// +// ApplicationConfiguration.swift +// MullvadVPN +// +// Created by pronebird on 05/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +class ApplicationConfiguration { + + /// The application group identifier used for sharing application preferences between processes + static let securityGroupIdentifier = "group.net.mullvad.MullvadVPN" + + /// The application identifier for the PacketTunnel extension + static let packetTunnelExtensionIdentifier = "net.mullvad.MullvadVPN.PacketTunnel" +} diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift new file mode 100644 index 0000000000..c35bf7b265 --- /dev/null +++ b/ios/MullvadVPN/RelayCache.swift @@ -0,0 +1,228 @@ +// +// RelayCache.swift +// MullvadVPN +// +// Created by pronebird on 05/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import ProcedureKit +import os + +class RelayCache { + /// Internal procedure queue + private let queue: ProcedureQueue = { + let queue = ProcedureQueue() + queue.qualityOfService = .utility + return queue + }() + + /// The cache location used by the class instance + private let cacheFileURL: URL + + /// Error emitted by read and write functions + enum Error: Swift.Error { + case defaultLocationNotFound + case io(Swift.Error) + case coding(Swift.Error) + } + + /// The default cache file location + static var defaultCacheFileURL: URL? { + let appGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier + let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) + + return containerURL.flatMap { URL(fileURLWithPath: "relays.json", relativeTo: $0) } + } + + init(cacheFileURL: URL) { + self.cacheFileURL = cacheFileURL + } + + class func withDefaultLocation() throws -> RelayCache { + guard let cacheFileURL = defaultCacheFileURL else { + throw Error.defaultLocationNotFound + } + return RelayCache(cacheFileURL: cacheFileURL) + } + + /// Read the relay cache and update it from remote if needed. + /// The completion handler is called on a background queue + func read(completion: @escaping (Result<CachedRelayList, Swift.Error>) -> Void) { + let cacheRequestProcedure = BlockProcedure { (blockProcedure) in + self.readAndUpdateRelaysIfNeeded(completion: { (result) in + completion(result) + blockProcedure.finish() + }) + } + + cacheRequestProcedure.addCondition(MutuallyExclusive<RelayCache>()) + + queue.addOperation(cacheRequestProcedure) + } + + private func readAndUpdateRelaysIfNeeded(completion: @escaping (Result<CachedRelayList, Swift.Error>) -> Void) { + let updateRelays = { (cachedRelaysFromDisk: CachedRelayList?, + finish: @escaping (Result<CachedRelayList, Swift.Error>) -> Void) in + self.downloadRelays(completion: { (result) in + switch result { + case .success: + finish(result) + + case .failure(let error): + os_log(.error, "Failed to update the relay cache: %s", error.localizedDescription) + + // Return the on-disk cache in the event of networking error + if let cachedRelaysFromDisk = cachedRelaysFromDisk { + finish(.success(cachedRelaysFromDisk)) + } else { + finish(result) + } + } + }) + } + + RelayCache.read(cacheFileURL: cacheFileURL) { (result) in + switch result { + case .success(let cachedRelays): + if cachedRelays.needsUpdate() { + updateRelays(cachedRelays, completion) + } else { + completion(.success(cachedRelays)) + } + + case .failure(let error): + os_log(.error, "Failed to read the relay cache: %s", error.localizedDescription) + updateRelays(nil, completion) + } + } + } + + private func downloadRelays(completion: @escaping (Result<CachedRelayList, Swift.Error>) -> Void) { + // Download relays + let downloadRelays = MullvadAPI.getRelayList() + + // Turn RelayList into CachedRelayList + let transform = TransformProcedure { (response) -> CachedRelayList in + let relayList = try response.result.get() + + return CachedRelayList(relayList: relayList, updatedAt: Date()) + }.injectResult(from: downloadRelays) + + // Write cache on disk + let writeCache = AsyncTransformProcedure<CachedRelayList, CachedRelayList> { (input, finish) in + RelayCache.write(cacheFileURL: self.cacheFileURL, record: input, completion: { (result) in + switch result { + case .success: + finish(.success(input)) + + case .failure(let error): + finish(.failure(error)) + } + }) + }.injectResult(from: transform) + + writeCache.addDidFinishBlockObserver { (procedure, error) in + if let result = procedure.output.value?.into() { + completion(result) + } else if let error = error { + completion(.failure(error)) + } + } + + queue.addOperation(GroupProcedure(operations: [downloadRelays, transform, writeCache])) + } + + /// Safely read the cache file from disk using file coordinator + private class func read(cacheFileURL: URL, completion: @escaping (Result<CachedRelayList, Error>) -> Void) { + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + let accessor = { (fileURLForReading: URL) -> Void in + var data: Data + + // Read data from disk + do { + data = try Data(contentsOf: fileURLForReading) + } catch { + completion(.failure(.io(error))) + return + } + + // Decode data into RelayListCacheFile + do { + let decoded = try JSONDecoder().decode(CachedRelayList.self, from: data) + + completion(.success(decoded)) + } catch { + completion(.failure(.coding(error))) + } + } + + var error: NSError? + fileCoordinator.coordinate(readingItemAt: cacheFileURL, + options: [.withoutChanges], + error: &error, + byAccessor: accessor) + + if let error = error { + completion(.failure(.io(error))) + } + } + + /// Safely write the cache file on disk using file coordinator + private class func write(cacheFileURL: URL, record: CachedRelayList, completion: @escaping (Result<Void, Error>) -> Void) { + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + + let accessor = { (fileURLForWriting: URL) -> Void in + var data: Data + + // Encode data + do { + data = try JSONEncoder().encode(record) + } catch { + completion(.failure(.coding(error))) + return + } + + // Write data + do { + try data.write(to: fileURLForWriting) + + completion(.success(())) + } catch { + completion(.failure(.io(error))) + } + } + + var error: NSError? + fileCoordinator.coordinate(writingItemAt: cacheFileURL, + options: [.forReplacing], + error: &error, + byAccessor: accessor) + + if let error = error { + completion(.failure(.io(error))) + } + } +} + +/// A struct that represents the relay cache on disk +struct CachedRelayList: Codable { + /// The relay list stored within the cache entry + var relayList: RelayList + + /// The date when this cache was last updated + var updatedAt: Date +} + +private extension CachedRelayList { + /// Returns true if it's time to refresh the relay list cache + func needsUpdate() -> Bool { + let now = Date() + guard let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: updatedAt) else { + return false + } + return now >= nextUpdate + } +} diff --git a/ios/MullvadVPN/RelayList.swift b/ios/MullvadVPN/RelayList.swift index 415b549942..8601572af8 100644 --- a/ios/MullvadVPN/RelayList.swift +++ b/ios/MullvadVPN/RelayList.swift @@ -8,15 +8,16 @@ // import Foundation +import Network -struct RelayList: Decodable { - struct Country: Decodable { +struct RelayList: Codable { + struct Country: Codable { let name: String let code: String let cities: [City] } - struct City: Decodable { + struct City: Codable { let name: String let code: String let latitude: Double @@ -24,11 +25,23 @@ struct RelayList: Decodable { let relays: [Hostname] } - struct Hostname: Decodable { + struct Hostname: Codable { let hostname: String - let ipv4AddrIn: String + let ipv4AddrIn: IPv4Address let includeInCountry: Bool let weight: Int32 + let tunnels: Tunnels? + } + + struct Tunnels: Codable { + let wireguard: [WireguardTunnel]? + } + + struct WireguardTunnel: Codable { + let ipv4Gateway: IPv4Address + let ipv6Gateway: IPv6Address + let publicKey: Data + let portRanges: [ClosedRange<UInt16>] } let countries: [Country] diff --git a/ios/MullvadVPN/SelectLocationController.swift b/ios/MullvadVPN/SelectLocationController.swift index b6d50e39eb..884e2ac1ba 100644 --- a/ios/MullvadVPN/SelectLocationController.swift +++ b/ios/MullvadVPN/SelectLocationController.swift @@ -7,19 +7,17 @@ // import UIKit -import ProcedureKit -import os.log +import os private let cellIdentifier = "Cell" class SelectLocationController: UITableViewController { + private let relayCache = try! RelayCache.withDefaultLocation() private var relayList: RelayList? private var expandedItems = [RelayListDataSourceItem]() private var displayedItems = [RelayListDataSourceItem]() - private let procedureQueue = ProcedureQueue() - var selectedItem: RelayListDataSourceItem? // MARK: - View lifecycle @@ -81,26 +79,21 @@ class SelectLocationController: UITableViewController { // MARK: - Relay list handling private func loadRelayList() { - let procedure = MullvadAPI.getRelayList() + relayCache.read { [weak self] (result) in + switch result { + case .success(let cachedRelays): + DispatchQueue.main.async { + self?.didReceiveRelayList(cachedRelays.relayList) + } - procedure.addDidFinishBlockObserver(synchronizedWith: DispatchQueue.main) { [weak self] (procedure, error) in - guard let response = procedure.output.success else { - os_log(.error, "Relay list network error: %{public}s", error?.localizedDescription ?? "(null)") - return + case .failure(let error): + os_log(.error, "Failed to read the relay cache: %{public}s", error.localizedDescription) } - - self?.didReceiveRelayList(response) } - - procedureQueue.addOperation(procedure) } - private func didReceiveRelayList(_ response: JsonRpcResponse<RelayList>) { - do { - relayList = try response.result.get() - } catch { - os_log(.error, "Relay list server error: %{public}s", error.localizedDescription) - } + private func didReceiveRelayList(_ relayList: RelayList) { + self.relayList = relayList updateDisplayedItems() tableView.reloadData() @@ -173,21 +166,34 @@ private extension RelayList { var items = [RelayListDataSourceItem]() for country in countries { - let countryItem = RelayListDataSourceItem.country(country) + let wrappedCountry = RelayListDataSourceItem.Country( + countryCode: country.code, + name: country.name, + cityCount: country.cities.count) + let countryItem = RelayListDataSourceItem.country(wrappedCountry) items.append(countryItem) guard filter(countryItem) else { continue } for city in country.cities { - let cityItem = RelayListDataSourceItem.city(city) + let wrappedCity = RelayListDataSourceItem.City( + countryCode: country.code, + cityCode: city.code, + name: city.name, + hostCount: city.relays.count) + let cityItem = RelayListDataSourceItem.city(wrappedCity) items.append(cityItem) guard filter(cityItem) else { continue } for host in city.relays { - items.append(.hostname(host)) + let wrappedHost = RelayListDataSourceItem.Hostname( + countryCode: country.code, + cityCode: city.code, + hostname: host.hostname) + items.append(.hostname(wrappedHost)) } } } @@ -200,25 +206,46 @@ private extension RelayList { /// A wrapper type for RelayList to be able to represent it as a flat list enum RelayListDataSourceItem: Equatable { - case country(RelayList.Country) - case city(RelayList.City) - case hostname(RelayList.Hostname) + struct Country { + let countryCode: String + let name: String + let cityCount: Int + } + + struct City { + let countryCode: String + let cityCode: String + let name: String + let hostCount: Int + } + + struct Hostname { + let countryCode: String + let cityCode: String + let hostname: String + } + + case country(Country) + case city(City) + case hostname(Hostname) static func == (lhs: RelayListDataSourceItem, rhs: RelayListDataSourceItem) -> Bool { switch (lhs, rhs) { case (.country(let a), .country(let b)): - return a.code == b.code + return a.countryCode == b.countryCode case (.city(let a), .city(let b)): - return a.code == b.code + return a.countryCode == b.countryCode && a.cityCode == b.cityCode case (.hostname(let a), .hostname(let b)): - return a.hostname == b.hostname + return a.countryCode == b.countryCode && a.cityCode == b.cityCode && + a.hostname == b.hostname default: return false } } + } private extension RelayListDataSourceItem { @@ -248,9 +275,9 @@ private extension RelayListDataSourceItem { func hasActiveRelays() -> Bool { switch self { case .country(let country): - return country.cities.count > 0 + return country.cityCount > 0 case .city(let city): - return city.relays.count > 0 + return city.hostCount > 0 case .hostname: return true } |
