diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-09-17 11:30:07 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-09-17 11:30:07 +0200 |
| commit | 3a9ed2a87e12545ab7ff4debe797672f75397e9e (patch) | |
| tree | 411735dba3185242c4b5ba44cc7c4d79eb6adee9 | |
| parent | 9a4412b5a2b48b2c069daa1c44a21280fa8e599b (diff) | |
| parent | 42fde982b7d7a71ef9ec32e9397b399a50415b06 (diff) | |
| download | mullvadvpn-3a9ed2a87e12545ab7ff4debe797672f75397e9e.tar.xz mullvadvpn-3a9ed2a87e12545ab7ff4debe797672f75397e9e.zip | |
Merge branch 'refactor-relaycache'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 51 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 27 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache.swift | 393 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/AnyRelayCacheObserver.swift | 31 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/CachedRelays.swift | 25 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCache.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCacheError.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCacheIO.swift | 112 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCacheObserver.swift | 13 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCacheTracker.swift | 246 | ||||
| -rw-r--r-- | ios/MullvadVPN/SelectLocationViewController.swift | 4 |
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 |
