summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-09-17 11:30:07 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-09-17 11:30:07 +0200
commit3a9ed2a87e12545ab7ff4debe797672f75397e9e (patch)
tree411735dba3185242c4b5ba44cc7c4d79eb6adee9
parent9a4412b5a2b48b2c069daa1c44a21280fa8e599b (diff)
parent42fde982b7d7a71ef9ec32e9397b399a50415b06 (diff)
downloadmullvadvpn-3a9ed2a87e12545ab7ff4debe797672f75397e9e.tar.xz
mullvadvpn-3a9ed2a87e12545ab7ff4debe797672f75397e9e.zip
Merge branch 'refactor-relaycache'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj51
-rw-r--r--ios/MullvadVPN/AppDelegate.swift27
-rw-r--r--ios/MullvadVPN/RelayCache.swift393
-rw-r--r--ios/MullvadVPN/RelayCache/AnyRelayCacheObserver.swift31
-rw-r--r--ios/MullvadVPN/RelayCache/CachedRelays.swift25
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCache.swift11
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheError.swift46
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheIO.swift112
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheObserver.swift13
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift246
-rw-r--r--ios/MullvadVPN/SelectLocationViewController.swift4
11 files changed, 548 insertions, 411 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 843362f450..a561a860af 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -30,9 +30,13 @@
5823FA5026CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; };
5820674E26E6510200655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
5820675026E6514100655B05 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674F26E6514100655B05 /* HTTP.swift */; };
+ 5820675526E6528200655B05 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; };
5820675626E6528A00655B05 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; };
5820675726E652A600655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
+ 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */; };
5820675926E652BE00655B05 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; };
+ 5820675B26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
+ 5820675C26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; };
5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; };
@@ -91,6 +95,10 @@
585834F824D2BC1F00A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834F724D2BC1F00A8AF56 /* Logging */; };
585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; };
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; };
+ 585DA87726B024A600B8C587 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
+ 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
+ 585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; };
+ 585DA87D26B0254000B8C587 /* RelayCacheIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */; };
585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
585DA88726B0277200B8C587 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; };
@@ -181,8 +189,7 @@
58BA791B2578F092006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA791A2578F092006FAEA0 /* WireGuardKit */; };
58BA7947257901A5006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA7946257901A5006FAEA0 /* WireGuardKit */; };
58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
- 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; };
- 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; };
+ 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */; };
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
58C3478B26C1094F0060838B /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; };
@@ -269,6 +276,8 @@
58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */; };
58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
+ 58FB865826EA213300F188BC /* AnyRelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865726EA213300F188BC /* AnyRelayCacheObserver.swift */; };
+ 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheObserver.swift */; };
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; };
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */; };
@@ -338,6 +347,7 @@
5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+BackgroundTask.swift"; sourceTree = "<group>"; };
5820674D26E6510200655B05 /* REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
5820674F26E6514100655B05 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; };
+ 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; };
5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; };
@@ -375,6 +385,9 @@
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; };
5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNavigationController.swift; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
+ 585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = "<group>"; };
+ 585DA87926B024F900B8C587 /* RelayCacheError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheError.swift; sourceTree = "<group>"; };
+ 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheIO.swift; sourceTree = "<group>"; };
585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRelaysResponse.swift; sourceTree = "<group>"; };
585DA88626B0277200B8C587 /* RESTError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTError.swift; sourceTree = "<group>"; };
585DA88926B027A300B8C587 /* RESTCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTCoding.swift; sourceTree = "<group>"; };
@@ -435,7 +448,7 @@
58B9EB142489139B00095626 /* DisplayChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayChainedError.swift; sourceTree = "<group>"; };
58BA692D23E99EFF009DC256 /* Locking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locking.swift; sourceTree = "<group>"; };
58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProvider.swift; sourceTree = "<group>"; };
- 58BFA5C522A7C97F00A6173D /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
+ 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.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>"; };
58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyWithMetadata.swift; sourceTree = "<group>"; };
@@ -503,6 +516,8 @@
58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainClass.swift; sourceTree = "<group>"; };
58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManagerError.swift; sourceTree = "<group>"; };
58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; };
+ 58FB865726EA213300F188BC /* AnyRelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelayCacheObserver.swift; sourceTree = "<group>"; };
+ 58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; };
58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; };
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceInteractionRestriction.swift; sourceTree = "<group>"; };
@@ -613,6 +628,20 @@
path = MullvadVPN;
sourceTree = "<group>";
};
+ 585DA87526B0249A00B8C587 /* RelayCache */ = {
+ isa = PBXGroup;
+ children = (
+ 58FB865726EA213300F188BC /* AnyRelayCacheObserver.swift */,
+ 585DA87626B024A600B8C587 /* CachedRelays.swift */,
+ 5820675A26E6576800655B05 /* RelayCache.swift */,
+ 585DA87926B024F900B8C587 /* RelayCacheError.swift */,
+ 585DA87C26B0254000B8C587 /* RelayCacheIO.swift */,
+ 58FB865926EA214400F188BC /* RelayCacheObserver.swift */,
+ 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */,
+ );
+ path = RelayCache;
+ sourceTree = "<group>";
+ };
585DA87F26B0268500B8C587 /* REST */ = {
isa = PBXGroup;
children = (
@@ -771,7 +800,7 @@
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
- 58BFA5C522A7C97F00A6173D /* RelayCache.swift */,
+ 585DA87526B0249A00B8C587 /* RelayCache */,
58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */,
58781CD422AFBA39009B9D8E /* RelaySelector.swift */,
585DA87F26B0268500B8C587 /* REST */,
@@ -1176,6 +1205,8 @@
5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */,
58E1337126D2BE9C00CC316B /* AnyOptional.swift in Sources */,
5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */,
+ 5820675B26E6576800655B05 /* RelayCache.swift in Sources */,
+ 585DA87D26B0254000B8C587 /* RelayCacheIO.swift in Sources */,
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */,
58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */,
587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */,
@@ -1187,7 +1218,8 @@
585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
- 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */,
+ 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
+ 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
@@ -1196,12 +1228,14 @@
5820675026E6514100655B05 /* HTTP.swift in Sources */,
5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */,
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */,
+ 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */,
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */,
58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */,
+ 585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */,
58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */,
5846226A26E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift in Sources */,
58E1337526D2BEC400CC316B /* Promise+Optional.swift in Sources */,
@@ -1287,6 +1321,8 @@
585DA88726B0277200B8C587 /* RESTError.swift in Sources */,
58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
+ 585DA87726B024A600B8C587 /* CachedRelays.swift in Sources */,
+ 58FB865826EA213300F188BC /* AnyRelayCacheObserver.swift in Sources */,
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */,
581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */,
@@ -1321,6 +1357,7 @@
5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */,
580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */,
58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */,
+ 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */,
5820675726E652A600655B05 /* REST.swift in Sources */,
58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
58B93A2526C683B300A55733 /* Promise.swift in Sources */,
@@ -1330,6 +1367,7 @@
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */,
+ 5820675C26E6576800655B05 /* RelayCache.swift in Sources */,
58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */,
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */,
@@ -1347,14 +1385,15 @@
5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */,
- 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
+ 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */,
584E96BD240FD4DA00D3334F /* Location.swift in Sources */,
58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */,
58F840B32464491D0044E708 /* ChainedError.swift in Sources */,
58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */,
5820675626E6528A00655B05 /* RESTError.swift in Sources */,
+ 5820675526E6528200655B05 /* RelayCacheError.swift in Sources */,
58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */,
58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index c240b4ddf0..fcbc2542ae 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -32,7 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private var connectController: ConnectViewController?
private weak var settingsNavController: SettingsNavigationController?
- private var cachedRelays: CachedRelays? {
+ private var cachedRelays: RelayCache.CachedRelays? {
didSet {
if let cachedRelays = cachedRelays {
self.selectLocationViewController?.setCachedRelays(cachedRelays)
@@ -75,23 +75,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
launchController.view.backgroundColor = .primaryColor
self.window?.rootViewController = launchController
- // Update relays
- RelayCache.shared.addObserver(self)
- RelayCache.shared.updateRelays()
+ // Add relay cache observer
+ RelayCache.Tracker.shared.addObserver(self)
// Load initial relays
- self.logger?.debug("Load relays")
- RelayCache.shared.read { (result) in
- DispatchQueue.main.async {
+ RelayCache.Tracker.shared.read()
+ .receive(on: .main)
+ .observe { completion in
+ guard let result = completion.unwrappedValue else { return }
+
switch result {
case .success(let cachedRelays):
self.cachedRelays = cachedRelays
- self.logger?.debug("Loaded relays")
case .failure(let error):
self.logger?.error(chainedError: error, message: "Failed to load initial relays")
}
- }
}
// Load tunnels
@@ -134,6 +133,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) {
TunnelManager.shared.refreshTunnelState(completionHandler: nil)
+
+ // Start periodic relays updates
+ RelayCache.Tracker.shared.startPeriodicUpdates()
+ }
+
+ func applicationWillResignActive(_ application: UIApplication) {
+ // Stop periodic relays updates
+ RelayCache.Tracker.shared.stopPeriodicUpdates()
}
// MARK: - Private
@@ -701,7 +708,7 @@ extension AppDelegate: UIAdaptivePresentationControllerDelegate {
extension AppDelegate: RelayCacheObserver {
- func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays) {
+ func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) {
DispatchQueue.main.async {
self.cachedRelays = cachedRelays
}
diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift
deleted file mode 100644
index fe07798bbd..0000000000
--- a/ios/MullvadVPN/RelayCache.swift
+++ /dev/null
@@ -1,393 +0,0 @@
-//
-// RelayCache.swift
-// MullvadVPN
-//
-// Created by pronebird on 05/06/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Logging
-
-/// Periodic update interval
-private let kUpdateIntervalSeconds = 3600
-
-/// Error emitted by read and write functions
-enum RelayCacheError: ChainedError {
- case readCache(Error)
- case readPrebundledRelays(Error)
- case decodePrebundledRelays(Error)
- case writeCache(Error)
- case encodeCache(Error)
- case decodeCache(Error)
- case rest(RestError)
-
- var errorDescription: String? {
- switch self {
- case .encodeCache:
- return "Encode cache error"
- case .decodeCache:
- return "Decode cache error"
- case .readCache:
- return "Read cache error"
- case .readPrebundledRelays:
- return "Read pre-bundled relays error"
- case .decodePrebundledRelays:
- return "Decode pre-bundled relays error"
- case .writeCache:
- return "Write cache error"
- case .rest:
- return "REST error"
- }
- }
-}
-
-protocol RelayCacheObserver: AnyObject {
- func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays)
-}
-
-private class AnyRelayCacheObserver: WeakObserverBox, RelayCacheObserver {
-
- typealias Wrapped = RelayCacheObserver
-
- private(set) weak var inner: RelayCacheObserver?
-
- init<T: RelayCacheObserver>(_ inner: T) {
- self.inner = inner
- }
-
- func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays) {
- inner?.relayCache(relayCache, didUpdateCachedRelays: cachedRelays)
- }
-
- static func == (lhs: AnyRelayCacheObserver, rhs: AnyRelayCacheObserver) -> Bool {
- return lhs.inner === rhs.inner
- }
-}
-
-class RelayCache {
- private let logger = Logger(label: "RelayCache")
-
- /// Mullvad REST client
- private let rest = MullvadRest()
-
- /// The cache location used by the class instance
- private let cacheFileURL: URL
-
- /// A dispatch queue used for thread synchronization
- private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.RelayCache")
-
- /// A timer source used for periodic updates
- private var timerSource: DispatchSourceTimer?
-
- /// A flag that indicates whether periodic updates are running
- private var isPeriodicUpdatesEnabled = false
-
- /// A download task used for relay RPC request
- private var downloadTask: URLSessionTask?
-
- /// The default cache file location
- static var defaultCacheFileURL: URL {
- let appGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier
- let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)!
-
- return containerURL.appendingPathComponent("relays.json")
- }
-
- /// The path to the pre-bundled relays.json file
- private static var preBundledRelaysFileURL: URL {
- return Bundle.main.url(forResource: "relays", withExtension: "json")!
- }
-
- /// Observers
- private let observerList = ObserverList<AnyRelayCacheObserver>()
-
- /// A shared instance of `RelayCache`
- static let shared = RelayCache(cacheFileURL: defaultCacheFileURL)
-
- private init(cacheFileURL: URL) {
- self.cacheFileURL = cacheFileURL
- }
-
- func startPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) {
- dispatchQueue.async {
- if !self.isPeriodicUpdatesEnabled {
- self.isPeriodicUpdatesEnabled = true
-
- switch Self.read(cacheFileURL: self.cacheFileURL) {
- case .success(let cachedRelayList):
- if let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelayList.updatedAt) {
- let startTime = Self.makeWalltime(fromDate: nextUpdate)
- self.scheduleRepeatingTimer(startTime: startTime)
- }
-
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache")
-
- if Self.shouldDownloadRelaysOnReadFailure(readError) {
- self.scheduleRepeatingTimer(startTime: .now())
- }
- }
- }
-
- queue.performOnWrappedOrCurrentQueue {
- completionHandler?()
- }
- }
- }
-
- func stopPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) {
- dispatchQueue.async {
- self.isPeriodicUpdatesEnabled = false
-
- self.timerSource?.cancel()
- self.timerSource = nil
- self.downloadTask?.cancel()
-
- queue.performOnWrappedOrCurrentQueue {
- completionHandler?()
- }
- }
- }
-
- func updateRelays() {
- dispatchQueue.async {
- self._updateRelays()
- }
- }
-
- /// Read the relay cache from disk
- func read(completionHandler: @escaping (Result<CachedRelays, RelayCacheError>) -> Void) {
- dispatchQueue.async {
- let result = Self.read(cacheFileURL: self.cacheFileURL)
- .flatMapError { (error) -> Result<CachedRelays, RelayCacheError> in
- switch error {
- case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile):
- return Self.readPrebundledRelays(fileURL: Self.preBundledRelaysFileURL)
- default:
- return .failure(error)
- }
- }
- completionHandler(result)
- }
- }
-
- // MARK: - Observation
-
- func addObserver<T: RelayCacheObserver>(_ observer: T) {
- observerList.append(AnyRelayCacheObserver(observer))
- }
-
- func removeObserver<T: RelayCacheObserver>(_ observer: T) {
- observerList.remove(AnyRelayCacheObserver(observer))
- }
-
- // MARK: - Private instance methods
-
- private func _updateRelays() {
- switch Self.read(cacheFileURL: self.cacheFileURL) {
- case .success(let cachedRelays):
- let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelays.updatedAt)
-
- if let nextUpdate = nextUpdate, nextUpdate <= Date() {
- self.downloadRelays(previouslyCachedRelays: cachedRelays)
- }
-
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated")
-
- if Self.shouldDownloadRelaysOnReadFailure(readError) {
- self.downloadRelays(previouslyCachedRelays: nil)
- }
- }
- }
-
- private func downloadRelays(previouslyCachedRelays: CachedRelays?) {
- let taskResult = makeDownloadTask(etag: previouslyCachedRelays?.etag) { (result) in
- switch result {
- case .success(.newContent(let etag, let relays)):
- let numRelays = relays.wireguard.relays.count
-
- self.logger.info("Downloaded \(numRelays) relays")
-
- let cachedRelays = CachedRelays(etag: etag, relays: relays, updatedAt: Date())
- switch Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) {
- case .success:
- self.observerList.forEach { (observer) in
- observer.relayCache(self, didUpdateCachedRelays: cachedRelays)
- }
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to store downloaded relays")
- }
-
- case .success(.notModified):
- self.logger.info("Relays haven't changed since last check.")
-
- var cachedRelays = previouslyCachedRelays!
- cachedRelays.updatedAt = Date()
-
- switch Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) {
- case .success:
- break
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to update cached relays timestamp")
- }
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to download relays")
- }
- }
-
- downloadTask?.cancel()
-
- switch taskResult {
- case .success(let newDownloadTask):
- downloadTask = newDownloadTask
- newDownloadTask.resume()
-
- case .failure(let restError):
- self.logger.error(chainedError: restError, message: "Failed to create a REST request for updating relays")
- downloadTask = nil
- }
- }
-
- private func scheduleRepeatingTimer(startTime: DispatchWallTime) {
- let timerSource = DispatchSource.makeTimerSource(queue: dispatchQueue)
- timerSource.setEventHandler { [weak self] in
- guard let self = self else { return }
-
- if self.isPeriodicUpdatesEnabled {
- self._updateRelays()
- }
- }
-
- timerSource.schedule(wallDeadline: startTime, repeating: .seconds(kUpdateIntervalSeconds))
- timerSource.activate()
-
- self.timerSource = timerSource
- }
-
- private func makeDownloadTask(etag: String?, completionHandler: @escaping (Result<HttpResourceCacheResponse<ServerRelaysResponse>, RelayCacheError>) -> Void) -> Result<URLSessionDataTask, RestError> {
- return rest.getRelays().dataTask(payload: ETagPayload(etag: etag, enforceWeakValidator: true, payload: EmptyPayload())) { (result) in
- self.dispatchQueue.async {
- completionHandler(result.mapError { RelayCacheError.rest($0) })
- }
- }
- }
-
- // MARK: - Private class methods
-
- /// Safely read the cache file from disk using file coordinator
- private class func read(cacheFileURL: URL) -> Result<CachedRelays, RelayCacheError> {
- var result: Result<CachedRelays, RelayCacheError>?
- let fileCoordinator = NSFileCoordinator(filePresenter: nil)
-
- let accessor = { (fileURLForReading: URL) -> Void in
- // Decode data from disk
- result = Result { try Data(contentsOf: fileURLForReading) }
- .mapError { RelayCacheError.readCache($0) }
- .flatMap { (data) in
- Result { try JSONDecoder().decode(CachedRelays.self, from: data) }
- .mapError { RelayCacheError.decodeCache($0) }
- }
- }
-
- var error: NSError?
- fileCoordinator.coordinate(readingItemAt: cacheFileURL,
- options: [.withoutChanges],
- error: &error,
- byAccessor: accessor)
-
- if let error = error {
- result = .failure(.readCache(error))
- }
-
- return result!
- }
-
- private class func readPrebundledRelays(fileURL: URL) -> Result<CachedRelays, RelayCacheError> {
- return Result { try Data(contentsOf: fileURL) }
- .mapError { RelayCacheError.readPrebundledRelays($0) }
- .flatMap { (data) -> Result<CachedRelays, RelayCacheError> in
- return Result { try MullvadRest.makeJSONDecoder().decode(ServerRelaysResponse.self, from: data) }
- .mapError { RelayCacheError.decodePrebundledRelays($0) }
- .map { (relays) -> CachedRelays in
- return CachedRelays(
- relays: relays,
- updatedAt: Date(timeIntervalSince1970: 0)
- )
- }
- }
- }
-
- /// Safely write the cache file on disk using file coordinator
- private class func write(cacheFileURL: URL, record: CachedRelays) -> Result<(), RelayCacheError> {
- var result: Result<(), RelayCacheError>?
- let fileCoordinator = NSFileCoordinator(filePresenter: nil)
-
- let accessor = { (fileURLForWriting: URL) -> Void in
- result = Result { try JSONEncoder().encode(record) }
- .mapError { RelayCacheError.encodeCache($0) }
- .flatMap { (data) in
- Result { try data.write(to: fileURLForWriting) }
- .mapError { RelayCacheError.writeCache($0) }
- }
- }
-
- var error: NSError?
- fileCoordinator.coordinate(writingItemAt: cacheFileURL,
- options: [.forReplacing],
- error: &error,
- byAccessor: accessor)
-
- if let error = error {
- result = .failure(.writeCache(error))
- }
-
- return result!
- }
-
- private class func makeWalltime(fromDate date: Date) -> DispatchWallTime {
- let (seconds, frac) = modf(date.timeIntervalSince1970)
-
- let nsec: Double = frac * Double(NSEC_PER_SEC)
- let walltime = timespec(tv_sec: Int(seconds), tv_nsec: Int(nsec))
-
- return DispatchWallTime(timespec: walltime)
- }
-
- private class func nextUpdateDate(lastUpdatedAt: Date) -> Date? {
- return Calendar.current.date(
- byAdding: .second,
- value: kUpdateIntervalSeconds,
- to: lastUpdatedAt
- )
- }
-
- private class func shouldDownloadRelaysOnReadFailure(_ error: RelayCacheError) -> Bool {
- switch error {
- case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache:
- return true
-
- case .readCache(CocoaError.fileReadNoSuchFile):
- return true
-
- default:
- return false
- }
- }
-}
-
-/// A struct that represents the relay cache on disk
-struct CachedRelays: Codable {
- /// E-tag returned by server
- var etag: String?
-
- /// The relay list stored within the cache entry
- var relays: ServerRelaysResponse
-
- /// The date when this cache was last updated
- var updatedAt: Date
-}
diff --git a/ios/MullvadVPN/RelayCache/AnyRelayCacheObserver.swift b/ios/MullvadVPN/RelayCache/AnyRelayCacheObserver.swift
new file mode 100644
index 0000000000..b588530311
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/AnyRelayCacheObserver.swift
@@ -0,0 +1,31 @@
+//
+// AnyRelayCacheObserver.swift
+// AnyRelayCacheObserver
+//
+// Created by pronebird on 09/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension RelayCache {
+
+ final class AnyRelayCacheObserver: WeakObserverBox, RelayCacheObserver {
+ typealias Wrapped = RelayCacheObserver
+
+ private(set) weak var inner: RelayCacheObserver?
+
+ init<T: RelayCacheObserver>(_ inner: T) {
+ self.inner = inner
+ }
+
+ func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: CachedRelays) {
+ inner?.relayCache(relayCache, didUpdateCachedRelays: cachedRelays)
+ }
+
+ static func == (lhs: AnyRelayCacheObserver, rhs: AnyRelayCacheObserver) -> Bool {
+ return lhs.inner === rhs.inner
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/RelayCache/CachedRelays.swift b/ios/MullvadVPN/RelayCache/CachedRelays.swift
new file mode 100644
index 0000000000..a4cbcc93e5
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/CachedRelays.swift
@@ -0,0 +1,25 @@
+//
+// CachedRelays.swift
+// CachedRelays
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension RelayCache {
+
+ /// A struct that represents the relay cache on disk
+ struct CachedRelays: Codable {
+ /// E-tag returned by server
+ var etag: String?
+
+ /// The relay list stored within the cache entry
+ var relays: REST.ServerRelaysResponse
+
+ /// The date when this cache was last updated
+ var updatedAt: Date
+ }
+
+}
diff --git a/ios/MullvadVPN/RelayCache/RelayCache.swift b/ios/MullvadVPN/RelayCache/RelayCache.swift
new file mode 100644
index 0000000000..6ad72e205a
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/RelayCache.swift
@@ -0,0 +1,11 @@
+//
+// RelayCache.swift
+// RelayCache
+//
+// Created by pronebird on 06/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum RelayCache {}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheError.swift b/ios/MullvadVPN/RelayCache/RelayCacheError.swift
new file mode 100644
index 0000000000..c161ee3937
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/RelayCacheError.swift
@@ -0,0 +1,46 @@
+//
+// RelayCacheError.swift
+// RelayCacheError
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension RelayCache {
+
+ /// Error emitted by RelayCache cluster.
+ enum Error: ChainedError {
+ case readCache(Swift.Error)
+ case readPrebundledRelays(Swift.Error)
+ case decodePrebundledRelays(Swift.Error)
+ case writeCache(Swift.Error)
+ case encodeCache(Swift.Error)
+ case decodeCache(Swift.Error)
+ case rest(REST.Error)
+ case backgroundTaskScheduler(Swift.Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .encodeCache:
+ return "Encode cache error"
+ case .decodeCache:
+ return "Decode cache error"
+ case .readCache:
+ return "Read cache error"
+ case .readPrebundledRelays:
+ return "Read pre-bundled relays error"
+ case .decodePrebundledRelays:
+ return "Decode pre-bundled relays error"
+ case .writeCache:
+ return "Write cache error"
+ case .rest:
+ return "REST error"
+ case .backgroundTaskScheduler:
+ return "Background task scheduler error"
+ }
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
new file mode 100644
index 0000000000..fc9a13dd41
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
@@ -0,0 +1,112 @@
+//
+// RelayCacheIO.swift
+// RelayCacheIO
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension RelayCache {
+ enum IO {}
+}
+
+extension RelayCache.IO {
+ /// The default cache file location bound by app group container.
+ static func defaultCacheFileURL(forSecurityApplicationGroupIdentifier appGroupIdentifier: String) -> URL? {
+ let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
+
+ return containerURL?.appendingPathComponent("relays.json")
+ }
+
+ /// The path to pre-bundled `relays.json` file.
+ static var preBundledRelaysFileURL: URL? {
+ return Bundle.main.url(forResource: "relays", withExtension: "json")
+ }
+
+ /// Safely read the cache file from disk using file coordinator.
+ static func read(cacheFileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
+ var result: Result<RelayCache.CachedRelays, RelayCache.Error>?
+ let fileCoordinator = NSFileCoordinator(filePresenter: nil)
+
+ let accessor = { (fileURLForReading: URL) -> Void in
+ // Decode data from disk
+ result = Result { try Data(contentsOf: fileURLForReading) }
+ .mapError { RelayCache.Error.readCache($0) }
+ .flatMap { (data) in
+ Result { try JSONDecoder().decode(RelayCache.CachedRelays.self, from: data) }
+ .mapError { RelayCache.Error.decodeCache($0) }
+ }
+ }
+
+ var error: NSError?
+ fileCoordinator.coordinate(readingItemAt: cacheFileURL,
+ options: [.withoutChanges],
+ error: &error,
+ byAccessor: accessor)
+
+ if let error = error {
+ result = .failure(.readCache(error))
+ }
+
+ return result!
+ }
+
+ /// Safely read the cache file from disk using file coordinator and fallback to prebundled relays in case if the
+ /// relay cache file is missing.
+ static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
+ return Self.read(cacheFileURL: cacheFileURL)
+ .flatMapError { (error) -> Result<RelayCache.CachedRelays, RelayCache.Error> in
+ switch error {
+ case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile):
+ return RelayCache.IO.readPrebundledRelays(fileURL: preBundledRelaysFileURL)
+ default:
+ return .failure(error)
+ }
+ }
+ }
+
+ /// Read pre-bundled relays file from disk.
+ static func readPrebundledRelays(fileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
+ return Result { try Data(contentsOf: fileURL) }
+ .mapError { RelayCache.Error.readPrebundledRelays($0) }
+ .flatMap { (data) -> Result<RelayCache.CachedRelays, RelayCache.Error> in
+ return Result { try REST.Coding.makeJSONDecoder().decode(REST.ServerRelaysResponse.self, from: data) }
+ .mapError { RelayCache.Error.decodePrebundledRelays($0) }
+ .map { (relays) -> RelayCache.CachedRelays in
+ return RelayCache.CachedRelays(
+ relays: relays,
+ updatedAt: Date(timeIntervalSince1970: 0)
+ )
+ }
+ }
+ }
+
+ /// Safely write the cache file on disk using file coordinator.
+ static func write(cacheFileURL: URL, record: RelayCache.CachedRelays) -> Result<(), RelayCache.Error> {
+ var result: Result<(), RelayCache.Error>?
+ let fileCoordinator = NSFileCoordinator(filePresenter: nil)
+
+ let accessor = { (fileURLForWriting: URL) -> Void in
+ result = Result { try JSONEncoder().encode(record) }
+ .mapError { RelayCache.Error.encodeCache($0) }
+ .flatMap { (data) in
+ Result { try data.write(to: fileURLForWriting) }
+ .mapError { RelayCache.Error.writeCache($0) }
+ }
+ }
+
+ var error: NSError?
+ fileCoordinator.coordinate(writingItemAt: cacheFileURL,
+ options: [.forReplacing],
+ error: &error,
+ byAccessor: accessor)
+
+ if let error = error {
+ result = .failure(.writeCache(error))
+ }
+
+ return result!
+ }
+}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift b/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift
new file mode 100644
index 0000000000..53a9edc299
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift
@@ -0,0 +1,13 @@
+//
+// RelayCacheObserver.swift
+// RelayCacheObserver
+//
+// Created by pronebird on 09/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol RelayCacheObserver: AnyObject {
+ func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays)
+}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
new file mode 100644
index 0000000000..9e34795c88
--- /dev/null
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -0,0 +1,246 @@
+//
+// RelayCacheTracker.swift
+// MullvadVPN
+//
+// Created by pronebird on 05/06/2019.
+// Copyright © 2019 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+extension RelayCache {
+
+ class Tracker {
+ /// Relay update interval (in seconds)
+ private static let relayUpdateInterval: TimeInterval = 60 * 60
+
+ /// Tracker log
+ private let logger = Logger(label: "RelayCacheTracker")
+
+ /// The cache location used by the class instance
+ private let cacheFileURL: URL
+
+ /// The location of prebundled `relays.json`
+ private let prebundledRelaysFileURL: URL
+
+ /// A dispatch queue used for thread synchronization
+ private let stateQueue = DispatchQueue(label: "RelayCacheTrackerStateQueue")
+
+ /// A dispatch queue used for serializing relay cache updates
+ private let updateQueue = DispatchQueue(label: "RelayCacheTrackerUpdateQueue")
+
+ /// A timer source used for periodic updates
+ private var timerSource: DispatchSourceTimer?
+
+ /// A flag that indicates whether periodic updates are running
+ private var isPeriodicUpdatesEnabled = false
+
+ /// Observers
+ private let observerList = ObserverList<AnyRelayCacheObserver>()
+
+ /// A shared instance of `RelayCache`
+ static let shared: RelayCache.Tracker = {
+ let cacheFileURL = RelayCache.IO.defaultCacheFileURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
+ let prebundledRelaysFileURL = RelayCache.IO.preBundledRelaysFileURL!
+
+ return Tracker(
+ cacheFileURL: cacheFileURL,
+ prebundledRelaysFileURL: prebundledRelaysFileURL
+ )
+ }()
+
+ private init(cacheFileURL: URL, prebundledRelaysFileURL: URL) {
+ self.cacheFileURL = cacheFileURL
+ self.prebundledRelaysFileURL = prebundledRelaysFileURL
+ }
+
+ func startPeriodicUpdates() {
+ stateQueue.async {
+ guard !self.isPeriodicUpdatesEnabled else { return }
+
+ self.logger.debug("Start periodic relay updates")
+
+ self.isPeriodicUpdatesEnabled = true
+
+ switch RelayCache.IO.read(cacheFileURL: self.cacheFileURL) {
+ case .success(let cachedRelays):
+ let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
+ self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
+
+ case .failure(let readError):
+ self.logger.error(chainedError: readError, message: "Failed to read the relay cache")
+
+ if Self.shouldDownloadRelaysOnReadFailure(readError) {
+ self.scheduleRepeatingTimer(startTime: .now())
+ }
+ }
+ }
+ }
+
+ func stopPeriodicUpdates() {
+ stateQueue.async {
+ guard self.isPeriodicUpdatesEnabled else { return }
+
+ self.logger.debug("Stop periodic relay updates")
+
+ self.isPeriodicUpdatesEnabled = false
+
+ self.timerSource?.cancel()
+ self.timerSource = nil
+ }
+ }
+
+ func updateRelays() -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise {
+ return Promise.deferred {
+ return RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
+ }
+ .schedule(on: stateQueue)
+ .then { result in
+ switch result {
+ case .success(let cachedRelays):
+ let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
+
+ if nextUpdate <= Date() {
+ return self.downloadRelays(previouslyCachedRelays: cachedRelays)
+ } else {
+ return .success(.throttled)
+ }
+
+ case .failure(let readError):
+ self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated")
+
+ if Self.shouldDownloadRelaysOnReadFailure(readError) {
+ return self.downloadRelays(previouslyCachedRelays: nil)
+ } else {
+ return .failure(readError)
+ }
+ }
+ }
+ .block(on: updateQueue)
+ }
+
+ func read() -> Result<CachedRelays, RelayCache.Error>.Promise {
+ return Promise.deferred {
+ return RelayCache.IO.readWithFallback(
+ cacheFileURL: self.cacheFileURL,
+ preBundledRelaysFileURL: self.prebundledRelaysFileURL
+ )
+ }.schedule(on: stateQueue)
+ }
+
+ // MARK: - Observation
+
+ func addObserver<T: RelayCacheObserver>(_ observer: T) {
+ observerList.append(AnyRelayCacheObserver(observer))
+ }
+
+ func removeObserver<T: RelayCacheObserver>(_ observer: T) {
+ observerList.remove(AnyRelayCacheObserver(observer))
+ }
+
+ // MARK: - Private instance methods
+
+ private func downloadRelays(previouslyCachedRelays: CachedRelays?) -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise {
+ return REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag)
+ .receive(on: stateQueue)
+ .mapError { error in
+ self.logger.error(chainedError: error, message: "Failed to download relays")
+ return RelayCache.Error.rest(error)
+ }
+ .mapThen { result in
+ switch result {
+ case .newContent(let etag, let relays):
+ let numRelays = relays.wireguard.relays.count
+
+ self.logger.info("Downloaded \(numRelays) relays")
+
+ let cachedRelays = CachedRelays(etag: etag, relays: relays, updatedAt: Date())
+
+ return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
+ .asPromise()
+ .map { _ in
+ self.observerList.forEach { (observer) in
+ observer.relayCache(self, didUpdateCachedRelays: cachedRelays)
+ }
+
+ return .newContent
+ }
+ .onFailure { error in
+ self.logger.error(chainedError: error, message: "Failed to store downloaded relays")
+ }
+
+ case .notModified:
+ self.logger.info("Relays haven't changed since last check.")
+
+ var cachedRelays = previouslyCachedRelays!
+ cachedRelays.updatedAt = Date()
+
+ return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
+ .asPromise()
+ .map { _ in
+ return .sameContent
+ }
+ .onFailure { error in
+ self.logger.error(chainedError: error, message: "Failed to update cached relays timestamp")
+ }
+ }
+ }
+ }
+
+ private func scheduleRepeatingTimer(startTime: DispatchWallTime) {
+ let timerSource = DispatchSource.makeTimerSource(queue: stateQueue)
+ timerSource.setEventHandler { [weak self] in
+ self?.updateRelays().observe { _ in }
+ }
+
+ timerSource.schedule(wallDeadline: startTime, repeating: .seconds(Int(Self.relayUpdateInterval)))
+ timerSource.activate()
+
+ self.timerSource = timerSource
+ }
+
+ // MARK: - Private class methods
+
+ private class func shouldDownloadRelaysOnReadFailure(_ error: RelayCache.Error) -> Bool {
+ switch error {
+ case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache:
+ return true
+
+ case .readCache(CocoaError.fileReadNoSuchFile):
+ return true
+
+ default:
+ return false
+ }
+ }
+ }
+
+}
+
+extension RelayCache {
+
+ /// Type describing the result of an attempt to fetch the new relay list from server.
+ enum FetchResult: CustomStringConvertible {
+ /// Request to update relays was throttled.
+ case throttled
+
+ /// Refreshed relays but the same content was found on remote.
+ case sameContent
+
+ /// Refreshed relays with new content.
+ case newContent
+
+ var description: String {
+ switch self {
+ case .throttled:
+ return "throttled"
+ case .sameContent:
+ return "same content"
+ case .newContent:
+ return "new content"
+ }
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/SelectLocationViewController.swift b/ios/MullvadVPN/SelectLocationViewController.swift
index 222aa14b85..3b672c2c09 100644
--- a/ios/MullvadVPN/SelectLocationViewController.swift
+++ b/ios/MullvadVPN/SelectLocationViewController.swift
@@ -25,7 +25,7 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
private let logger = Logger(label: "SelectLocationController")
private var dataSource: LocationDataSource?
- private var setCachedRelaysOnViewDidLoad: CachedRelays?
+ private var setCachedRelaysOnViewDidLoad: RelayCache.CachedRelays?
private var setRelayLocationOnViewDidLoad: RelayLocation?
private var setScrollPositionOnViewDidLoad: UITableView.ScrollPosition = .none
private var isViewAppeared = false
@@ -203,7 +203,7 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
// MARK: - Public
- func setCachedRelays(_ cachedRelays: CachedRelays) {
+ func setCachedRelays(_ cachedRelays: RelayCache.CachedRelays) {
guard isViewLoaded else {
self.setCachedRelaysOnViewDidLoad = cachedRelays
return