diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-08-01 16:10:04 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-08-01 16:10:04 +0200 |
| commit | a3df757018bc0b41036d2ec710819b16be356bb5 (patch) | |
| tree | 3488694d74c9ea19ce744be14ecda8ca639c829b | |
| parent | 68303e0872fa3370e31ec3e83a1d1f40c05caf37 (diff) | |
| parent | a9fd39392f26e8952051295357246daa598205b5 (diff) | |
| download | mullvadvpn-a3df757018bc0b41036d2ec710819b16be356bb5.tar.xz mullvadvpn-a3df757018bc0b41036d2ec710819b16be356bb5.zip | |
Merge branch 'add-device-state'
37 files changed, 1820 insertions, 1282 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3a2fb2c726..55f041db82 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; }; 58059DDC28465E8F002B1049 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDB28465E8F002B1049 /* TransformOperation.swift */; }; 58059DDE28468158002B1049 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDD28468158002B1049 /* OutputOperation.swift */; }; 58059DE02846823E002B1049 /* ResultOperation+Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */; }; @@ -16,6 +15,7 @@ 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; + 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; }; 58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C4E2760BA9100890776 /* AddressCacheStore.swift */; }; 58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C502760BBB400890776 /* AddressCacheTracker.swift */; }; 58095C532760EEC700890776 /* RESTNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C522760EEC700890776 /* RESTNetworkOperation.swift */; }; @@ -53,7 +53,7 @@ 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 */; }; - 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerError.swift */; }; + 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; }; 5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */; }; 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; }; 58289082286B590900478596 /* UIFont+Monospaced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58289081286B590900478596 /* UIFont+Monospaced.swift */; }; @@ -193,6 +193,7 @@ 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; }; 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; }; + 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58968FAD28743E2000B799DC /* TunnelInteractor.swift */; }; 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; @@ -349,7 +350,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerState.swift; sourceTree = "<group>"; }; 58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; }; 58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultOperation+Output.swift"; sourceTree = "<group>"; }; @@ -358,6 +358,7 @@ 5808273928487E3E006B77A4 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; }; 5808273B284888BC006B77A4 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = "<group>"; }; 5808273C284888E5006B77A4 /* PacketTunnel.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PacketTunnel.xcconfig; sourceTree = "<group>"; }; + 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceViewController.swift; sourceTree = "<group>"; }; 58095C4E2760BA9100890776 /* AddressCacheStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheStore.swift; sourceTree = "<group>"; }; 58095C502760BBB400890776 /* AddressCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTracker.swift; sourceTree = "<group>"; }; 58095C522760EEC700890776 /* RESTNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTNetworkOperation.swift; sourceTree = "<group>"; }; @@ -380,7 +381,7 @@ 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>"; }; - 5820676326E771DB00655B05 /* TunnelManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerError.swift; sourceTree = "<group>"; }; + 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; }; 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+UIBackgroundFetchResult.swift"; sourceTree = "<group>"; }; 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; }; 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = "<group>"; }; @@ -483,6 +484,7 @@ 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; }; 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+KeyboardNavigation.swift"; sourceTree = "<group>"; }; 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; }; + 58968FAD28743E2000B799DC /* TunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInteractor.swift; sourceTree = "<group>"; }; 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; }; 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; }; 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentView.swift; sourceTree = "<group>"; }; @@ -691,27 +693,27 @@ 5823FA5726CE4A4100283BF8 /* TunnelManager */ = { isa = PBXGroup; children = ( - 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, - 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, - 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */, - 5820676326E771DB00655B05 /* TunnelManagerError.swift */, - 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, - 58B93A1226C3F13600A55733 /* TunnelState.swift */, 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */, - 584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */, + 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */, + 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */, + 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */, + 584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */, + 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */, 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */, 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */, 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */, 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */, - 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */, - 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */, 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */, + 58968FAD28743E2000B799DC /* TunnelInteractor.swift */, + 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, + 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */, + 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */, + 58B93A1226C3F13600A55733 /* TunnelState.swift */, 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, - 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */, ); path = TunnelManager; sourceTree = "<group>"; @@ -915,6 +917,7 @@ 585DA87F26B0268500B8C587 /* REST */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */, + 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */, 5888AD82227B11080051EB06 /* SelectLocationCell.swift */, @@ -1328,6 +1331,7 @@ 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, + 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5820675026E6514100655B05 /* HTTP.swift in Sources */, 584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */, @@ -1376,7 +1380,6 @@ 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */, 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */, 58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */, - 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */, 5868585524054096000B8131 /* AppButton.swift in Sources */, 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */, 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, @@ -1385,7 +1388,7 @@ 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, 585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */, 584B17AB27637DE40057F3B8 /* ReconnectTunnelOperation.swift in Sources */, - 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */, + 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */, 58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, @@ -1411,6 +1414,7 @@ 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, + 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, @@ -1483,6 +1487,7 @@ files = ( 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */, + 58E072A528814C28008902F8 /* TunnelMonitorDelegate.swift in Sources */, 587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */, @@ -1491,6 +1496,7 @@ 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */, 585DA89426B0323E00B8C587 /* TunnelProviderMessage.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */, + 58E07299288031D5008902F8 /* WireGuardAdapterError+Localization.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */, 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index f594e0a6c1..579ca685fd 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -25,7 +25,6 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb private var pendingPayment: SKPayment? private let alertPresenter = AlertPresenter() - private let logger = Logger(label: "AccountViewController") weak var delegate: AccountViewControllerDelegate? @@ -84,7 +83,6 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb comment: "" ) - contentView.accountTokenRowView.accountNumber = TunnelManager.shared.accountNumber contentView.accountTokenRowView.copyAccountNumber = { [weak self] in self?.copyAccountToken() } @@ -96,8 +94,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb AppStorePaymentManager.shared.addPaymentObserver(self) TunnelManager.shared.addObserver(self) - updateAccountExpiry(expiryDate: TunnelManager.shared.accountExpiry) - updateDeviceName(TunnelManager.shared.device?.name) + updateView(from: TunnelManager.shared.deviceState) // Make sure to disable IAPs when payments are restricted if AppStorePaymentManager.canMakePayments { @@ -109,12 +106,14 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb // MARK: - Private methods - private func updateDeviceName(_ deviceName: String?) { - contentView.accountDeviceRow.deviceName = deviceName - } + private func updateView(from deviceState: DeviceState?) { + guard case .loggedIn(let accountData, let deviceData) = deviceState else { + return + } - private func updateAccountExpiry(expiryDate: Date?) { - contentView.accountExpiryRowView.value = expiryDate + contentView.accountDeviceRow.deviceName = deviceData.name + contentView.accountTokenRowView.accountNumber = accountData.number + contentView.accountExpiryRowView.value = accountData.expiry } private func requestStoreProducts() { @@ -320,17 +319,16 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - guard let tunnelSettings = tunnelSettings else { - return - } + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } - updateDeviceName(tunnelSettings.device.name) - updateAccountExpiry(expiryDate: tunnelSettings.account.expiry) + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + updateView(from: deviceState) } // MARK: - AppStorePaymentObserver @@ -384,26 +382,35 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb } private func copyAccountToken() { - UIPasteboard.general.string = TunnelManager.shared.accountNumber + guard let accountData = TunnelManager.shared.deviceState.accountData else { + return + } + + UIPasteboard.general.string = accountData.number } @objc private func doPurchase() { - guard let product = product, let accountNumber = TunnelManager.shared.accountNumber else { return } + guard let accountData = TunnelManager.shared.deviceState.accountData, + let product = product else { + return + } let payment = SKPayment(product: product) pendingPayment = payment compoundInteractionRestriction.increase(animated: true) - AppStorePaymentManager.shared.addPayment(payment, for: accountNumber) + AppStorePaymentManager.shared.addPayment(payment, for: accountData.number) } @objc private func restorePurchases() { - guard let accountNumber = TunnelManager.shared.accountNumber else { return } + guard let accountData = TunnelManager.shared.deviceState.accountData else { + return + } compoundInteractionRestriction.increase(animated: true) - _ = AppStorePaymentManager.shared.restorePurchases(for: accountNumber) { completion in + _ = AppStorePaymentManager.shared.restorePurchases(for: accountData.number) { completion in switch completion { case .success(let response): self.showTimeAddedConfirmationAlert(with: response, context: .restoration) diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 95a32e0ffd..3db5eebe09 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -32,7 +32,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Application lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Setup logging initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!) logger = Logger(label: "AppDelegate") @@ -43,10 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #endif if #available(iOS 13.0, *) { - // Register background tasks on iOS 13 registerBackgroundTasks() } else { - // Set background refresh interval on iOS 12 application.setMinimumBackgroundFetchInterval( ApplicationConfiguration.minimumBackgroundFetchInterval ) @@ -55,44 +52,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate { setupPaymentHandler() setupNotificationHandler() - // Start initialization - let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { blockOperation in + let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in TunnelManager.shared.loadConfiguration { error in - dispatchPrecondition(condition: .onQueue(.main)) - + // TODO: avoid throwing fatal error and show the problem report UI instead. if let error = error { - self.logger.error(chainedError: error, message: "Failed to load tunnels") - - // TODO: avoid throwing fatal error and show the problem report UI instead. - fatalError( - error.displayChain(message: "Failed to load VPN tunnel configuration") - ) + fatalError(error.localizedDescription) } - blockOperation.finish() - } - } + self.logger.debug("Finished initialization.") - let setupUIOperation = AsyncBlockOperation(dispatchQueue: .main) { - self.logger.debug("Finished initialization.") + NotificationManager.shared.updateNotifications() + AppStorePaymentManager.shared.startPaymentQueueMonitoring() - NotificationManager.shared.updateNotifications() - AppStorePaymentManager.shared.startPaymentQueueMonitoring() + operation.finish() + } } - operationQueue.addOperations([ - setupTunnelManagerOperation, - setupUIOperation - ], waitUntilFinished: false) + operationQueue.addOperation(setupTunnelManagerOperation) if #available(iOS 13, *) { - return true + // no-op } else { sceneDelegate = SceneDelegate() sceneDelegate?.setupScene(windowFactory: ClassicWindowFactory()) - - return true } + + return true } func application( @@ -123,7 +108,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - let rotatePrivateKeyOperation = ResultBlockOperation<Bool, TunnelManager.Error> + let rotatePrivateKeyOperation = ResultBlockOperation<Bool, Error> { operation in let handle = TunnelManager.shared.rotatePrivateKey(forceRotate: false) { completion in operation.finish(completion: completion) @@ -387,21 +372,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - AppStorePaymentManagerDelegate extension AppDelegate: AppStorePaymentManagerDelegate { - - func appStorePaymentManager(_ manager: AppStorePaymentManager, - didRequestAccountTokenFor payment: SKPayment) -> String? + func appStorePaymentManager( + _ manager: AppStorePaymentManager, + didRequestAccountTokenFor payment: SKPayment + ) -> String? { - // Since we do not persist the relation between the payment and account token between the - // app launches, we assume that all successful purchases belong to the active account token. - return TunnelManager.shared.accountNumber + // Since we do not persist the relation between payment and account number between the + // app launches, we assume that all successful purchases belong to the active account + // number. + return TunnelManager.shared.deviceState.accountData?.number } - } // MARK: - UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let blockOperation = AsyncBlockOperation(dispatchQueue: .main) { if response.notification.request.identifier == accountExpiryNotificationIdentifier, @@ -429,5 +414,4 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([]) } } - } diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 4dbde5d2c2..6a3fe21d0c 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -50,17 +50,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } var preferredHeaderBarPresentation: HeaderBarPresentation { - guard TunnelManager.shared.isAccountSet else { + switch TunnelManager.shared.deviceState { + case .loggedIn, .revoked: + return HeaderBarPresentation( + style: tunnelState.isSecured ? .secured : .unsecured, + showsDivider: false + ) + case .loggedOut: return HeaderBarPresentation(style: .default, showsDivider: true) } - - switch tunnelState { - case .connecting, .reconnecting, .connected: - return HeaderBarPresentation(style: .secured, showsDivider: false) - - case .disconnecting, .disconnected, .pendingReconnect: - return HeaderBarPresentation(style: .unsecured, showsDivider: false) - } } var prefersHeaderBarHidden: Bool { @@ -92,7 +90,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen contentView.selectLocationButton.addTarget(self, action: #selector(handleSelectLocation(_:)), for: .touchUpInside) TunnelManager.shared.addObserver(self) - self.tunnelState = TunnelManager.shared.tunnelState + self.tunnelState = TunnelManager.shared.tunnelStatus.state addSubviews() setupMapView() @@ -151,7 +149,11 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { setNeedsHeaderBarStyleAppearanceUpdate() } @@ -159,7 +161,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen self.tunnelState = tunnelState } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index 0120d2fb14..cb0ab32a9d 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -55,189 +55,6 @@ extension REST.Error: DisplayChainedError { } } -extension TunnelManager.Error: DisplayChainedError { - var errorChainDescription: String? { - switch self { - case .loadAllVPNConfigurations(let systemError): - return String( - format: NSLocalizedString( - "LOAD_ALL_VPN_CONFIGURATIONS_ERROR", - tableName: "TunnelManager", - value: "Failed to load system VPN configurations: %@", - comment: "" - ), - systemError.localizedDescription - ) - case .reloadVPNConfiguration(let systemError): - return String( - format: NSLocalizedString( - "RELOAD_VPN_CONFIGURATIONS_ERROR", - tableName: "TunnelManager", - value: "Failed to reload a VPN configuration: %@", - comment: "" - ), - systemError.localizedDescription - ) - case .saveVPNConfiguration(let systemError): - return String( - format: NSLocalizedString( - "SAVE_VPN_CONFIGURATION_ERROR", - tableName: "TunnelManager", - value: "Failed to save a VPN tunnel configuration: %@", - comment: "" - ), - systemError.localizedDescription - ) - case .startVPNTunnel(let systemError): - return String( - format: NSLocalizedString( - "START_VPN_TUNNEL_ERROR", - tableName: "TunnelManager", - value: "System error when starting the VPN tunnel: %@", - comment: "" - ), - systemError.localizedDescription - ) - case .removeVPNConfiguration(let systemError): - return String( - format: NSLocalizedString( - "REMOVE_VPN_CONFIGURATION_ERROR", - tableName: "TunnelManager", - value: "Failed to remove the system VPN configuration: %@", - comment: "" - ), - systemError.localizedDescription - ) - case .readSettings: - return NSLocalizedString( - "READ_TUNNEL_SETTINGS_ERROR", - tableName: "TunnelManager", - value: "Failed to read settings", - comment: "" - ) - case .writeSettings: - return NSLocalizedString( - "WRITE_TUNNEL_SETTINGS_ERROR", - tableName: "TunnelManager", - value: "Failed to write settings", - comment: "" - ) - case .deleteSettings: - return NSLocalizedString( - "DELETE_TUNNEL_SETTINGS_ERROR", - tableName: "TunnelManager", - value: "Failed to delete settings", - comment: "" - ) - case .deleteDevice(let restError): - return String( - format: NSLocalizedString( - "DELETE_DEVICE_ERROR", - tableName: "TunnelManager", - value: "Failed to create a device: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - case .getDevice(let restError): - return String( - format: NSLocalizedString( - "CREATE_DEVICE_ERROR", - tableName: "TunnelManager", - value: "Failed to obtain device data: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - case .deviceRevoked: - return NSLocalizedString( - "DEVICE_REVOKED_ERROR", - tableName: "TunnelManager", - value: "Device is revoked.", - comment: "" - ) - case .createDevice(let restError): - return String( - format: NSLocalizedString( - "CREATE_DEVICE_ERROR", - tableName: "TunnelManager", - value: "Failed to create a device: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - case .rotateKey(let restError): - return String( - format: NSLocalizedString( - "ROTATE_KEY_ERROR", - tableName: "TunnelManager", - value: "Failed to rotate WireGuard key: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - case .unsetAccount: - return NSLocalizedString( - "UNSET_ACCOUNT_ERROR", - tableName: "TunnelManager", - value: "Internal error: account is unset", - comment: "" - ) - case .unsetTunnel: - return NSLocalizedString( - "UNSET_TUNNEL_ERROR", - tableName: "TunnelManager", - value: "Tunnel is unset.", - comment: "" - ) - case .readRelays: - return NSLocalizedString( - "READ_RELAYS_ERROR", - tableName: "TunnelManager", - value: "Failed to read relays.", - comment: "" - ) - case .cannotSatisfyRelayConstraints: - return NSLocalizedString( - "CANNOT_SATISFY_RELAY_CONSTRAINTS_ERROR", - tableName: "TunnelManager", - value: "Failed to satisfy relay constraints.", - comment: "" - ) - case .reloadTunnel(let error): - return String( - format: NSLocalizedString( - "RELOAD_TUNNEL_ERROR", - tableName: "TunnelManager", - value: "Failed to reload tunnel: %@", - comment: "" - ), - error.localizedDescription - ) - case .getAccountData(let restError): - return String( - format: NSLocalizedString( - "GET_ACCOUNT_DATA_ERROR", - tableName: "TunnelManager", - value: "Failed to obtain account data: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - case .createAccount(let restError): - return String( - format: NSLocalizedString( - "CREATE_ACCOUNT_ERROR", - tableName: "TunnelManager", - value: "Failed to create new account: %@", - comment: "" - ), - restError.errorChainDescription ?? "" - ) - } - } -} - extension SKError: LocalizedError { public var errorDescription: String? { switch self.code { @@ -296,9 +113,7 @@ extension AppStorePaymentManager.Error: DisplayChainedError { case .validateAccount(let restError): let reason = restError.errorChainDescription ?? "" - if case .unhandledResponse(_, let serverErrorResponse) = restError, - serverErrorResponse?.code == .invalidAccount - { + if restError.compareErrorCode(.invalidAccount) { return String( format: NSLocalizedString( "INVALID_ACCOUNT_ERROR", diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index bdffad84b2..0b8213dfd6 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -16,7 +16,7 @@ enum AuthenticationMethod { enum LoginState { case `default` case authenticating(AuthenticationMethod) - case failure(TunnelManager.Error) + case failure(Error) case success(AuthenticationMethod) } @@ -24,12 +24,12 @@ protocol LoginViewControllerDelegate: AnyObject { func loginViewController( _ controller: LoginViewController, loginWithAccountToken accountToken: String, - completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void + completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void ) func loginViewControllerLoginWithNewAccount( _ controller: LoginViewController, - completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void + completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void ) func loginViewControllerDidLogin(_ controller: LoginViewController) @@ -389,7 +389,7 @@ private extension LoginState { } case .failure(let error): - return error.errorChainDescription ?? "" + return error.localizedDescription case .success(let method): switch method { diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift index 5fa9794bf9..ae3942ca30 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift @@ -27,8 +27,8 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio super.init() - accountExpiry = TunnelManager.shared.accountExpiry TunnelManager.shared.addObserver(self) + accountExpiry = TunnelManager.shared.deviceState.accountData?.expiry } private var trigger: UNNotificationTrigger? { @@ -124,23 +124,31 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio ) } + private func invalidate(deviceState: DeviceState) { + accountExpiry = deviceState.accountData?.expiry + invalidate() + } + // MARK: - TunnelObserver func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - // no-op + invalidate(deviceState: manager.deviceState) } func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - accountExpiry = tunnelSettings?.account.expiry - invalidate() + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + invalidate(deviceState: manager.deviceState) } } diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift index 3018400c22..24c21479f2 100644 --- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift @@ -16,19 +16,22 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr var notificationDescriptor: InAppNotificationDescriptor? { guard let lastError = lastError else { return nil } + let body = (lastError as? LocalizedNotificationError)?.localizedNotificationBody + ?? lastError.localizedDescription + return InAppNotificationDescriptor( identifier: identifier, style: .error, title: NSLocalizedString( "TUNNEL_ERROR_INAPP_NOTIFICATION_TITLE", - value: "Tunnel error", + value: "TUNNEL ERROR", comment: "" ), - body: lastError.errorChainDescription ?? "No error description provided." + body: body ) } - private var lastError: TunnelManager.Error? + private var lastError: Error? override init() { super.init() @@ -52,17 +55,49 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr invalidate() } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // Save tunnel error lastError = error // Tell manager to refresh displayed notifications invalidate() } +} + +protocol LocalizedNotificationError { + var localizedNotificationBody: String? { get } +} +extension StartTunnelError: LocalizedNotificationError { + var localizedNotificationBody: String? { + return String( + format: NSLocalizedString( + "START_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", + value: "Failed to start the tunnel: %@.", + comment: "" + ), + underlyingError.localizedDescription + ) + } +} +extension StopTunnelError: LocalizedNotificationError { + var localizedNotificationBody: String? { + return String( + format: NSLocalizedString( + "STOP_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", + value: "Failed to stop the tunnel: %@.", + comment: "" + ), + underlyingError.localizedDescription + ) + } } diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift index b8cf56a574..5bb73071a3 100644 --- a/ios/MullvadVPN/Operations/OperationCompletion.swift +++ b/ios/MullvadVPN/Operations/OperationCompletion.swift @@ -57,6 +57,14 @@ enum OperationCompletion<Success, Failure: Error> { } } + init(error: Failure?) where Success == Void { + if let error = error { + self = .failure(error) + } else { + self = .success(()) + } + } + func map<NewSuccess>(_ block: (Success) -> NewSuccess) -> OperationCompletion<NewSuccess, Failure> { switch self { case .success(let value): @@ -116,20 +124,6 @@ enum OperationCompletion<Success, Failure: Error> { } } - func assertNoSuccess<NewSuccess>() -> OperationCompletion<NewSuccess, Failure> { - return map { success in - return success as! NewSuccess - } - } - - func assertFailure<NewFailure: Error>(_ failureType: NewFailure.Type) - -> OperationCompletion<Success, NewFailure> - { - return mapError { error -> NewFailure in - return error as! NewFailure - } - } - func ignoreOutput() -> OperationCompletion<Void, Failure> { return map { _ in () } } diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift index d3ab3ffa74..05fdb793fd 100644 --- a/ios/MullvadVPN/PreferencesViewController.swift +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -11,8 +11,6 @@ import Logging class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate, TunnelObserver { - private let logger = Logger(label: "PreferencesViewController") - private let dataSource = PreferencesDataSource() override var preferredStatusBarStyle: UIStatusBarStyle { @@ -47,10 +45,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel navigationItem.rightBarButtonItem = editButtonItem TunnelManager.shared.addObserver(self) - - if let dnsSettings = TunnelManager.shared.tunnelSettings?.dnsSettings { - dataSource.update(from: dnsSettings) - } + dataSource.update(from: TunnelManager.shared.settings.dnsSettings) } override func setEditing(_ editing: Bool, animated: Bool) { @@ -73,11 +68,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel func preferencesDataSource(_ dataSource: PreferencesDataSource, didChangeViewModel dataModel: PreferencesViewModel) { let dnsSettings = dataModel.asDNSSettings() - TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] error in - if let error = error { - self?.logger.error(chainedError: error, message: "Failed to save DNS settings") - } - } + TunnelManager.shared.setDNSSettings(dnsSettings) } // MARK: - TunnelObserver @@ -90,14 +81,17 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - guard let dnsSettings = tunnelSettings?.dnsSettings else { return } + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + dataSource.update(from: tunnelSettings.dnsSettings) + } + - dataSource.update(from: dnsSettings) + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + // no-op } } diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift index 5f1213f76c..0087faa18f 100644 --- a/ios/MullvadVPN/ProblemReportViewController.swift +++ b/ios/MullvadVPN/ProblemReportViewController.swift @@ -19,7 +19,9 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier // TODO: make sure we redact old tokens - let redactStrings = TunnelManager.shared.accountNumber.flatMap { [$0] } ?? [] + + let redactStrings = [TunnelManager.shared.deviceState.accountData?.number] + .compactMap { $0 } let report = ConsolidatedApplicationLog( redactCustomStrings: redactStrings, diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift index e275a9ab34..175370563f 100644 --- a/ios/MullvadVPN/REST/RESTError.swift +++ b/ios/MullvadVPN/REST/RESTError.swift @@ -46,6 +46,14 @@ extension REST { return "Failure to decode URL response data." } } + + func compareErrorCode(_ code: ServerResponseCode) -> Bool { + if case .unhandledResponse(_, let serverResponse) = self { + return serverResponse?.code == code + } else { + return false + } + } } struct ServerErrorResponse: Decodable { diff --git a/ios/MullvadVPN/REST/ServerRelaysResponse.swift b/ios/MullvadVPN/REST/ServerRelaysResponse.swift index a5606510dc..f2f6baa4ad 100644 --- a/ios/MullvadVPN/REST/ServerRelaysResponse.swift +++ b/ios/MullvadVPN/REST/ServerRelaysResponse.swift @@ -18,7 +18,7 @@ extension REST { let longitude: Double } - struct ServerRelay: Codable { + struct ServerRelay: Codable, Equatable { let hostname: String let active: Bool let owned: Bool diff --git a/ios/MullvadVPN/Result+Extensions.swift b/ios/MullvadVPN/Result+Extensions.swift index 795ad8901d..9a2245d673 100644 --- a/ios/MullvadVPN/Result+Extensions.swift +++ b/ios/MullvadVPN/Result+Extensions.swift @@ -27,3 +27,14 @@ extension Result { } } } + +extension Result { + func flattenValue<T>() -> T? where Success == Optional<T> { + switch self { + case .success(let optional): + return optional.flatMap { $0 } + case .failure: + return nil + } + } +} diff --git a/ios/MullvadVPN/RevokedDeviceViewController.swift b/ios/MullvadVPN/RevokedDeviceViewController.swift new file mode 100644 index 0000000000..117ad69e91 --- /dev/null +++ b/ios/MullvadVPN/RevokedDeviceViewController.swift @@ -0,0 +1,191 @@ +// +// RevokedDeviceViewController.swift +// MullvadVPN +// +// Created by pronebird on 07/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +protocol RevokedDeviceViewControllerDelegate: AnyObject { + func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController) +} + +class RevokedDeviceViewController: UIViewController, RootContainment, TunnelObserver { + + private lazy var imageView: StatusImageView = { + let statusImageView = StatusImageView(style: .failure) + statusImageView.translatesAutoresizingMaskIntoConstraints = false + return statusImageView + }() + + private lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) + titleLabel.numberOfLines = 0 + titleLabel.textColor = .white + titleLabel.text = NSLocalizedString( + "TITLE_LABEL", + tableName: "RevokedDevice", + value: "Device is inactive", + comment: "" + ) + titleLabel.font = UIFont.systemFont(ofSize: 32) + return titleLabel + }() + + private lazy var bodyLabel: UILabel = { + let bodyLabel = UILabel() + bodyLabel.translatesAutoresizingMaskIntoConstraints = false + bodyLabel.font = UIFont.systemFont(ofSize: 17) + bodyLabel.numberOfLines = 0 + bodyLabel.textColor = .white + bodyLabel.text = NSLocalizedString( + "DESCRIPTION_LABEL", + tableName: "RevokedDevice", + value: "You have revoked this device. To connect again, you will need to log back in.", + comment: "" + ) + return bodyLabel + }() + + private lazy var footerLabel: UILabel = { + let bodyLabel = UILabel() + bodyLabel.translatesAutoresizingMaskIntoConstraints = false + bodyLabel.font = UIFont.systemFont(ofSize: 17) + bodyLabel.numberOfLines = 0 + bodyLabel.textColor = .white + bodyLabel.text = NSLocalizedString( + "UNBLOCK_INTERNET_LABEL", + tableName: "RevokedDevice", + value: "Going to login will unblock the Internet on this device.", + comment: "" + ) + return bodyLabel + }() + + private lazy var logoutButton: AppButton = { + let button = AppButton(style: .default) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle( + NSLocalizedString( + "GOTO_LOGIN_BUTTON_LABEL", + tableName: "RevokedDevice", + value: "Go to login", + comment: "" + ), + for: .normal + ) + return button + }() + + weak var delegate: RevokedDeviceViewControllerDelegate? + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + var preferredHeaderBarPresentation: HeaderBarPresentation { + let tunnelState = TunnelManager.shared.tunnelStatus.state + + return HeaderBarPresentation( + style: tunnelState.isSecured ? .secured : .unsecured, + showsDivider: true + ) + } + + var prefersHeaderBarHidden: Bool { + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .secondaryColor + view.layoutMargins = UIMetrics.contentLayoutMargins + + for subview in [imageView, titleLabel, bodyLabel, footerLabel, logoutButton] { + view.addSubview(subview) + } + + logoutButton.addTarget( + self, + action: #selector(didTapLogoutButton(_:)), + for: .touchUpInside + ) + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint( + equalTo: view.layoutMarginsGuide.topAnchor, + constant: 30 + ), + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + titleLabel.topAnchor.constraint( + equalTo: imageView.bottomAnchor, + constant: 30 + ), + titleLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + bodyLabel.topAnchor.constraint( + equalTo: titleLabel.bottomAnchor, + constant: 16 + ), + bodyLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + bodyLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + footerLabel.topAnchor.constraint( + equalTo: bodyLabel.bottomAnchor, + constant: 16 + ), + footerLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + footerLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + logoutButton.topAnchor.constraint(greaterThanOrEqualTo: footerLabel.bottomAnchor), + logoutButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + logoutButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + logoutButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor) + ]) + + TunnelManager.shared.addObserver(self) + updateView(tunnelState: TunnelManager.shared.tunnelStatus.state) + } + + @objc private func didTapLogoutButton(_ sender: Any?) { + logoutButton.isEnabled = false + + delegate?.revokedDeviceControllerDidRequestLogout(self) + } + + private func updateView(tunnelState: TunnelState) { + logoutButton.style = tunnelState.isSecured ? .danger : .default + footerLabel.isHidden = !tunnelState.isSecured + } + + // MARK: - TunnelObserver + + func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + setNeedsHeaderBarStyleAppearanceUpdate() + updateView(tunnelState: tunnelState) + } + + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { + // no-op + } + +} diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 19ce61eb36..3d134115e1 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -17,6 +17,11 @@ class SceneDelegate: UIResponder { private var isSceneConfigured = false private let rootContainer = RootContainerViewController() + + // Modal root container is used on iPad to present login, TOS, revoked device, device management + // view controllers above `rootContainer` which only contains split controller. + private lazy var modalRootContainer = RootContainerViewController() + private var splitViewController: CustomSplitViewController? private var selectLocationViewController: SelectLocationViewController? private var connectController: ConnectViewController? @@ -39,7 +44,7 @@ class SceneDelegate: UIResponder { window?.makeKeyAndVisible() TunnelManager.shared.addObserver(self) - if TunnelManager.shared.isLoadedConfiguration { + if TunnelManager.shared.isConfigurationLoaded { configureScene() } } @@ -207,9 +212,9 @@ extension SceneDelegate: RootContainerViewControllerDelegate { } func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool { - guard TunnelManager.shared.isAccountSet else { return false } + guard TunnelManager.shared.deviceState.isLoggedIn else { return false } - switch TunnelManager.shared.tunnelState { + switch TunnelManager.shared.tunnelStatus.state { case .connected, .connecting, .reconnecting: TunnelManager.shared.reconnectTunnel(selectNewRelay: true) case .disconnecting, .disconnected: @@ -224,6 +229,7 @@ extension SceneDelegate: RootContainerViewControllerDelegate { extension SceneDelegate { private func setupPadUI() { + let tunnelManager = TunnelManager.shared let selectLocationController = makeSelectLocationController() let connectController = makeConnectViewController() @@ -240,32 +246,77 @@ extension SceneDelegate { self.connectController = connectController rootContainer.setViewControllers([splitViewController], animated: false) - showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false) + showSplitViewMaster(tunnelManager.deviceState.isLoggedIn, animated: false) + + modalRootContainer.delegate = self + + let showNextController = { [weak self] (animated: Bool) in + guard let self = self else { return } - let rootContainerWrapper = makeLoginContainerController() + lazy var viewControllers: [UIViewController] = [self.makeLoginController()] - if !TermsOfService.isAgreed { - let termsOfServiceViewController = self.makeTermsOfServiceController { [weak self] viewController in - guard let self = self else { return } + switch tunnelManager.deviceState { + case .loggedIn: + let didDismissModalRoot = { + self.showAccountSettingsControllerIfAccountExpired() + } - if TunnelManager.shared.isAccountSet { - rootContainerWrapper.dismiss(animated: true) { - self.showAccountSettingsControllerIfAccountExpired() - } + // Dismiss modal root container if needed before proceeding. + if self.isModalRootPresented { + self.modalRootContainer.dismiss(animated: animated, completion: didDismissModalRoot) } else { - rootContainerWrapper.pushViewController(self.makeLoginController(), animated: true) + didDismissModalRoot() } + + return + + case .loggedOut: + break + + case .revoked: + viewControllers.append(self.makeRevokedDeviceController()) } - rootContainerWrapper.setViewControllers([termsOfServiceViewController], animated: false) - rootContainer.present(rootContainerWrapper, animated: false) - } else if !TunnelManager.shared.isAccountSet { - rootContainerWrapper.setViewControllers([makeLoginController()], animated: false) - rootContainer.present(rootContainerWrapper, animated: false) + + // Configure modal container. + self.modalRootContainer.setViewControllers( + viewControllers, + animated: self.isModalRootPresented && animated + ) + + // Present modal container if not presented yet. + self.presentModalRootContainerIfNeeded(animated: animated) + } + + if TermsOfService.isAgreed { + showNextController(false) } else { - self.showAccountSettingsControllerIfAccountExpired() + let termsOfServiceController = self.makeTermsOfServiceController { _ in + showNextController(true) + } + + modalRootContainer.setViewControllers([termsOfServiceController], animated: false) + presentModalRootContainerIfNeeded(animated: false) } } + private func presentModalRootContainerIfNeeded(animated: Bool) { + modalRootContainer.preferredContentSize = CGSize(width: 480, height: 600) + modalRootContainer.modalPresentationStyle = .formSheet + modalRootContainer.presentationController?.delegate = self + + if #available(iOS 13.0, *) { + modalRootContainer.isModalInPresentation = true + } + + if modalRootContainer.presentingViewController == nil { + rootContainer.present(modalRootContainer, animated: animated) + } + } + + private var isModalRootPresented: Bool { + return modalRootContainer.presentingViewController != nil + } + private func setupPhoneUI() { let showNextController = { [weak self] (animated: Bool) in guard let self = self else { return } @@ -273,10 +324,17 @@ extension SceneDelegate { let loginViewController = self.makeLoginController() var viewControllers: [UIViewController] = [loginViewController] - if TunnelManager.shared.isAccountSet { + switch TunnelManager.shared.deviceState { + case .loggedIn: let connectController = self.makeConnectViewController() viewControllers.append(connectController) self.connectController = connectController + + case .loggedOut: + break + + case .revoked: + viewControllers.append(self.makeRevokedDeviceController()) } self.rootContainer.setViewControllers(viewControllers, animated: animated) { @@ -328,14 +386,13 @@ extension SceneDelegate { selectLocationController.setCachedRelays(cachedRelays) } - let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints - if let relayLocation = relayConstraints?.location.value { - selectLocationController.setSelectedRelayLocation( - relayLocation, - animated: false, - scrollPosition: .middle - ) - } + let relayConstraints = TunnelManager.shared.settings.relayConstraints + + selectLocationController.setSelectedRelayLocation( + relayConstraints.location.value, + animated: false, + scrollPosition: .middle + ) return selectLocationController } @@ -361,22 +418,10 @@ extension SceneDelegate { return controller } - private func makeLoginContainerController() -> RootContainerViewController { - let rootContainerWrapper = RootContainerViewController() - rootContainerWrapper.delegate = self - rootContainerWrapper.preferredContentSize = CGSize(width: 480, height: 600) - - if UIDevice.current.userInterfaceIdiom == .pad { - rootContainerWrapper.modalPresentationStyle = .formSheet - if #available(iOS 13.0, *) { - // Prevent swiping off the login or terms of service controllers - rootContainerWrapper.isModalInPresentation = true - } - } - - rootContainerWrapper.presentationController?.delegate = self - - return rootContainerWrapper + private func makeRevokedDeviceController() -> RevokedDeviceViewController { + let controller = RevokedDeviceViewController() + controller.delegate = self + return controller } private func makeLoginController() -> LoginViewController { @@ -386,22 +431,57 @@ extension SceneDelegate { } private func showAccountSettingsControllerIfAccountExpired() { - guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return } + guard case .loggedIn(let accountData, _) = TunnelManager.shared.deviceState else { + return + } - rootContainer.showSettings(navigateTo: .account, animated: true) + if accountData.expiry <= Date() { + rootContainer.showSettings(navigateTo: .account, animated: true) + } } private func showSplitViewMaster(_ show: Bool, animated: Bool) { splitViewController?.preferredDisplayMode = show ? .allVisible : .primaryHidden connectController?.setMainContentHidden(!show, animated: animated) } + + private func showLoginViewAfterLogout(dismissController: UIViewController?) { + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let loginController = rootContainer.viewControllers.first as? LoginViewController + loginController?.reset() + + rootContainer.popToRootViewController(animated: false) + dismissController?.dismiss(animated: true) + + case .pad: + let didDismissSourceController = { + self.presentModalRootContainerIfNeeded(animated: true) + } + + let loginController = modalRootContainer.viewControllers.first as? LoginViewController + loginController?.reset() + + modalRootContainer.popToRootViewController(animated: isModalRootPresented) + showSplitViewMaster(false, animated: true) + + if let dismissController = dismissController { + dismissController.dismiss(animated: true, completion: didDismissSourceController) + } else { + didDismissSourceController() + } + + default: + fatalError() + } + } } // MARK: - LoginViewControllerDelegate extension SceneDelegate: LoginViewControllerDelegate { - func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { + func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) { rootContainer.setEnableSettingsButton(false) TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in @@ -411,7 +491,10 @@ extension SceneDelegate: LoginViewControllerDelegate { // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to log in with existing account.") + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to log in with existing account." + ) fallthrough case .cancelled: @@ -422,7 +505,7 @@ extension SceneDelegate: LoginViewControllerDelegate { } } - func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { + func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) { rootContainer.setEnableSettingsButton(false) TunnelManager.shared.setAccount(action: .new) { operationCompletion in @@ -432,7 +515,10 @@ extension SceneDelegate: LoginViewControllerDelegate { // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to log in with new account.") + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to log in with new account." + ) fallthrough case .cancelled: @@ -449,9 +535,9 @@ extension SceneDelegate: LoginViewControllerDelegate { // Move the settings button back into header bar rootContainer.removeSettingsButtonFromPresentationContainer() - let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints + let relayConstraints = TunnelManager.shared.settings.relayConstraints self.selectLocationViewController?.setSelectedRelayLocation( - relayConstraints?.location.value, + relayConstraints.location.value, animated: false, scrollPosition: .middle ) @@ -484,34 +570,11 @@ extension SceneDelegate: LoginViewControllerDelegate { extension SceneDelegate: SettingsNavigationControllerDelegate { func settingsNavigationController(_ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason) { - switch UIDevice.current.userInterfaceIdiom { - case .phone: - if case .userLoggedOut = reason { - rootContainer.popToRootViewController(animated: false) - - let loginController = rootContainer.topViewController as? LoginViewController - - loginController?.reset() - } + if case .userLoggedOut = reason { + showLoginViewAfterLogout(dismissController: controller) + } else { controller.dismiss(animated: true) - - case .pad: - if case .userLoggedOut = reason { - self.showSplitViewMaster(false, animated: true) - } - - controller.dismiss(animated: true) { - if case .userLoggedOut = reason { - let rootContainerWrapper = self.makeLoginContainerController() - rootContainerWrapper.setViewControllers([self.makeLoginController()], animated: false) - self.rootContainer.present(rootContainerWrapper, animated: true) - } - } - - default: - fatalError() } - } } @@ -565,14 +628,18 @@ extension SceneDelegate: SelectLocationViewControllerDelegate { private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) { let relayConstraints = RelayConstraints(location: .only(relayLocation)) - TunnelManager.shared.setRelayConstraints(relayConstraints) { error in - if let error = error { - self.logger.error(chainedError: error, message: "Failed to update relay constraints") - } else { - self.logger.debug("Updated relay constraints: \(relayConstraints)") + TunnelManager.shared.setRelayConstraints(relayConstraints) { + TunnelManager.shared.startTunnel() + } + } +} - TunnelManager.shared.startTunnel() - } +// MARK: - RevokedDeviceViewControllerDelegate + +extension SceneDelegate: RevokedDeviceViewControllerDelegate { + func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController) { + TunnelManager.shared.unsetAccount { [weak self] in + self?.showLoginViewAfterLogout(dismissController: nil) } } } @@ -640,11 +707,59 @@ extension SceneDelegate: TunnelObserver { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + guard deviceState == .revoked else { return } + + switch UIDevice.current.userInterfaceIdiom { + case .phone: + guard let loginController = rootContainer.viewControllers.first as? LoginViewController else { + return + } + + loginController.reset() + + let viewControllers = [ + loginController, + makeRevokedDeviceController() + ] + + rootContainer.setViewControllers(viewControllers, animated: true) + + case .pad: + guard let loginController = modalRootContainer.viewControllers.first as? LoginViewController else { + return + } + + loginController.reset() + + let viewControllers = [ + loginController, + makeRevokedDeviceController() + ] + + let didDismissSettings = { + self.showSplitViewMaster(false, animated: true) + self.presentModalRootContainerIfNeeded(animated: true) + } + + modalRootContainer.setViewControllers(viewControllers, animated: isModalRootPresented) + + if let settingsNavController = settingsNavController { + settingsNavController.dismiss(animated: true, completion: didDismissSettings) + } else { + didDismissSettings() + } + + default: + fatalError() + } + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } } @@ -654,9 +769,7 @@ extension SceneDelegate: TunnelObserver { extension SceneDelegate: RelayCacheObserver { func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) { - DispatchQueue.main.async { - self.selectLocationViewController?.setCachedRelays(cachedRelays) - } + selectLocationViewController?.setCachedRelays(cachedRelays) } } diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift index 551d78b2f2..203d446b5f 100644 --- a/ios/MullvadVPN/SettingsDataSource.swift +++ b/ios/MullvadVPN/SettingsDataSource.swift @@ -67,7 +67,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab super.init() TunnelManager.shared.addObserver(self) - storedAccountData = TunnelManager.shared.tunnelSettings?.account + storedAccountData = TunnelManager.shared.deviceState.accountData updateDataSnapshot() } @@ -85,7 +85,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab private func updateDataSnapshot() { var newSnapshot = DataSourceSnapshot<Section, Item>() - if TunnelManager.shared.isAccountSet { + if TunnelManager.shared.deviceState.isLoggedIn { newSnapshot.appendSections([.main]) newSnapshot.appendItems([.account, .preferences, .wireguardKey], in: .main) } @@ -121,7 +121,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab value: "Account", comment: "" ) - cell.accountExpiryDate = TunnelManager.shared.accountExpiry + cell.accountExpiryDate = TunnelManager.shared.deviceState.accountData?.expiry cell.accessibilityIdentifier = "AccountCell" cell.disclosureType = .chevron @@ -238,7 +238,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } @@ -246,8 +246,8 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - let newAccountData = tunnelSettings?.account + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + let newAccountData = deviceState.accountData let oldAccountData = storedAccountData storedAccountData = newAccountData @@ -267,4 +267,8 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab updateDataSnapshot() tableView?.reloadData() } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op + } } diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift index 41945b807d..54aa293615 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift +++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift @@ -16,128 +16,169 @@ struct LegacyTunnelSettings { let tunnelSettings: TunnelSettingsV1 } -let keychainServiceName = "Mullvad VPN" +private let keychainServiceName = "Mullvad VPN" -enum KeychainAccountName: String, CaseIterable { +private enum Item: String, CaseIterable { case settings = "Settings" + case deviceState = "DeviceState" case lastUsedAccount = "LastUsedAccount" } -extension SettingsManager { +struct StringDecodingError: LocalizedError { + let data: Data - // MARK: - + var errorDescription: String? { + return "Failed to decode string from data." + } +} - static func getLastUsedAccount() throws -> String { - var query = createDefaultAttributes(accountName: .lastUsedAccount) - query[kSecReturnData] = true +struct StringEncodingError: LocalizedError { + let string: String - var result: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &result) + var errorDescription: String? { + return "Failed to encode string into data." + } +} - guard status == errSecSuccess else { - throw KeychainError(code: status) - } +extension SettingsManager { - let data = result as! Data + // MARK: - Lsat used account - return String(data: data, encoding: .utf8)! + static func getLastUsedAccount() throws -> String { + let data = try readItemData(.lastUsedAccount) + + if let string = String(data: data, encoding: .utf8) { + return string + } else { + throw StringDecodingError(data: data) + } } static func setLastUsedAccount(_ string: String?) throws { - let query = createDefaultAttributes(accountName: .lastUsedAccount) + if let string = string { + guard let data = string.data(using: .utf8) else { + throw StringEncodingError(string: string) + } - guard let string = string else { - switch SecItemDelete(query as CFDictionary) { - case errSecSuccess, errSecItemNotFound: + try addOrUpdateItem(.lastUsedAccount, data: data) + } else { + do { + try deleteItem(.lastUsedAccount) + } catch let error as KeychainError where error == .itemNotFound { return - case let status: - throw KeychainError(code: status) + } catch { + throw error } } + } - let data = string.data(using: .utf8)! - var status = SecItemUpdate( - query as CFDictionary, - [kSecValueData: data] as CFDictionary - ) + // MARK: - Settings - switch status { - case errSecItemNotFound: - var insert = query - insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock - insert[kSecValueData] = data + static func readSettings() throws -> TunnelSettingsV2 { + let data = try readItemData(.settings) - status = SecItemAdd(insert as CFDictionary, nil) - if status != errSecSuccess { - throw KeychainError(code: status) - } - case errSecSuccess: - break - default: - throw KeychainError(code: status) - } + return try JSONDecoder().decode(TunnelSettingsV2.self, from: data) } - // MARK: - + static func writeSettings(_ settings: TunnelSettingsV2) throws { + let data = try JSONEncoder().encode(settings) - static func readSettings() throws -> TunnelSettingsV2 { - var query = createDefaultAttributes(accountName: .settings) - query[kSecReturnData] = true + try addOrUpdateItem(.settings, data: data) + } - var result: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &result) + static func deleteSettings() throws { + try deleteItem(.settings) + } - guard status == errSecSuccess else { - throw KeychainError(code: status) - } + // MARK: - Device state - let data = result as! Data + static func readDeviceState() throws -> DeviceState { + let data = try readItemData(.deviceState) - let decoder = JSONDecoder() - return try decoder.decode(TunnelSettingsV2.self, from: data) + return try JSONDecoder().decode(DeviceState.self, from: data) } - static func writeSettings(_ settings: TunnelSettingsV2) throws { - let encoder = JSONEncoder() - let data = try encoder.encode(settings) + static func writeDeviceState(_ deviceState: DeviceState) throws { + let data = try JSONEncoder().encode(deviceState) + + try addOrUpdateItem(.deviceState, data: data) + } + + + static func deleteDeviceState() throws { + try deleteItem(.deviceState) + } + + // MARK: - Keychain helpers - let query = createDefaultAttributes(accountName: .settings) - var status = SecItemUpdate( + private static func addItem(_ item: Item, data: Data) throws { + var query = createDefaultAttributes(item: item) + query.merge(createAccessAttributes()) { current, _ in + return current + } + query[kSecValueData] = data + + let status = SecItemAdd(query as CFDictionary, nil) + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + + private static func updateItem(_ item: Item, data: Data) throws { + let query = createDefaultAttributes(item: item) + let status = SecItemUpdate( query as CFDictionary, [kSecValueData: data] as CFDictionary ) - switch status { - case errSecItemNotFound: - var insert = query - insert[kSecAttrAccessGroup] = ApplicationConfiguration.securityGroupIdentifier - insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock - insert[kSecValueData] = data + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + private static func addOrUpdateItem(_ item: Item, data: Data) throws { + do { + try updateItem(item, data: data) + } catch let error as KeychainError where error == .itemNotFound { + try addItem(item, data: data) + } catch { + throw error + } + } - status = SecItemAdd(insert as CFDictionary, nil) - if status != errSecSuccess { - throw KeychainError(code: status) - } - case errSecSuccess: - break - default: + private static func readItemData(_ item: Item) throws -> Data { + var query = createDefaultAttributes(item: item) + query[kSecReturnData] = true + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecSuccess { + return result as? Data ?? Data() + } else { throw KeychainError(code: status) } } - static func deleteSettings() throws { - let query = createDefaultAttributes(accountName: .settings) + private static func deleteItem(_ item: Item) throws { + let query = createDefaultAttributes(item: item) let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess { throw KeychainError(code: status) } } - private static func createDefaultAttributes(accountName: KeychainAccountName) -> [CFString: Any] { + private static func createDefaultAttributes(item: Item) -> [CFString: Any] { return [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainServiceName, - kSecAttrAccount: accountName.rawValue + kSecAttrAccount: item.rawValue + ] + } + + private static func createAccessAttributes() -> [CFString: Any] { + return [ + kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock ] } @@ -251,6 +292,6 @@ extension SettingsManager { return false } - return KeychainAccountName(rawValue: accountNumber) == nil + return Item(rawValue: accountNumber) == nil } } diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift index 942e3a27fd..d21194c92f 100644 --- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift @@ -13,17 +13,11 @@ import class WireGuardKitTypes.PrivateKey import struct WireGuardKitTypes.IPAddressRange struct TunnelSettingsV2: Codable, Equatable { - /// Mullvad account data. - var account: StoredAccountData - - /// Device data. - var device: StoredDeviceData - /// Relay constraints. - var relayConstraints: RelayConstraints + var relayConstraints: RelayConstraints = RelayConstraints() /// DNS settings. - var dnsSettings: DNSSettings + var dnsSettings: DNSSettings = DNSSettings() } struct StoredAccountData: Codable, Equatable { @@ -37,6 +31,44 @@ struct StoredAccountData: Codable, Equatable { var expiry: Date } +enum DeviceState: Codable, Equatable { + case loggedIn(StoredAccountData, StoredDeviceData) + case loggedOut + case revoked + + private enum LoggedInCodableKeys: String, CodingKey { + case _0 = "account" + case _1 = "device" + } + + var isLoggedIn: Bool { + switch self { + case .loggedIn: + return true + case .loggedOut, .revoked: + return false + } + } + + var accountData: StoredAccountData? { + switch self { + case .loggedIn(let accountData, _): + return accountData + case .loggedOut, .revoked: + return nil + } + } + + var deviceData: StoredDeviceData? { + switch self { + case .loggedIn(_, let deviceData): + return deviceData + case .loggedOut, .revoked: + return nil + } + } +} + struct StoredDeviceData: Codable, Equatable { /// Device creation date. var creationDate: Date diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift index 131bc97873..0fc5818d59 100644 --- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift @@ -9,12 +9,12 @@ import Foundation import Logging -class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error> { +class LoadTunnelConfigurationOperation: ResultOperation<(), Error> { private let logger = Logger(label: "LoadTunnelConfigurationOperation") - private let state: TunnelManager.State + private let interactor: TunnelInteractor - init(dispatchQueue: DispatchQueue, state: TunnelManager.State) { - self.state = state + init(dispatchQueue: DispatchQueue, interactor: TunnelInteractor) { + self.interactor = interactor super.init(dispatchQueue: dispatchQueue) } @@ -23,7 +23,7 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error> TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in self.dispatchQueue.async { if let error = error { - self.finish(completion: .failure(.loadAllVPNConfigurations(error))) + self.finish(completion: .failure(error)) } else { self.didLoadVPNConfigurations(tunnels: tunnels) } @@ -32,73 +32,101 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error> } private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) { - var returnError: TunnelManager.Error? - var tunnelSettings: TunnelSettingsV2? - do { - tunnelSettings = try SettingsManager.readSettings() - } catch .itemNotFound as KeychainError { - logger.debug("Settings not found in keychain.") - } catch let error as DecodingError { - logger.error( - chainedError: AnyChainedError(error), - message: "Cannot decode settings. Will attempt to delete them from keychain." - ) - - do { - try SettingsManager.deleteSettings() - } catch { - returnError = .deleteSettings(error) - - logger.error( - chainedError: AnyChainedError(error), - message: "Failed to delete settings from keychain." - ) - } - } catch { - returnError = .readSettings(error) - - logger.error( - chainedError: AnyChainedError(error), - message: "Unexpected error when reading settings." - ) - } + let settingsResult = readSettings() + let deviceStateResult = readDeviceState() let tunnel = tunnels?.first.map { tunnelProvider in return Tunnel(tunnelProvider: tunnelProvider) } - if let tunnelSettings = tunnelSettings { - state.tunnelSettings = tunnelSettings - state.setTunnel(tunnel, shouldRefreshTunnelState: true) - state.isLoadedConfiguration = true + let settings = settingsResult.flattenValue() + let deviceState = deviceStateResult.flattenValue() + + interactor.setSettings(settings ?? TunnelSettingsV2(), persist: false) + interactor.setDeviceState(deviceState ?? .loggedOut, persist: false) - finish(completion: .success(())) + if let tunnel = tunnel, deviceState == nil { + logger.debug("Remove orphaned VPN configuration.") + + tunnel.removeFromPreferences { error in + if let error = error { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to remove VPN configuration." + ) + } + self.finishOperation(tunnel: nil) + } } else { - let onFinish = { - self.state.tunnelSettings = nil - self.state.setTunnel(nil, shouldRefreshTunnelState: true) - self.state.isLoadedConfiguration = returnError == nil + finishOperation(tunnel: tunnel) + } + } + private func finishOperation(tunnel: Tunnel?) { + interactor.setTunnel(tunnel, shouldRefreshTunnelState: true) + interactor.setConfigurationLoaded() + + finish(completion: .success(())) + } + + private func readSettings() -> Result<TunnelSettingsV2?, Error> { + return Result { try SettingsManager.readSettings() } + .flatMapError { error in + if let error = error as? KeychainError, error == .itemNotFound { + logger.debug("Settings not found in keychain.") - self.finish(completion: returnError.map { .failure($0) } ?? .success(())) + return .success(nil) + } else if let error = error as? DecodingError { + logger.error( + chainedError: AnyChainedError(error), + message: "Cannot decode settings. Will attempt to delete them from keychain." + ) + + return Result { try SettingsManager.deleteSettings() } + .mapError { error in + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to delete settings from keychain." + ) + + return error + } + .map { _ in + return nil + } + } else { + return .failure(error) + } } + } - if let tunnel = tunnel { - logger.debug("Remove orphaned VPN configuration.") + private func readDeviceState() -> Result<DeviceState?, Error> { + return Result { try SettingsManager.readDeviceState() } + .flatMapError { error in + if let error = error as? KeychainError, error == .itemNotFound { + logger.debug("Device state not found in keychain.") - tunnel.removeFromPreferences { error in - self.dispatchQueue.async { - if let error = error { - self.logger.error( + return .success(nil) + } else if let error = error as? DecodingError { + logger.error( + chainedError: AnyChainedError(error), + message: "Cannot decode device state. Will attempt to delete it from keychain." + ) + + return Result { try SettingsManager.deleteDeviceState() } + .mapError { error in + logger.error( chainedError: AnyChainedError(error), - message: "Failed to remove VPN configuration." + message: "Failed to delete device state from keychain." ) + + return error } - onFinish() - } + .map { _ in + return nil + } + } else { + return .failure(error) } - } else { - onFinish() } - } } } diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index dee1ef6d6b..873968521b 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -11,36 +11,31 @@ import NetworkExtension import Logging class MapConnectionStatusOperation: AsyncOperation { - typealias StartTunnelHandler = () -> Void - - private let state: TunnelManager.State + private let interactor: TunnelInteractor private let connectionStatus: NEVPNStatus - private var startTunnelHandler: StartTunnelHandler? private var request: Cancellable? private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation") init( queue: DispatchQueue, - state: TunnelManager.State, - connectionStatus: NEVPNStatus, - startTunnelHandler: @escaping StartTunnelHandler + interactor: TunnelInteractor, + connectionStatus: NEVPNStatus ) { - self.state = state + self.interactor = interactor self.connectionStatus = connectionStatus - self.startTunnelHandler = startTunnelHandler super.init(dispatchQueue: queue) } override func main() { - guard let tunnel = state.tunnel else { + guard let tunnel = interactor.tunnel else { finish() return } - let tunnelState = state.tunnelStatus.state + let tunnelState = interactor.tunnelStatus.state switch connectionStatus { case .connecting: @@ -48,7 +43,7 @@ class MapConnectionStatusOperation: AsyncOperation { case .connecting(.some(_)): break default: - state.tunnelStatus.state = .connecting(nil) + interactor.updateTunnelState(.connecting(nil)) } updateTunnelRelayAndFinish(tunnel: tunnel) { relay in @@ -76,13 +71,11 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting(.reconnect): logger.debug("Restart the tunnel on disconnect.") - state.tunnelStatus.reset(to: .pendingReconnect) - - startTunnelHandler?() - startTunnelHandler = nil + interactor.resetTunnelState(to: .pendingReconnect) + interactor.startTunnel() default: - state.tunnelStatus.reset(to: .disconnected) + interactor.resetTunnelState(to: .disconnected) } case .disconnecting: @@ -90,11 +83,11 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting: break default: - state.tunnelStatus.reset(to: .disconnecting(.nothing)) + interactor.resetTunnelState(to: .disconnecting(.nothing)) } case .invalid: - state.tunnelStatus.reset(to: .disconnected) + interactor.resetTunnelState(to: .disconnected) @unknown default: logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)") @@ -117,7 +110,7 @@ class MapConnectionStatusOperation: AsyncOperation { self.dispatchQueue.async { if case .success(let packetTunnelStatus) = completion, !self.isCancelled { - self.state.tunnelStatus.update( + self.interactor.updateTunnelStatus( from: packetTunnelStatus, mappingRelayToState: mapRelayToState ) diff --git a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift index 61327ec409..d67f8e37e8 100644 --- a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift @@ -200,13 +200,13 @@ class MigrateSettingsOperation: AsyncOperation { logger.debug("Store new settings...") // Create new settings. - let newSettings = TunnelSettingsV2( - account: StoredAccountData( + let newDeviceState = DeviceState.loggedIn( + StoredAccountData( identifier: accountData.id, number: settings.accountNumber, expiry: accountData.expiry ), - device: StoredDeviceData( + StoredDeviceData( creationDate: device.created, identifier: device.id, name: device.name, @@ -217,7 +217,10 @@ class MigrateSettingsOperation: AsyncOperation { creationDate: privateKeyWithMetadata.creationDate, privateKey: privateKeyWithMetadata.privateKey ) - ), + ) + ) + + let newSettings = TunnelSettingsV2( relayConstraints: tunnelSettings.relayConstraints, dnsSettings: interfaceData.dnsSettings ) @@ -225,6 +228,7 @@ class MigrateSettingsOperation: AsyncOperation { // Save settings. do { try SettingsManager.writeSettings(newSettings) + try SettingsManager.writeDeviceState(newDeviceState) } catch { logger.error( chainedError: AnyChainedError(error), diff --git a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift index 0a2995d4c1..e48d17482a 100644 --- a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift @@ -8,27 +8,26 @@ import Foundation -class ReconnectTunnelOperation: ResultOperation<(), TunnelManager.Error> { - private let state: TunnelManager.State +class ReconnectTunnelOperation: ResultOperation<(), Error> { + private let interactor: TunnelInteractor private let selectNewRelay: Bool private var task: Cancellable? init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, selectNewRelay: Bool ) { - self.state = state + self.interactor = interactor self.selectNewRelay = selectNewRelay super.init(dispatchQueue: dispatchQueue) } override func main() { - guard let tunnel = self.state.tunnel, - let relayConstraints = state.tunnelSettings?.relayConstraints else { - finish(completion: .failure(.unsetTunnel)) + guard let tunnel = interactor.tunnel else { + finish(completion: .failure(UnsetTunnelError())) return } @@ -39,17 +38,17 @@ class ReconnectTunnelOperation: ResultOperation<(), TunnelManager.Error> { let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays() selectorResult = try RelaySelector.evaluate( relays: cachedRelays.relays, - constraints: relayConstraints + constraints: interactor.settings.relayConstraints ) } task = tunnel.reconnectTunnel( relaySelectorResult: selectorResult ) { [weak self] completion in - self?.finish(completion: completion.mapError { .reloadTunnel($0) }) + self?.finish(completion: completion) } } catch { - finish(completion: .failure(.reloadTunnel(error))) + finish(completion: .failure(error)) } } diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift index d8a0a4fed1..ae5470b75e 100644 --- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift +++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift @@ -10,8 +10,8 @@ import Foundation import Logging import class WireGuardKitTypes.PrivateKey -class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> { - private let state: TunnelManager.State +class RotateKeyOperation: ResultOperation<Bool, Error> { + private let interactor: TunnelInteractor private let devicesProxy: REST.DevicesProxy private var task: Cancellable? @@ -21,31 +21,29 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> { init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, devicesProxy: REST.DevicesProxy, - rotationInterval: TimeInterval?, - completionHandler: @escaping CompletionHandler + rotationInterval: TimeInterval? ) { - self.state = state - + self.interactor = interactor self.devicesProxy = devicesProxy self.rotationInterval = rotationInterval super.init( dispatchQueue: dispatchQueue, - completionQueue: dispatchQueue, - completionHandler: completionHandler + completionQueue: nil, + completionHandler: nil ) } override func main() { - guard let tunnelSettings = state.tunnelSettings else { - finish(completion: .failure(.unsetAccount)) + guard case .loggedIn(let accountData, let deviceData) = interactor.deviceState else { + finish(completion: .failure(InvalidDeviceStateError())) return } if let rotationInterval = rotationInterval { - let creationDate = tunnelSettings.device.wgKeyData.creationDate + let creationDate = deviceData.wgKeyData.creationDate let nextRotationDate = creationDate.addingTimeInterval(rotationInterval) if nextRotationDate > Date() { @@ -65,14 +63,13 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> { let newPrivateKey = PrivateKey() task = devicesProxy.rotateDeviceKey( - accountNumber: tunnelSettings.account.number, - identifier: tunnelSettings.device.identifier, + accountNumber: accountData.number, + identifier: deviceData.identifier, publicKey: newPrivateKey.publicKey, retryStrategy: .default ) { completion in self.dispatchQueue.async { self.didRotateKey( - tunnelSettings: tunnelSettings, newPrivateKey: newPrivateKey, completion: completion ) @@ -86,47 +83,39 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> { } private func didRotateKey( - tunnelSettings: TunnelSettingsV2, newPrivateKey: PrivateKey, completion: OperationCompletion<REST.Device, REST.Error> ) { - let mappedCompletion = completion.mapError { error -> TunnelManager.Error in - logger.error( - chainedError: error, - message: "Failed to rotate device key." - ) + switch completion { + case .success(let device): + logger.debug("Successfully rotated device key. Persisting settings...") - return .rotateKey(error) - } - - guard let device = mappedCompletion.value else { - finish(completion: mappedCompletion.assertNoSuccess()) - return - } - - logger.debug("Successfully rotated device key. Persisting settings...") - - do { - var newTunnelSettings = tunnelSettings - newTunnelSettings.device.update(from: device) - newTunnelSettings.device.wgKeyData = StoredWgKeyData( - creationDate: Date(), - privateKey: newPrivateKey - ) + switch interactor.deviceState { + case .loggedIn(let accountData, var deviceData): + deviceData.update(from: device) + deviceData.wgKeyData = StoredWgKeyData( + creationDate: Date(), + privateKey: newPrivateKey + ) - try SettingsManager.writeSettings(newTunnelSettings) + interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true) - state.tunnelSettings = newTunnelSettings + finish(completion: .success(true)) + default: + finish(completion: .failure(InvalidDeviceStateError())) + } - finish(completion: .success(true)) - } catch { + case .failure(let error): logger.error( chainedError: AnyChainedError(error), - message: "Failed to write settings." + message: "Failed to rotate device key." ) + finish(completion: .failure(error)) - finish(completion: .failure(.writeSettings(error))) + case .cancelled: + finish(completion: .cancelled) } + } } diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index c92c6a1c0b..022264489d 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -59,14 +59,11 @@ private struct SetAccountContext: OperationInputContext { } } -class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Error> { - typealias WillDeleteVPNConfigurationHandler = () -> Void - - private let state: TunnelManager.State +class SetAccountOperation: ResultOperation<StoredAccountData?, Error> { + private let interactor: TunnelInteractor private let accountsProxy: REST.AccountsProxy private let devicesProxy: REST.DevicesProxy private let action: SetAccountAction - private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler? private let logger = Logger(label: "SetAccountOperation") private let operationQueue = AsyncOperationQueue() @@ -75,36 +72,37 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy, - action: SetAccountAction, - willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler + action: SetAccountAction ) { - self.state = state + self.interactor = interactor self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy self.action = action - self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler super.init(dispatchQueue: dispatchQueue) } override func main() { var deleteDeviceOperation: AsyncOperation? - if let tunnelSettings = state.tunnelSettings { - let operation = getDeleteDeviceOperation(tunnelSettings: tunnelSettings) + if case .loggedIn(let accountData, let deviceData) = interactor.deviceState { + let operation = getDeleteDeviceOperation( + accounNumber: accountData.number, + deviceIdentifier: deviceData.identifier + ) deleteDeviceOperation = operation } - let deleteSettingsOperation = getDeleteSettingsOperation() - deleteSettingsOperation.addCondition( + let unsetDeviceStateOperation = getUnsetDeviceStateOperation() + unsetDeviceStateOperation.addCondition( NoFailedDependenciesCondition(ignoreCancellations: false) ) if let deleteDeviceOperation = deleteDeviceOperation { - deleteSettingsOperation.addDependency(deleteDeviceOperation) + unsetDeviceStateOperation.addDependency(deleteDeviceOperation) } let setupAccountOperations = getAccountDataOperation() @@ -112,7 +110,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err accountOperation.addCondition( NoFailedDependenciesCondition(ignoreCancellations: false) ) - accountOperation.addDependency(deleteSettingsOperation) + accountOperation.addDependency(unsetDeviceStateOperation) let createDeviceOperation = getCreateDeviceOperation() createDeviceOperation.addCondition( @@ -144,7 +142,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err return [accountOperation, createDeviceOperation, saveSettingsOperation] } ?? [] - var enqueueOperations: [Operation] = [deleteDeviceOperation, deleteSettingsOperation] + var enqueueOperations: [Operation] = [deleteDeviceOperation, unsetDeviceStateOperation] .compactMap { $0 } enqueueOperations.append(contentsOf: setupAccountOperations) @@ -173,8 +171,8 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err return } - let errors = children.compactMap { operation -> TunnelManager.Error? in - return (operation as? AsyncOperation)?.error as? TunnelManager.Error + let errors = children.compactMap { operation -> Error? in + return (operation as? AsyncOperation)?.error } if let error = errors.first { @@ -184,9 +182,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err } } - private func getAccountDataOperation() - -> ResultOperation<StoredAccountData, TunnelManager.Error>? - { + private func getAccountDataOperation() -> ResultOperation<StoredAccountData, Error>? { switch action { case .new: return getCreateAccountOperation() @@ -199,41 +195,30 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err } } - private func getCreateAccountOperation() - -> ResultBlockOperation<StoredAccountData, TunnelManager.Error> - { - let operation = ResultBlockOperation< - StoredAccountData, - TunnelManager.Error - >(dispatchQueue: dispatchQueue) + private func getCreateAccountOperation() -> ResultBlockOperation<StoredAccountData, Error> { + let operation = ResultBlockOperation<StoredAccountData, Error>(dispatchQueue: dispatchQueue) operation.setExecutionBlock { operation in self.logger.debug("Create new account...") let task = self.accountsProxy.createAccount(retryStrategy: .default) { completion in - let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + let mappedCompletion = completion.mapError { error -> Error in self.logger.error( chainedError: AnyChainedError(error), message: "Failed to create new account." ) + return error + }.map { newAccountData -> StoredAccountData in + self.logger.debug("Created new account.") - return .createAccount(error) - } - - guard let newAccountData = mappedCompletion.value else { - operation.finish(completion: mappedCompletion.assertNoSuccess()) - return + return StoredAccountData( + identifier: newAccountData.id, + number: newAccountData.number, + expiry: newAccountData.expiry + ) } - self.logger.debug("Created new account.") - - let storedAccountData = StoredAccountData( - identifier: newAccountData.id, - number: newAccountData.number, - expiry: newAccountData.expiry - ) - - operation.finish(completion: .success(storedAccountData)) + operation.finish(completion: mappedCompletion) } operation.addCancellationBlock { @@ -245,11 +230,9 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err } private func getExistingAccountOperation(accountNumber: String) - -> ResultOperation<StoredAccountData, TunnelManager.Error> + -> ResultOperation<StoredAccountData, Error> { - let operation = ResultBlockOperation<StoredAccountData, TunnelManager.Error>( - dispatchQueue: dispatchQueue - ) + let operation = ResultBlockOperation<StoredAccountData, Error>(dispatchQueue: dispatchQueue) operation.setExecutionBlock { operation in self.logger.debug("Request account data...") @@ -258,29 +241,23 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err accountNumber: accountNumber, retryStrategy: .default ) { completion in - let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + let mappedCompletion = completion.mapError { error -> Error in self.logger.error( chainedError: AnyChainedError(error), message: "Failed to receive account data." ) + return error + }.map { accountData -> StoredAccountData in + self.logger.debug("Received account data.") - return .getAccountData(error) - } - - guard let accountData = mappedCompletion.value else { - operation.finish(completion: mappedCompletion.assertNoSuccess()) - return + return StoredAccountData( + identifier: accountData.id, + number: accountNumber, + expiry: accountData.expiry + ) } - self.logger.debug("Received account data.") - - let storedAccountData = StoredAccountData( - identifier: accountData.id, - number: accountNumber, - expiry: accountData.expiry - ) - - operation.finish(completion: .success(storedAccountData)) + operation.finish(completion: mappedCompletion) } operation.addCancellationBlock { @@ -291,10 +268,10 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err return operation } - private func getDeleteDeviceOperation(tunnelSettings: TunnelSettingsV2) - -> ResultBlockOperation<Void, TunnelManager.Error> + private func getDeleteDeviceOperation(accounNumber: String, deviceIdentifier: String) + -> ResultBlockOperation<Void, Error> { - let operation = ResultBlockOperation<Void, TunnelManager.Error>( + let operation = ResultBlockOperation<Void, Error>( dispatchQueue: dispatchQueue ) @@ -302,28 +279,26 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err self.logger.debug("Delete current device...") let task = self.devicesProxy.deleteDevice( - accountNumber: tunnelSettings.account.number, - identifier: tunnelSettings.device.identifier, + accountNumber: accounNumber, + identifier: deviceIdentifier, retryStrategy: .default ) { completion in - let mappedCompletion = completion.mapError { error -> TunnelManager.Error in - self.logger.error(chainedError: error, message: "Failed to delete device.") - - return .deleteDevice(error) - } - - guard let isDeleted = mappedCompletion.value else { - operation.finish(completion: mappedCompletion.assertNoSuccess()) - return - } - - if isDeleted { - self.logger.debug("Deleted device.") - } else { - self.logger.debug("Device is already deleted.") - } + let mappedCompletion = completion + .mapError { error -> Error in + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to delete device." + ) + return error + }.map { isDeleted in + if isDeleted { + self.logger.debug("Deleted device.") + } else { + self.logger.debug("Device is already deleted.") + } + } - operation.finish(completion: .success(())) + operation.finish(completion: mappedCompletion) } operation.addCancellationBlock { @@ -334,47 +309,25 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err return operation } - private func getDeleteSettingsOperation() -> ResultBlockOperation<Void, TunnelManager.Error> { - let deleteSettingsOperation = ResultBlockOperation<Void, TunnelManager.Error>( - dispatchQueue: dispatchQueue - ) - - deleteSettingsOperation.setExecutionBlock { operation in - self.logger.debug("Delete settings.") - - do { - try SettingsManager.deleteSettings() - } catch .itemNotFound as KeychainError { - self.logger.debug("Settings are already deleted.") - } catch { - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to delete settings." - ) - operation.finish(completion: .failure(.deleteSettings(error))) - return - } - + private func getUnsetDeviceStateOperation() -> AsyncBlockOperation { + return AsyncBlockOperation(dispatchQueue: dispatchQueue) { operation in // Tell the caller to unsubscribe from VPN status notifications. - self.willDeleteVPNConfigurationHandler?() - self.willDeleteVPNConfigurationHandler = nil + self.interactor.prepareForVPNConfigurationDeletion() - // Reset tunnel state to disconnected - self.state.tunnelStatus.reset(to: .disconnected) - - // Remove tunnel settins - self.state.tunnelSettings = nil + // Reset tunnel and device state. + self.interactor.resetTunnelState(to: .disconnected) + self.interactor.setDeviceState(.loggedOut, persist: true) // Finish immediately if tunnel provider is not set. - guard let tunnel = self.state.tunnel else { - operation.finish(completion: .success(())) + guard let tunnel = self.interactor.tunnel else { + operation.finish() return } - // Remove VPN configuration + // Remove VPN configuration. tunnel.removeFromPreferences { error in self.dispatchQueue.async { - // Ignore error but log it + // Ignore error but log it. if let error = error { self.logger.error( chainedError: AnyChainedError(error), @@ -382,23 +335,21 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err ) } - self.state.setTunnel(nil, shouldRefreshTunnelState: false) + self.interactor.setTunnel(nil, shouldRefreshTunnelState: false) - operation.finish(completion: .success(())) + operation.finish() } } } - - return deleteSettingsOperation } private func getCreateDeviceOperation() - -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device), TunnelManager.Error> + -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device), Error> { let createDeviceOperation = TransformOperation< StoredAccountData, (PrivateKey, REST.Device), - TunnelManager.Error + Error >(dispatchQueue: dispatchQueue) createDeviceOperation.setExecutionBlock { storedAccountData, operation in @@ -431,9 +382,9 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err .map { device in return (privateKey, device) } - .mapError { error -> TunnelManager.Error in + .mapError { error -> Error in self.logger.error(chainedError: error, message: "Failed to create device.") - return .createDevice(error) + return error } operation.finish(completion: mappedCompletion) @@ -448,21 +399,19 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err } private func getSaveSettingsOperation() - -> TransformOperation<SetAccountResult, StoredAccountData, TunnelManager.Error> + -> TransformOperation<SetAccountResult, StoredAccountData, Error> { let saveSettingsOperation = TransformOperation< - SetAccountResult, - StoredAccountData, - TunnelManager.Error + SetAccountResult, StoredAccountData, Error >(dispatchQueue: dispatchQueue) saveSettingsOperation.setExecutionBlock { input in self.logger.debug("Saving settings...") let device = input.device - let tunnelSettings = TunnelSettingsV2( - account: input.accountData, - device: StoredDeviceData( + let newDeviceState = DeviceState.loggedIn( + input.accountData, + StoredDeviceData( creationDate: device.created, identifier: device.id, name: device.name, @@ -473,25 +422,13 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err creationDate: Date(), privateKey: input.privateKey ) - ), - relayConstraints: RelayConstraints(), - dnsSettings: DNSSettings() + ) ) - do { - try SettingsManager.writeSettings(tunnelSettings) - - self.state.tunnelSettings = tunnelSettings + self.interactor.setSettings(TunnelSettingsV2(), persist: true) + self.interactor.setDeviceState(newDeviceState, persist: true) - return input.accountData - } catch { - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to write settings." - ) - - throw TunnelManager.Error.writeSettings(error) - } + return input.accountData } return saveSettingsOperation diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 2ffa3cb9d6..cb3a738490 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -8,22 +8,21 @@ import Foundation import NetworkExtension +import Logging -class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { +class StartTunnelOperation: ResultOperation<(), Error> { typealias EncodeErrorHandler = (Error) -> Void - private let state: TunnelManager.State - private var encodeErrorHandler: EncodeErrorHandler? + private let interactor: TunnelInteractor + private let logger = Logger(label: "StartTunnelOperation") init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, - encodeErrorHandler: @escaping EncodeErrorHandler, + interactor: TunnelInteractor, completionHandler: @escaping CompletionHandler ) { - self.state = state - self.encodeErrorHandler = encodeErrorHandler + self.interactor = interactor super.init( dispatchQueue: dispatchQueue, @@ -33,118 +32,119 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { } override func main() { - guard let tunnelSettings = state.tunnelSettings else { - finish(completion: .failure(.unsetAccount)) + guard case .loggedIn = interactor.deviceState else { + finish(completion: .failure(InvalidDeviceStateError())) return } - switch state.tunnelStatus.state { + switch interactor.tunnelStatus.state { case .disconnecting(.nothing): - state.tunnelStatus.state = .disconnecting(.reconnect) + interactor.updateTunnelState(.disconnecting(.reconnect)) finish(completion: .success(())) case .disconnected, .pendingReconnect: - guard let cachedRelays = try? RelayCache.Tracker.shared.getCachedRelays() else { - finish(completion: .failure(.readRelays)) - return - } + do { + let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays() + let selectorResult = try RelaySelector.evaluate( + relays: cachedRelays.relays, + constraints: interactor.settings.relayConstraints + ) - didReceiveRelays( - tunnelSettings: tunnelSettings, - cachedRelays: cachedRelays - ) + makeTunnelProviderAndStartTunnel(selectorResult: selectorResult) { error in + self.finish(completion: OperationCompletion(error: error)) + } + } catch { + finish(completion: .failure(error)) + } default: - // Do not attempt to start the tunnel in all other cases. finish(completion: .success(())) } } - private func didReceiveRelays(tunnelSettings: TunnelSettingsV2, cachedRelays: RelayCache.CachedRelays) { - guard let selectorResult = try? RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: tunnelSettings.relayConstraints - ) else { - finish(completion: .failure(.cannotSatisfyRelayConstraints)) - return - } - - Self.makeTunnelProvider { makeTunnelProviderResult in + private func makeTunnelProviderAndStartTunnel( + selectorResult: RelaySelectorResult, + completionHandler: @escaping (Error?) -> Void + ) { + Self.makeTunnelProvider { result in self.dispatchQueue.async { - switch makeTunnelProviderResult { - case .success(let tunnelProvider): - let startTunnelResult = Result { try self.startTunnel(tunnelProvider: tunnelProvider, selectorResult: selectorResult) } - .mapError { error -> TunnelManager.Error in - return .startVPNTunnel(error) - } + do { + let tunnelProvider = try result.get() - self.finish(completion: OperationCompletion(result: startTunnelResult)) + try self.startTunnel( + tunnelProvider: tunnelProvider, + selectorResult: selectorResult + ) - case .failure(let error): - self.finish(completion: .failure(error)) + completionHandler(nil) + } catch { + completionHandler(error) } } } } - private func startTunnel(tunnelProvider: TunnelProviderManagerType, selectorResult: RelaySelectorResult) throws { + private func startTunnel( + tunnelProvider: TunnelProviderManagerType, + selectorResult: RelaySelectorResult + ) throws { var tunnelOptions = PacketTunnelOptions() do { try tunnelOptions.setSelectorResult(selectorResult) } catch { - encodeErrorHandler?(error) + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to encode the selector result." + ) } - encodeErrorHandler = nil - - state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false) - state.tunnelStatus.reset(to: .connecting(selectorResult.packetTunnelRelay)) + interactor.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false) + interactor.resetTunnelState(to: .connecting(selectorResult.packetTunnelRelay)) try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) } - private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) { + private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, Error>) -> Void) { TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in if let error = error { - completionHandler(.failure(.loadAllVPNConfigurations(error))) + completionHandler(.failure(error)) return } - let protocolConfig = NETunnelProviderProtocol() - protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier - protocolConfig.serverAddress = "" - let tunnelProvider = tunnelProviders?.first ?? TunnelProviderManagerType() - tunnelProvider.isEnabled = true - tunnelProvider.localizedDescription = "WireGuard" - tunnelProvider.protocolConfiguration = protocolConfig - // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular. - let alwaysOnRule = NEOnDemandRuleConnect() - alwaysOnRule.interfaceTypeMatch = .any - tunnelProvider.onDemandRules = [alwaysOnRule] - tunnelProvider.isOnDemandEnabled = true + configureTunnelProvider(tunnelProvider) tunnelProvider.saveToPreferences { error in if let error = error { - completionHandler(.failure(.saveVPNConfiguration(error))) - return - } - - // Refresh connection status after saving the tunnel preferences. - // Basically it's only necessary to do for new instances of - // `NETunnelProviderManager`, but we do that for the existing ones too - // for simplicity as it has no side effects. - tunnelProvider.loadFromPreferences { error in - if let error = error { - completionHandler(.failure(.reloadVPNConfiguration(error))) - } else { - completionHandler(.success(tunnelProvider)) + completionHandler(.failure(error)) + } else { + // Refresh connection status after saving the tunnel preferences. + // Basically it's only necessary to do for new instances of + // `NETunnelProviderManager`, but we do that for the existing ones too + // for simplicity as it has no side effects. + tunnelProvider.loadFromPreferences { error in + completionHandler(error.map { .failure($0) } ?? .success(tunnelProvider)) } } } } } + + private class func configureTunnelProvider(_ tunnelProvider: TunnelProviderManagerType) { + let protocolConfig = NETunnelProviderProtocol() + protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier + protocolConfig.serverAddress = "" + + tunnelProvider.isEnabled = true + tunnelProvider.localizedDescription = "WireGuard" + tunnelProvider.protocolConfiguration = protocolConfig + + let alwaysOnRule = NEOnDemandRuleConnect() + alwaysOnRule.interfaceTypeMatch = .any + tunnelProvider.onDemandRules = [alwaysOnRule] + tunnelProvider.isOnDemandEnabled = true + } } diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 4ca000562d..bd0501f114 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -8,16 +8,16 @@ import Foundation -class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> { - private let state: TunnelManager.State +class StopTunnelOperation: ResultOperation<(), Error> { + private let interactor: TunnelInteractor init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, completionHandler: @escaping CompletionHandler ) { - self.state = state + self.interactor = interactor super.init( dispatchQueue: dispatchQueue, @@ -27,25 +27,25 @@ class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> { } override func main() { - guard let tunnel = state.tunnel else { - finish(completion: .failure(.unsetTunnel)) - return - } - - switch state.tunnelStatus.state { + switch interactor.tunnelStatus.state { case .disconnecting(.reconnect): - state.tunnelStatus.state = .disconnecting(.nothing) + interactor.updateTunnelState(.disconnecting(.nothing)) finish(completion: .success(())) case .connected, .connecting, .reconnecting: + guard let tunnel = interactor.tunnel else { + finish(completion: .failure(UnsetTunnelError())) + return + } + // Disable on-demand when stopping the tunnel to prevent it from coming back up tunnel.isOnDemandEnabled = false tunnel.saveToPreferences { error in self.dispatchQueue.async { if let error = error { - self.finish(completion: .failure(.saveVPNConfiguration(error))) + self.finish(completion: .failure(error)) } else { tunnel.stop() self.finish(completion: .success(())) diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift new file mode 100644 index 0000000000..b3d2b7b495 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -0,0 +1,45 @@ +// +// TunnelInteractor.swift +// MullvadVPN +// +// Created by pronebird on 05/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol TunnelInteractor { + + // MARK: - Tunnel manipulation + + var tunnel: Tunnel? { get } + func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) + + // MARK: - Tunnel status manipulation + + var tunnelStatus: TunnelStatus { get } + + func setTunnelStatus(_ tunnelStatus: TunnelStatus) + func updateTunnelStatus( + from packetTunnelStatus: PacketTunnelStatus, + mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? + ) + + // MARK: - Tunnel state + + func updateTunnelState(_ state: TunnelState) + func resetTunnelState(to state: TunnelState) + + // MARK: - Configuration + + var isConfigurationLoaded: Bool { get } + var settings: TunnelSettingsV2 { get } + var deviceState: DeviceState { get } + + func setConfigurationLoaded() + func setSettings(_ settings: TunnelSettingsV2, persist: Bool) + func setDeviceState(_ deviceState: DeviceState, persist: Bool) + + func startTunnel() + func prepareForVPNConfigurationDeletion() +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 6b82105427..0b1236f743 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -38,12 +38,16 @@ enum TunnelManagerConfiguration { /// A class that provides a convenient interface for VPN tunnels configuration, manipulation and /// monitoring. -final class TunnelManager: TunnelManagerStateDelegate { - /// Operation categories - private enum OperationCategory { - static let manageTunnelProvider = "TunnelManager.manageTunnelProvider" - static let changeTunnelSettings = "TunnelManager.changeTunnelSettings" - static let tunnelStateUpdate = "TunnelManager.tunnelStateUpdate" +final class TunnelManager { + private enum OperationCategory: String { + case manageTunnel + case deviceStateUpdate + case settingsUpdate + case tunnelStateUpdate + + var category: String { + return "TunnelManager.\(rawValue)" + } } static let shared: TunnelManager = { @@ -59,19 +63,18 @@ final class TunnelManager: TunnelManagerStateDelegate { private let devicesProxy: REST.DevicesProxy private let logger = Logger(label: "TunnelManager") - private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue") + private var nslock = NSRecursiveLock() private let operationQueue = AsyncOperationQueue() + private let internalQueue = DispatchQueue(label: "TunnelManager.internalQueue") private var statusObserver: TunnelStatusBlockObserver? private var lastMapConnectionStatusOperation: Operation? private let observerList = ObserverList<TunnelObserver>() - private let state: TunnelManager.State - private var privateKeyRotationTimer: DispatchSourceTimer? private var lastKeyRotationData: ( attempt: Date, - completion: OperationCompletion<Bool, TunnelManager.Error> + completion: OperationCompletion<Bool, Error> )? private var isRunningPeriodicPrivateKeyRotation = false @@ -79,116 +82,71 @@ final class TunnelManager: TunnelManagerStateDelegate { private var isPolling = false private var lastConnectingDate: Date? - var isLoadedConfiguration: Bool { - return state.isLoadedConfiguration - } - - var accountNumber: String? { - return state.tunnelSettings?.account.number - } - - var accountExpiry: Date? { - return state.tunnelSettings?.account.expiry - } - - var isAccountSet: Bool { - return state.tunnelSettings != nil - } - - var device: StoredDeviceData? { - return state.tunnelSettings?.device - } + private var _isConfigurationLoaded = false + private var _deviceState: DeviceState = .loggedOut + private var _tunnelSettings = TunnelSettingsV2() - var tunnelSettings: TunnelSettingsV2? { - return state.tunnelSettings - } + private var _tunnel: Tunnel? + private var _tunnelStatus = TunnelStatus() - var tunnelState: TunnelState { - return state.tunnelStatus.state - } + // MARK: - Initialization private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) { self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy - self.state = TunnelManager.State(delegateQueue: stateQueue) - self.state.delegate = self self.operationQueue.name = "TunnelManager.operationQueue" - self.operationQueue.underlyingQueue = stateQueue + self.operationQueue.underlyingQueue = internalQueue } // MARK: - Periodic private key rotation func startPeriodicPrivateKeyRotation() { - stateQueue.async { - guard !self.isRunningPeriodicPrivateKeyRotation else { return } - - self.logger.debug("Start periodic private key rotation.") + nslock.lock() + defer { nslock.unlock() } - self.isRunningPeriodicPrivateKeyRotation = true - self.updatePrivateKeyRotationTimer() - } - } - - func stopPeriodicPrivateKeyRotation() { - stateQueue.async { - guard self.isRunningPeriodicPrivateKeyRotation else { return } + guard !isRunningPeriodicPrivateKeyRotation else { return } - self.logger.debug("Stop periodic private key rotation.") + logger.debug("Start periodic private key rotation.") - self.isRunningPeriodicPrivateKeyRotation = false - self.updatePrivateKeyRotationTimer() - } - } + isRunningPeriodicPrivateKeyRotation = true + updatePrivateKeyRotationTimer() - func getNextKeyRotationDate() -> Date? { - return stateQueue.sync { - return _getNextKeyRotationDate() - } + nslock.unlock() } - private func updatePrivateKeyRotationTimer() { - dispatchPrecondition(condition: .onQueue(stateQueue)) - - privateKeyRotationTimer?.cancel() - privateKeyRotationTimer = nil - - guard self.isRunningPeriodicPrivateKeyRotation else { return } - - guard let scheduleDate = _getNextKeyRotationDate() else { return } - - let timer = DispatchSource.makeTimerSource(queue: stateQueue) - - timer.setEventHandler { [weak self] in - guard let self = self else { return } - - _ = self.rotatePrivateKey(forceRotate: false) { _ in - // no-op - } - } + func stopPeriodicPrivateKeyRotation() { + nslock.lock() + defer { nslock.unlock() } - timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow) - timer.activate() + guard isRunningPeriodicPrivateKeyRotation else { return } - privateKeyRotationTimer = timer + logger.debug("Stop periodic private key rotation.") - logger.debug("Schedule next private key rotation at \(scheduleDate.logFormatDate()).") + isRunningPeriodicPrivateKeyRotation = false + updatePrivateKeyRotationTimer() } - private func _getNextKeyRotationDate() -> Date? { - guard let tunnelSettings = state.tunnelSettings else { + func getNextKeyRotationDate() -> Date? { + nslock.lock() + defer { nslock.unlock() } + + guard case .loggedIn(_, let deviceData) = deviceState else { return nil } if case .some(let (lastAttemptDate, completion)) = lastKeyRotationData { - // Do not rotate the key when logged out. - if case .unsetAccount = completion.error { + if completion.error is InvalidDeviceStateError { + return nil + } + + if completion.error is RevokedDeviceError { return nil } // Do not rotate the key if account or device is not found. - if case .rotateKey(.unhandledResponse(_, let serverErrorResponse)) = completion.error, - serverErrorResponse?.code == .invalidAccount || - serverErrorResponse?.code == .deviceNotFound { + if let restError = completion.error as? REST.Error, + restError.compareErrorCode(.invalidAccount) || + restError.compareErrorCode(.deviceNotFound) { return nil } @@ -203,21 +161,49 @@ final class TunnelManager: TunnelManagerStateDelegate { } // Rotate at long intervals otherwise. - let date = tunnelSettings.device.wgKeyData.creationDate + let date = deviceData.wgKeyData.creationDate .addingTimeInterval(TunnelManagerConfiguration.privateKeyRotationInterval) return max(date, Date()) } - private func setFinishedKeyRotation(_ completion: OperationCompletion<Bool, TunnelManager.Error>) { - dispatchPrecondition(condition: .onQueue(stateQueue)) + private func updatePrivateKeyRotationTimer() { + nslock.lock() + defer { nslock.unlock() } + + privateKeyRotationTimer?.cancel() + privateKeyRotationTimer = nil + + guard isRunningPeriodicPrivateKeyRotation, + let scheduleDate = getNextKeyRotationDate() else { return } + + let timer = DispatchSource.makeTimerSource(queue: .main) + + timer.setEventHandler { [weak self] in + _ = self?.rotatePrivateKey(forceRotate: false) { _ in + // no-op + } + } + + timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow) + timer.activate() + + privateKeyRotationTimer = timer + + logger.debug("Schedule next private key rotation at \(scheduleDate.logFormatDate()).") + } + + private func setFinishedKeyRotation(_ completion: OperationCompletion<Bool, Error>) { + nslock.lock() + defer { nslock.unlock() } lastKeyRotationData = (Date(), completion) updatePrivateKeyRotationTimer() } private func resetKeyRotationData() { - dispatchPrecondition(condition: .onQueue(stateQueue)) + nslock.lock() + defer { nslock.unlock() } lastKeyRotationData = nil updatePrivateKeyRotationTimer() @@ -226,32 +212,31 @@ final class TunnelManager: TunnelManagerStateDelegate { // MARK: - Public methods - func loadConfiguration(completionHandler: @escaping (TunnelManager.Error?) -> Void) { + func loadConfiguration(completionHandler: @escaping (Error?) -> Void) { let migrateSettingsOperation = MigrateSettingsOperation( - dispatchQueue: stateQueue, + dispatchQueue: internalQueue, accountsProxy: accountsProxy, devicesProxy: devicesProxy ) let loadTunnelOperation = LoadTunnelConfigurationOperation( - dispatchQueue: stateQueue, - state: state + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self) ) - loadTunnelOperation.completionQueue = stateQueue + loadTunnelOperation.completionQueue = .main loadTunnelOperation.completionHandler = { [weak self] completion in guard let self = self else { return } - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - if case .failure(let error) = completion { - self.logger.error(chainedError: error, message: "Failed to load tunnel.") + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to load configuration." + ) } self.updatePrivateKeyRotationTimer() - DispatchQueue.main.async { - completionHandler(completion.error) - } + completionHandler(completion.error) } loadTunnelOperation.addDependency(migrateSettingsOperation) @@ -264,156 +249,131 @@ final class TunnelManager: TunnelManagerStateDelegate { ) groupOperation.addCondition( - MutuallyExclusive(category: OperationCategory.manageTunnelProvider) + MutuallyExclusive(category: OperationCategory.manageTunnel.category) + ) + groupOperation.addCondition( + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) ) groupOperation.addCondition( - MutuallyExclusive(category: OperationCategory.changeTunnelSettings) + MutuallyExclusive(category: OperationCategory.settingsUpdate.category) ) operationQueue.addOperation(groupOperation) } func refreshTunnelStatus() { - stateQueue.async { - self.logger.debug("Refresh tunnel status due to application becoming active.") - self._refreshTunnelStatus() - } + logger.debug("Refresh tunnel status due to application becoming active.") + _refreshTunnelStatus() } func startTunnel() { let operation = StartTunnelOperation( - dispatchQueue: stateQueue, - state: state, - encodeErrorHandler: { [weak self] error in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - self.logger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel options") - }, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), completionHandler: { [weak self] completion in - guard let self = self else { return } + guard let self = self, let error = completion.error else { return } - dispatchPrecondition(condition: .onQueue(self.stateQueue)) + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to start the tunnel." + ) + + DispatchQueue.main.async { + let tunnelError = StartTunnelError(underlyingError: error) - if case .failure(let error) = completion { - self.logger.error(chainedError: error, message: "Failed to start the tunnel.") + self.observerList.forEach { observer in + observer.tunnelManager(self, didFailWithError: tunnelError) + } } }) operation.addObserver(BackgroundObserver(name: "Start tunnel", cancelUponExpiration: true)) - operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider)) + operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnel.category)) operationQueue.addOperation(operation) } func stopTunnel() { let operation = StopTunnelOperation( - dispatchQueue: stateQueue, - state: state + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self) ) { [weak self] completion in guard let self = self, let error = completion.error else { return } - // Pass tunnel failure to observers + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to stop the tunnel." + ) + DispatchQueue.main.async { + let tunnelError = StopTunnelError(underlyingError: error) + self.observerList.forEach { observer in - observer.tunnelManager(self, didFailWithError: error) + observer.tunnelManager(self, didFailWithError: tunnelError) } } } operation.addObserver(BackgroundObserver(name: "Stop tunnel", cancelUponExpiration: true)) - operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider)) + operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnel.category)) operationQueue.addOperation(operation) } func reconnectTunnel( selectNewRelay: Bool, - completionHandler: ((OperationCompletion<(), TunnelManager.Error>) -> Void)? = nil + completionHandler: ((OperationCompletion<(), Error>) -> Void)? = nil ) { let operation = ReconnectTunnelOperation( - dispatchQueue: stateQueue, - state: state, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), selectNewRelay: selectNewRelay ) + operation.completionQueue = .main operation.completionHandler = { [weak self] completion in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - if let error = completion.error { - self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel.") - } - - // Refresh tunnel status only when connecting or reasserting to pick up the next relay, - // since both states may persist for a long period of time until the tunnel is fully - // connected. - switch self.tunnelState { - case .connecting, .reconnecting: - self.logger.debug("Refresh tunnel status due to reconnect.") - self._refreshTunnelStatus() - - default: - break - } + self?.didReconnectTunnel(completion: completion) - DispatchQueue.main.async { - completionHandler?(completion) - } + completionHandler?(completion) } - operation.completionQueue = stateQueue operation.addObserver( BackgroundObserver(name: "Reconnect tunnel", cancelUponExpiration: true) ) operation.addCondition( - MutuallyExclusive(category: OperationCategory.manageTunnelProvider) + MutuallyExclusive(category: OperationCategory.manageTunnel.category) ) operationQueue.addOperation(operation) } - func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { + func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) { let operation = SetAccountOperation( - dispatchQueue: stateQueue, - state: state, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), accountsProxy: accountsProxy, devicesProxy: devicesProxy, - action: action, - willDeleteVPNConfigurationHandler: { [weak self] in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - // Unregister from receiving VPN connection status changes - self.unsubscribeVPNStatusObserver() - - // Cancel last VPN status mapping operation - self.lastMapConnectionStatusOperation?.cancel() - self.lastMapConnectionStatusOperation = nil - }) + action: action + ) - operation.completionQueue = stateQueue + operation.completionQueue = .main operation.completionHandler = { [weak self] completion in - guard let self = self else { return } - - self.resetKeyRotationData() + self?.resetKeyRotationData() - DispatchQueue.main.async { - completionHandler(completion) - } + completionHandler(completion) } operation.addObserver(BackgroundObserver(name: action.taskName, cancelUponExpiration: true)) operation.addCondition( - MutuallyExclusive(category: OperationCategory.manageTunnelProvider) + MutuallyExclusive(category: OperationCategory.manageTunnel.category) + ) + operation.addCondition( + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) ) operation.addCondition( - MutuallyExclusive(category: OperationCategory.changeTunnelSettings) + MutuallyExclusive(category: OperationCategory.settingsUpdate.category) ) operationQueue.addOperation(operation) @@ -425,10 +385,10 @@ final class TunnelManager: TunnelManagerStateDelegate { } } - func updateAccountData(_ completionHandler: ((TunnelManager.Error?) -> Void)? = nil) { + func updateAccountData(_ completionHandler: ((Error?) -> Void)? = nil) { let operation = UpdateAccountDataOperation( - dispatchQueue: stateQueue, - state: state, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), accountsProxy: accountsProxy ) @@ -442,28 +402,33 @@ final class TunnelManager: TunnelManagerStateDelegate { ) operation.addCondition( - MutuallyExclusive(category: OperationCategory.changeTunnelSettings) + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) ) operationQueue.addOperation(operation) } - func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, TunnelManager.Error>) -> Void) -> Cancellable { + func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, Error>) -> Void) -> Cancellable { let operation = UpdateDeviceDataOperation( - dispatchQueue: stateQueue, - state: state, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), devicesProxy: devicesProxy ) operation.completionQueue = .main - operation.completionHandler = completionHandler + operation.completionHandler = { [weak self] completion in + if completion.error is RevokedDeviceError { + self?.didDetectDeviceRevoked() + } + completionHandler(completion) + } operation.addObserver( BackgroundObserver(name: "Update device data", cancelUponExpiration: true) ) operation.addCondition( - MutuallyExclusive(category: OperationCategory.changeTunnelSettings) + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) ) operationQueue.addOperation(operation) @@ -473,7 +438,7 @@ final class TunnelManager: TunnelManagerStateDelegate { func rotatePrivateKey( forceRotate: Bool, - completionHandler: @escaping (OperationCompletion<Bool, TunnelManager.Error>) -> Void + completionHandler: @escaping (OperationCompletion<Bool, Error>) -> Void ) -> Cancellable { var rotationInterval: TimeInterval? if !forceRotate { @@ -481,14 +446,16 @@ final class TunnelManager: TunnelManagerStateDelegate { } let operation = RotateKeyOperation( - dispatchQueue: stateQueue, - state: state, + dispatchQueue: internalQueue, + interactor: TunnelInteractorProxy(self), devicesProxy: devicesProxy, rotationInterval: rotationInterval - ) { [weak self] completion in + ) + + operation.completionQueue = .main + operation.completionHandler = { [weak self] completion in guard let self = self else { return } - dispatchPrecondition(condition: .onQueue(self.stateQueue)) self.setFinishedKeyRotation(completion) switch completion { @@ -498,16 +465,15 @@ final class TunnelManager: TunnelManagerStateDelegate { } case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to rotate private key.") + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to rotate private key." + ) - DispatchQueue.main.async { - completionHandler(completion) - } + completionHandler(completion) case .cancelled: - DispatchQueue.main.async { - completionHandler(completion) - } + completionHandler(completion) } } @@ -516,7 +482,7 @@ final class TunnelManager: TunnelManagerStateDelegate { ) operation.addCondition( - MutuallyExclusive(category: OperationCategory.changeTunnelSettings) + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) ) operationQueue.addOperation(operation) @@ -524,21 +490,21 @@ final class TunnelManager: TunnelManagerStateDelegate { return operation } - func setRelayConstraints(_ newConstraints: RelayConstraints, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - scheduleTunnelSettingsUpdate( + func setRelayConstraints(_ newConstraints: RelayConstraints, completionHandler: (() -> Void)? = nil) { + scheduleSettingsUpdate( taskName: "Set relay constraints", - modificationBlock: { tunnelSettings in - tunnelSettings.relayConstraints = newConstraints + modificationBlock: { settings in + settings.relayConstraints = newConstraints }, completionHandler: completionHandler ) } - func setDNSSettings(_ newDNSSettings: DNSSettings, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - scheduleTunnelSettingsUpdate( + func setDNSSettings(_ newDNSSettings: DNSSettings, completionHandler: (() -> Void)? = nil) { + scheduleSettingsUpdate( taskName: "Set DNS settings", - modificationBlock: { tunnelSettings in - tunnelSettings.dnsSettings = newDNSSettings + modificationBlock: { settings in + settings.dnsSettings = newDNSSettings }, completionHandler: completionHandler ) @@ -558,46 +524,125 @@ final class TunnelManager: TunnelManagerStateDelegate { observerList.remove(observer) } - // MARK: - TunnelManagerStateDelegate + // MARK: - TunnelInteractor + + var isConfigurationLoaded: Bool { + nslock.lock() + defer { nslock.unlock() } + + return _isConfigurationLoaded + } + + fileprivate var tunnel: Tunnel? { + nslock.lock() + defer { nslock.unlock() } + + return _tunnel + } + + var tunnelStatus: TunnelStatus { + nslock.lock() + defer { nslock.unlock() } + + return _tunnelStatus + } + + var settings: TunnelSettingsV2 { + nslock.lock() + defer { nslock.unlock() } + + return _tunnelSettings + } + + var deviceState: DeviceState { + nslock.lock() + defer { nslock.unlock() } + + return _deviceState + } + + fileprivate func setConfigurationLoaded() { + nslock.lock() + defer { nslock.unlock() } + + guard !_isConfigurationLoaded else { + return + } + + _isConfigurationLoaded = true - func tunnelManagerState( - _ state: State, - didChangeLoadedConfiguration isLoadedConfiguration: Bool - ) - { DispatchQueue.main.async { self.observerList.forEach { observer in - if isLoadedConfiguration { - observer.tunnelManagerDidLoadConfiguration(self) - } + observer.tunnelManagerDidLoadConfiguration(self) } } } - func tunnelManagerState( - _ state: TunnelManager.State, - didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2? - ) - { - DispatchQueue.main.async { - self.observerList.forEach { observer in - observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettings) - } + fileprivate func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) { + nslock.lock() + defer { nslock.unlock() } + + if let tunnel = tunnel { + subscribeVPNStatusObserver(tunnel: tunnel) + } else { + unsubscribeVPNStatusObserver() + } + + _tunnel = tunnel + + // Update the existing state + if shouldRefreshTunnelState { + logger.debug("Refresh tunnel status for new tunnel.") + _refreshTunnelStatus() } } - func tunnelManagerState( - _ state: TunnelManager.State, - didChangeTunnelStatus newTunnelStatus: TunnelStatus + fileprivate func updateTunnelState(_ state: TunnelState) { + nslock.lock() + defer { nslock.unlock() } + + var updatedStatus = _tunnelStatus + updatedStatus.state = state + setTunnelStatus(updatedStatus) + } + + fileprivate func updateTunnelStatus( + from packetTunnelStatus: PacketTunnelStatus, + mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? ) { + nslock.lock() + defer { nslock.unlock() } + + var updatedStatus = _tunnelStatus + updatedStatus.update(from: packetTunnelStatus, mappingRelayToState: mapper) + setTunnelStatus(updatedStatus) + } + + fileprivate func resetTunnelStatus(to state: TunnelState) { + nslock.lock() + defer { nslock.unlock() } + + var updatedStatus = _tunnelStatus + updatedStatus.reset(to: state) + setTunnelStatus(updatedStatus) + } + + fileprivate func setTunnelStatus(_ newTunnelStatus: TunnelStatus) { + nslock.lock() + defer { nslock.unlock() } + logger.info("Status: \(newTunnelStatus).") + _tunnelStatus = newTunnelStatus + switch newTunnelStatus.state { case .connecting, .reconnecting: // Start polling tunnel status to keep the relay information up to date // while the tunnel process is trying to connect. - startPollingTunnelStatus(connectingDate: newTunnelStatus.packetTunnelStatus.connectingDate) + startPollingTunnelStatus( + connectingDate: newTunnelStatus.packetTunnelStatus.connectingDate + ) case .pendingReconnect, .connected, .disconnecting, .disconnected: // Stop polling tunnel status once connection moved to final state. @@ -611,34 +656,117 @@ final class TunnelManager: TunnelManagerStateDelegate { } } - func tunnelManagerState( - _ state: TunnelManager.State, - didChangeTunnelProvider newTunnelObject: Tunnel?, - shouldRefreshTunnelState: Bool - ) - { - dispatchPrecondition(condition: .onQueue(stateQueue)) + fileprivate func setSettings(_ settings: TunnelSettingsV2, persist: Bool) { + nslock.lock() + defer { nslock.unlock() } - // Register for tunnel connection status changes - if let newTunnelObject = newTunnelObject { - subscribeVPNStatusObserver(tunnel: newTunnelObject) - } else { - unsubscribeVPNStatusObserver() + let shouldCallDelegate = _tunnelSettings != settings && _isConfigurationLoaded + + _tunnelSettings = settings + + if persist { + do { + try SettingsManager.writeSettings(settings) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write settings." + ) + } } - // Update the existing state - if shouldRefreshTunnelState { - logger.debug("Refresh tunnel status for new tunnel.") - _refreshTunnelStatus() + if shouldCallDelegate { + DispatchQueue.main.async { + self.observerList.forEach { observer in + observer.tunnelManager(self, didUpdateTunnelSettings: settings) + } + } + } + } + + fileprivate func setDeviceState(_ deviceState: DeviceState, persist: Bool) { + nslock.lock() + defer { nslock.unlock() } + + let shouldCallDelegate = _deviceState != deviceState && _isConfigurationLoaded + + _deviceState = deviceState + + if persist { + do { + try SettingsManager.writeDeviceState(deviceState) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write device state." + ) + } + } + + if shouldCallDelegate { + DispatchQueue.main.async { + self.observerList.forEach { observer in + observer.tunnelManager(self, didUpdateDeviceState: deviceState) + } + } } } // MARK: - Private methods + fileprivate func prepareForVPNConfigurationDeletion() { + nslock.lock() + defer { nslock.unlock() } + + // Unregister from receiving VPN connection status changes + unsubscribeVPNStatusObserver() + + // Cancel last VPN status mapping operation + lastMapConnectionStatusOperation?.cancel() + lastMapConnectionStatusOperation = nil + } + + private func didDetectDeviceRevoked() { + scheduleDeviceStateUpdate( + taskName: "Set device revoked", + modificationBlock: { deviceState in + deviceState = .revoked + }, + completionHandler: nil + ) + } + + private func didReconnectTunnel(completion: OperationCompletion<(), Error>) { + nslock.lock() + defer { nslock.unlock() } + + if let error = completion.error { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to reconnect the tunnel." + ) + } + + // Refresh tunnel status only when connecting or reasserting to pick up the next relay, + // since both states may persist for a long period of time until the tunnel is fully + // connected. + switch tunnelStatus.state { + case .connecting, .reconnecting: + logger.debug("Refresh tunnel status due to reconnect.") + _refreshTunnelStatus() + + default: + break + } + } + private func subscribeVPNStatusObserver(tunnel: Tunnel) { + nslock.lock() + defer { nslock.unlock() } + unsubscribeVPNStatusObserver() - statusObserver = tunnel.addBlockObserver(queue: stateQueue) { [weak self] tunnel, status in + statusObserver = tunnel.addBlockObserver(queue: internalQueue) { [weak self] tunnel, status in guard let self = self else { return } self.logger.debug("VPN connection status changed to \(status).") @@ -647,14 +775,18 @@ final class TunnelManager: TunnelManagerStateDelegate { } private func unsubscribeVPNStatusObserver() { + nslock.lock() + defer { nslock.unlock() } + statusObserver?.invalidate() statusObserver = nil } private func _refreshTunnelStatus() { - dispatchPrecondition(condition: .onQueue(stateQueue)) + nslock.lock() + defer { nslock.unlock() } - if let connectionStatus = state.tunnel?.status { + if let connectionStatus = _tunnel?.status { updateTunnelStatus(connectionStatus) } } @@ -663,21 +795,18 @@ final class TunnelManager: TunnelManagerStateDelegate { /// Collects the `PacketTunnelStatus` from the tunnel via IPC if needed before assigning /// the `tunnelStatus`. private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) { - dispatchPrecondition(condition: .onQueue(stateQueue)) + nslock.lock() + defer { nslock.unlock() } let operation = MapConnectionStatusOperation( - queue: stateQueue, - state: state, + queue: internalQueue, + interactor: TunnelInteractorProxy(self), connectionStatus: connectionStatus - ) { [weak self] in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - self.startTunnel() - } + ) - operation.addCondition(MutuallyExclusive(category: OperationCategory.tunnelStateUpdate)) + operation.addCondition( + MutuallyExclusive(category: OperationCategory.tunnelStateUpdate.category) + ) // Cancel last VPN status mapping operation lastMapConnectionStatusOperation?.cancel() @@ -686,53 +815,71 @@ final class TunnelManager: TunnelManagerStateDelegate { operationQueue.addOperation(operation) } - fileprivate func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettingsV2) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - let operation = ResultBlockOperation<Void, TunnelManager.Error>( - dispatchQueue: stateQueue - ) { operation in - guard let currentSettings = self.tunnelSettings else { - operation.finish(completion: .failure(.unsetAccount)) - return - } + private func scheduleSettingsUpdate( + taskName: String, + modificationBlock: @escaping (inout TunnelSettingsV2) -> Void, + completionHandler: (() -> Void)? + ) + { + let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { + let currentSettings = self._tunnelSettings + var updatedSettings = self._tunnelSettings - do { - var updatedSettings = currentSettings + modificationBlock(&updatedSettings) - modificationBlock(&updatedSettings) + // Select new relay only when relay constraints change. + let currentConstraints = currentSettings.relayConstraints + let updatedConstraints = updatedSettings.relayConstraints + let selectNewRelay = currentConstraints != updatedConstraints - // Select new relay only when relay constraints change. - let currentConstraints = currentSettings.relayConstraints - let updatedConstraints = updatedSettings.relayConstraints - let selectNewRelay = currentConstraints != updatedConstraints + self.setSettings(updatedSettings, persist: true) + self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil) + } - try SettingsManager.writeSettings(updatedSettings) + operation.completionBlock = { + DispatchQueue.main.async { + completionHandler?() + } + } - self.state.tunnelSettings = updatedSettings - self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil) + operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: false)) + operation.addCondition( + MutuallyExclusive(category: OperationCategory.settingsUpdate.category) + ) - operation.finish(completion: .success(())) - } catch { - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to write settings." - ) + operationQueue.addOperation(operation) + } - operation.finish(completion: .failure(.writeSettings(error))) - } + private func scheduleDeviceStateUpdate( + taskName: String, + modificationBlock: @escaping (inout DeviceState) -> Void, + completionHandler: (() -> Void)? + ) + { + let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { + var deviceState = self.deviceState + + modificationBlock(&deviceState) + + self.setDeviceState(deviceState, persist: true) + self.reconnectTunnel(selectNewRelay: false, completionHandler: nil) } - operation.completionQueue = .main - operation.completionHandler = { completion in - completionHandler(completion.error) + operation.completionBlock = { + DispatchQueue.main.async { + completionHandler?() + } } - operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: true)) - operation.addCondition(MutuallyExclusive(category: OperationCategory.changeTunnelSettings)) + operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: false)) + operation.addCondition( + MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category) + ) operationQueue.addOperation(operation) } - // MARK: - Tunnel status polling. + // MARK: - Tunnel status polling private func computeNextPollDateAndRepeatInterval(connectingDate: Date?) -> (Date, TimeInterval) { let delay, repeating: TimeInterval @@ -775,7 +922,7 @@ final class TunnelManager: TunnelManagerStateDelegate { let (fireDate, repeating) = computeNextPollDateAndRepeatInterval(connectingDate: connectingDate) logger.debug("Start polling tunnel status at \(fireDate.logFormatDate()) every \(repeating) second(s).") - let timer = DispatchSource.makeTimerSource(queue: stateQueue) + let timer = DispatchSource.makeTimerSource(queue: .main) timer.setEventHandler { [weak self] in guard let self = self else { return } @@ -810,37 +957,112 @@ final class TunnelManager: TunnelManagerStateDelegate { // MARK: - AppStore payment observer extension TunnelManager: AppStorePaymentObserver { - func appStorePaymentManager(_ manager: AppStorePaymentManager, - transaction: SKPaymentTransaction?, - payment: SKPayment, - accountToken: String?, - didFailWithError error: AppStorePaymentManager.Error + func appStorePaymentManager( + _ manager: AppStorePaymentManager, + transaction: SKPaymentTransaction?, + payment: SKPayment, + accountToken: String?, + didFailWithError error: AppStorePaymentManager.Error ) { // no-op } - func appStorePaymentManager(_ manager: AppStorePaymentManager, - transaction: SKPaymentTransaction, - accountToken: String, - didFinishWithResponse response: REST.CreateApplePaymentResponse + func appStorePaymentManager( + _ manager: AppStorePaymentManager, + transaction: SKPaymentTransaction, + accountToken: String, + didFinishWithResponse response: REST.CreateApplePaymentResponse ) { - scheduleTunnelSettingsUpdate( + scheduleDeviceStateUpdate( taskName: "Update account expiry after in-app purchase", - modificationBlock: { tunnelSettings in - if tunnelSettings.account.number == accountToken { - tunnelSettings.account.expiry = response.newExpiry + modificationBlock: { deviceState in + switch deviceState { + case .loggedIn(var accountData, let deviceData): + if accountData.number == accountToken { + accountData.expiry = response.newExpiry + deviceState = .loggedIn(accountData, deviceData) + } + + case .loggedOut, .revoked: + break } }, - completionHandler: { error in - guard let error = error else { return } - - self.logger.error( - chainedError: error, - message: "Failed to update account expiry after in-app purchase" - ) - } + completionHandler: nil ) } } + +private struct TunnelInteractorProxy: TunnelInteractor { + private let tunnelManager: TunnelManager + + init(_ tunnelManager: TunnelManager) { + self.tunnelManager = tunnelManager + } + + var tunnel: Tunnel? { + return tunnelManager.tunnel + } + + func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) { + tunnelManager.setTunnel(tunnel, shouldRefreshTunnelState: shouldRefreshTunnelState) + } + + var tunnelStatus: TunnelStatus { + return tunnelManager.tunnelStatus + } + + func setTunnelStatus(_ tunnelStatus: TunnelStatus) { + tunnelManager.setTunnelStatus(tunnelStatus) + } + + func updateTunnelStatus( + from packetTunnelStatus: PacketTunnelStatus, + mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState? + ) + { + tunnelManager.updateTunnelStatus(from: packetTunnelStatus, mappingRelayToState: mapper) + } + + func updateTunnelState(_ state: TunnelState) { + tunnelManager.updateTunnelState(state) + } + + func resetTunnelState(to state: TunnelState) { + tunnelManager.resetTunnelStatus(to: state) + } + + var isConfigurationLoaded: Bool { + return tunnelManager.isConfigurationLoaded + } + + var settings: TunnelSettingsV2 { + return tunnelManager.settings + } + + var deviceState: DeviceState { + return tunnelManager.deviceState + } + + func setConfigurationLoaded() { + tunnelManager.setConfigurationLoaded() + } + + func setSettings(_ settings: TunnelSettingsV2, persist: Bool) { + tunnelManager.setSettings(settings, persist: persist) + } + + func setDeviceState(_ deviceState: DeviceState, persist: Bool) { + tunnelManager.setDeviceState(deviceState, persist: persist) + } + + func startTunnel() { + tunnelManager.startTunnel() + } + + func prepareForVPNConfigurationDeletion() { + tunnelManager.prepareForVPNConfigurationDeletion() + } + +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift new file mode 100644 index 0000000000..77c09cdc9c --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift @@ -0,0 +1,75 @@ +// +// TunnelManagerErrors.swift +// MullvadVPN +// +// Created by pronebird on 07/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +struct UnsetTunnelError: LocalizedError { + var errorDescription: String? { + return NSLocalizedString( + "UNSET_TUNNEL_ERROR", + tableName: "TunnelManager", + value: "Tunnel is unset.", + comment: "" + ) + } +} + +struct InvalidDeviceStateError: LocalizedError { + var errorDescription: String? { + return NSLocalizedString( + "INVALID_DEVICE_STATE_ERROR", + tableName: "TunnelManager", + value: "Invalid device state.", + comment: "" + ) + } +} + +struct RevokedDeviceError: LocalizedError { + var errorDescription: String? { + return NSLocalizedString( + "REVOKED_DEVICE_ERROR", + tableName: "TunnelManager", + value: "Device is revoked.", + comment: "" + ) + } +} + +struct StartTunnelError: LocalizedError { + var errorDescription: String? { + return NSLocalizedString( + "START_TUNNEL_ERROR", + tableName: "TunnelManager", + value: "Failed to start the tunnel.", + comment: "" + ) + } + + let underlyingError: Error + init(underlyingError: Error) { + self.underlyingError = underlyingError + } +} + +struct StopTunnelError: LocalizedError { + var errorDescription: String? { + return NSLocalizedString( + "STOP_TUNNEL_ERROR", + tableName: "TunnelManager", + value: "Failed to stop the tunnel.", + comment: "" + ) + } + + let underlyingError: Error + init(underlyingError: Error) { + self.underlyingError = underlyingError + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift index 93ab9d8849..7a3c4f910f 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -1,6 +1,6 @@ // // TunnelObserver.swift -// TunnelObserver +// MullvadVPN // // Created by pronebird on 19/08/2021. // Copyright © 2021 Mullvad VPN AB. All rights reserved. @@ -11,6 +11,12 @@ import Foundation protocol TunnelObserver: AnyObject { func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) + + func tunnelManager( + _ manager: TunnelManager, + didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 + ) + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index ee65346f82..bfbc0da3e9 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -91,6 +91,15 @@ enum TunnelState: Equatable, CustomStringConvertible { return "reconnecting to \(tunnelRelay.hostname)" } } + + var isSecured: Bool { + switch self { + case .reconnecting, .connecting, .connected: + return true + case .pendingReconnect, .disconnecting, .disconnected: + return false + } + } } /// A enum that describes the action to perform after disconnect diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift index ae0f0dea7a..25669631c5 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift @@ -9,39 +9,36 @@ import Foundation import Logging -class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> { +class UpdateAccountDataOperation: ResultOperation<Void, Error> { private let logger = Logger(label: "UpdateAccountDataOperation") - private let state: TunnelManager.State + private let interactor: TunnelInteractor private let accountsProxy: REST.AccountsProxy private var task: Cancellable? init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, accountsProxy: REST.AccountsProxy ) { - self.state = state + self.interactor = interactor self.accountsProxy = accountsProxy super.init(dispatchQueue: dispatchQueue) } override func main() { - guard let tunnelSettings = state.tunnelSettings else { - finish(completion: .failure(.unsetAccount)) + guard case .loggedIn(let accountData, _) = interactor.deviceState else { + finish(completion: .failure(InvalidDeviceStateError())) return } task = accountsProxy.getAccountData( - accountNumber: tunnelSettings.account.number, + accountNumber: accountData.number, retryStrategy: .default ) { completion in self.dispatchQueue.async { - self.didReceiveAccountData( - tunnelSettings: tunnelSettings, - completion: completion - ) + self.didReceiveAccountData(completion: completion) } } } @@ -52,36 +49,28 @@ class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> { } private func didReceiveAccountData( - tunnelSettings: TunnelSettingsV2, completion: OperationCompletion<REST.AccountData, REST.Error> - ) - { - let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + ) { + let mappedCompletion = completion.mapError { error -> Error in self.logger.error( chainedError: error, message: "Failed to fetch account expiry." ) - return .getAccountData(error) - } - - guard let accountData = mappedCompletion.value else { - finish(completion: mappedCompletion.assertNoSuccess()) - return - } + return error + }.tryMap { accountData in + switch interactor.deviceState { + case .loggedIn(var storedAccountData, let storedDeviceData): + storedAccountData.expiry = accountData.expiry - do { - var newTunnelSettings = tunnelSettings - newTunnelSettings.account.expiry = accountData.expiry - try SettingsManager.writeSettings(newTunnelSettings) + let newDeviceState = DeviceState.loggedIn(storedAccountData, storedDeviceData) - finish(completion: .success(())) - } catch { - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to save account data." - ) + interactor.setDeviceState(newDeviceState, persist: true) - finish(completion: .failure(.writeSettings(error))) + default: + throw InvalidDeviceStateError() + } } + + finish(completion: mappedCompletion) } } diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift index 6c0fd08823..3c7968371f 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift @@ -10,40 +10,37 @@ import Foundation import Logging import class WireGuardKitTypes.PublicKey -class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, TunnelManager.Error> { - private let logger = Logger(label: "UpdateDeviceDataOperation") - - private let state: TunnelManager.State +class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, Error> { + private let interactor: TunnelInteractor private let devicesProxy: REST.DevicesProxy private var task: Cancellable? init( dispatchQueue: DispatchQueue, - state: TunnelManager.State, + interactor: TunnelInteractor, devicesProxy: REST.DevicesProxy ) { - self.state = state + self.interactor = interactor self.devicesProxy = devicesProxy super.init(dispatchQueue: dispatchQueue) } override func main() { - guard let tunnelSettings = state.tunnelSettings else { - finish(completion: .failure(.unsetAccount)) + guard case .loggedIn(let accountData, let deviceData) = interactor.deviceState else { + finish(completion: .failure(InvalidDeviceStateError())) return } task = devicesProxy.getDevice( - accountNumber: tunnelSettings.account.number, - identifier: tunnelSettings.device.identifier, + accountNumber: accountData.number, + identifier: deviceData.identifier, retryStrategy: .default, completion: { [weak self] completion in self?.dispatchQueue.async { self?.didReceiveDeviceResponse( - tunnelSettings: tunnelSettings, completion: completion ) } @@ -55,42 +52,27 @@ class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, TunnelManager task = nil } - private func didReceiveDeviceResponse( - tunnelSettings: TunnelSettingsV2, - completion: OperationCompletion<REST.Device?, REST.Error> - ) { - let mappedCompletion = completion - .mapError { error -> TunnelManager.Error in - return .getDevice(error) - } - .flatMap { device -> OperationCompletion<REST.Device, TunnelManager.Error> in - if let device = device { - return .success(device) - } else { - return .failure(.deviceRevoked) - } + private func didReceiveDeviceResponse(completion: OperationCompletion<REST.Device?, REST.Error>) + { + let mappedCompletion = completion.tryMap { device -> StoredDeviceData in + guard let device = device else { + throw RevokedDeviceError() } - guard let device = mappedCompletion.value else { - finish(completion: mappedCompletion.assertNoSuccess()) - return - } - - do { - var newTunnelSettings = tunnelSettings - newTunnelSettings.device.update(from: device) - - try SettingsManager.writeSettings(newTunnelSettings) + switch interactor.deviceState { + case .loggedIn(let storedAccount, var storedDevice): + storedDevice.update(from: device) + let newDeviceState = DeviceState.loggedIn(storedAccount, storedDevice) + interactor.setDeviceState(newDeviceState, persist: true) - finish(completion: .success(newTunnelSettings.device)) - } catch { - logger.error( - chainedError: AnyChainedError(error), - message: "Failed to write settings." - ) + return storedDevice - finish(completion: .failure(.writeSettings(error))) + default: + throw InvalidDeviceStateError() + } } + + finish(completion: mappedCompletion) } } diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index e772290cd2..0e29a11d64 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -85,7 +85,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { contentView.verifyKeyButton.addTarget(self, action: #selector(handleVerifyKey(_:)), for: .touchUpInside) TunnelManager.shared.addObserver(self) - updatePublicKey(deviceData: TunnelManager.shared.device, animated: false) + updatePublicKey(deviceData: TunnelManager.shared.deviceState.deviceData, animated: false) startPublicKeyPeriodicUpdate() } @@ -94,7 +94,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { let interval = DispatchTimeInterval.seconds(creationDateRefreshInterval) let timerSource = DispatchSource.makeTimerSource(queue: .main) timerSource.setEventHandler { [weak self] () -> Void in - self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true) + self?.updatePublicKey(deviceData: TunnelManager.shared.deviceState.deviceData, animated: true) } timerSource.schedule(deadline: .now() + interval, repeating: interval) timerSource.activate() @@ -112,20 +112,24 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - updatePublicKey(deviceData: tunnelSettings?.device, animated: true) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { + // no-op } - func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + updatePublicKey(deviceData: deviceState.deviceData, animated: true) + } + + func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { // no-op } // MARK: - Actions private func copyPublicKey() { - guard let tunnelSettings = TunnelManager.shared.tunnelSettings else { return } + guard let deviceData = TunnelManager.shared.deviceState.deviceData else { return } - UIPasteboard.general.string = tunnelSettings.device.wgKeyData.privateKey.publicKey.base64Key + UIPasteboard.general.string = deviceData.wgKeyData.privateKey.publicKey.base64Key setPublicKeyTitle( string: NSLocalizedString( @@ -137,7 +141,10 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { animated: true) let workItem = DispatchWorkItem { [weak self] in - self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true) + self?.updatePublicKey( + deviceData: TunnelManager.shared.deviceState.deviceData, + animated: true + ) } DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem) @@ -240,7 +247,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { self.updateViewState(.verifiedKey(true)) case .failure(let error): - if case .deviceRevoked = error { + if error is RevokedDeviceError { self.updateViewState(.verifiedKey(false)) } else { self.showKeyVerificationFailureAlert(error) @@ -266,8 +273,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { } } - private func showKeyVerificationFailureAlert(_ error: TunnelManager.Error) { - let reason = error.errorChainDescription ?? "" + private func showKeyVerificationFailureAlert(_ error: Error) { let errorDescription = String( format: NSLocalizedString( "VERIFY_KEY_FAILURE_ALERT_MESSAGE", @@ -275,7 +281,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { value: "Failed to verify the WireGuard key: %@", comment: "" ), - reason + error.localizedDescription ) let alertController = UIAlertController( @@ -304,7 +310,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { alertPresenter.enqueue(alertController, presentingController: self) } - private func showKeyRegenerationFailureAlert(_ error: TunnelManager.Error) { + private func showKeyRegenerationFailureAlert(_ error: Error) { let alertController = UIAlertController( title: NSLocalizedString( "REGENERATE_KEY_FAILURE_ALERT_TITLE", @@ -312,7 +318,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { value: "Cannot regenerate the key", comment: "" ), - message: error.errorChainDescription, + message: error.localizedDescription, preferredStyle: .alert ) alertController.addAction( diff --git a/ios/PacketTunnel/PacketTunnelConfiguration.swift b/ios/PacketTunnel/PacketTunnelConfiguration.swift index 8493ffbf6f..69de080868 100644 --- a/ios/PacketTunnel/PacketTunnelConfiguration.swift +++ b/ios/PacketTunnel/PacketTunnelConfiguration.swift @@ -11,6 +11,7 @@ import WireGuardKit import protocol Network.IPAddress struct PacketTunnelConfiguration { + var deviceState: DeviceState var tunnelSettings: TunnelSettingsV2 var selectorResult: RelaySelectorResult } @@ -34,15 +35,19 @@ extension PacketTunnelConfiguration { return peerConfig } - var interfaceConfig = InterfaceConfiguration( - privateKey: tunnelSettings.device.wgKeyData.privateKey - ) + var interfaceConfig: InterfaceConfiguration + + switch deviceState { + case .loggedIn(_, let device): + interfaceConfig = InterfaceConfiguration(privateKey: device.wgKeyData.privateKey) + interfaceConfig.addresses = [device.ipv4Address, device.ipv6Address] + interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } + + case .loggedOut, .revoked: + interfaceConfig = InterfaceConfiguration(privateKey: PrivateKey()) + } + interfaceConfig.listenPort = 0 - interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } - interfaceConfig.addresses = [ - tunnelSettings.device.ipv4Address, - tunnelSettings.device.ipv6Address - ] return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs) } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 67ce4db94a..01a3da6e03 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -334,6 +334,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) throws -> PacketTunnelConfiguration { + let deviceState = try SettingsManager.readDeviceState() let tunnelSettings = try SettingsManager.readSettings() let selectorResult = try appSelectorResult ?? Self.selectRelayEndpoint( @@ -341,6 +342,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) return PacketTunnelConfiguration( + deviceState: deviceState, tunnelSettings: tunnelSettings, selectorResult: selectorResult ) |
