summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-09-19 14:03:39 +0200
committerAndrej Mihajlov <and@mullvad.net>2019-09-23 14:01:46 +0200
commit28c6a7971815e283d406673abaad55d0cd7fe1e0 (patch)
treed5afed174df812f9274c88e4999ffd984a31dfb3
parentfaed38f77854ba657790043444a82fd29d3b7aa4 (diff)
downloadmullvadvpn-28c6a7971815e283d406673abaad55d0cd7fe1e0.tar.xz
mullvadvpn-28c6a7971815e283d406673abaad55d0cd7fe1e0.zip
Add RelayCache
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj17
-rw-r--r--ios/MullvadVPN/ApplicationConfiguration.swift18
-rw-r--r--ios/MullvadVPN/RelayCache.swift228
-rw-r--r--ios/MullvadVPN/RelayList.swift23
-rw-r--r--ios/MullvadVPN/SelectLocationController.swift87
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
}