diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-07-22 18:47:16 +0300 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-07-22 18:47:16 +0300 |
| commit | d6871530f4875ac9f4f644589cc0a69b328911f9 (patch) | |
| tree | e77e58bdc99883ee443ea6ccd14fdf629b1dca89 | |
| parent | f10dc9875b3d8e5d35448af2a9c58b9db07b8829 (diff) | |
| parent | b4d6cd798392957ddb50f111d4f5231b89f3defc (diff) | |
| download | mullvadvpn-d6871530f4875ac9f4f644589cc0a69b328911f9.tar.xz mullvadvpn-d6871530f4875ac9f4f644589cc0a69b328911f9.zip | |
Merge branch 'rest'
27 files changed, 1191 insertions, 1115 deletions
diff --git a/ios/BuildInstructions.md b/ios/BuildInstructions.md index 4f8dddca4d..13f84867ce 100644 --- a/ios/BuildInstructions.md +++ b/ios/BuildInstructions.md @@ -147,15 +147,6 @@ xcrun altool --store-password-in-keychain-item <KEYCHAIN_ITEM_NAME> \ [Apple ID website]: https://appleid.apple.com/account/manage -# Install Xcode project dependencies - -Xcode project uses a pre-build action to bundle the relay list with the app, which depends on `jq`. -You can install it with `brew install jq`. See [jq website] for more installation options. - -[jq website]: https://stedolan.github.io/jq/download/ - -The log output is saved to `ios/prebuild.log`. - # Automated build and deployment Build script does not bump the build number, so make sure to do that manually and commit to repo: diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8fa3f3734e..bb12fd7909 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -62,6 +62,20 @@ 584E96BE240FD4DB00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; + 5857F22F24C8404C00CF6F47 /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; }; + 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; + 5857F23324C8442800CF6F47 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */; }; + 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; + 5857F23524C8444E00CF6F47 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21A24B3236900F9D8A1 /* InputOperation.swift */; }; + 5857F23624C8445300CF6F47 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */; }; + 5857F23724C8446400CF6F47 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; }; + 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; + 5857F23924C8446A00CF6F47 /* AnyOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */; }; + 5857F23B24C8448600CF6F47 /* OperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */; }; + 5857F23C24C8449500CF6F47 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */; }; + 5857F23D24C8449A00CF6F47 /* TransformOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */; }; + 5857F23E24C844A000CF6F47 /* OperationBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */; }; + 5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1C123A785C600CEA666 /* WireguardDevice.swift */; }; 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1C323A8D25F00CEA666 /* WireguardConfiguration.swift */; }; 5860F1EB23AA4CF300CEA666 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1EA23AA4CF300CEA666 /* Logging.swift */; }; @@ -88,7 +102,6 @@ 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */; }; 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; }; 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationController.swift */; }; - 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; }; 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; @@ -103,8 +116,6 @@ 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; }; - 58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; }; - 58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; @@ -112,7 +123,6 @@ 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; - 58B0A2AB238EE6BF00BC001D /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; 58B0A2AC238EE6D500BC001D /* IpAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IpAddress+Codable.swift */; }; 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; @@ -123,9 +133,6 @@ 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; - 58BFA5C022A7C8A900A6173D /* MullvadRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */; }; - 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; }; - 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; @@ -140,6 +147,8 @@ 58C6B36122C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B36022C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift */; }; 58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B36422C10596003C19AD /* AnyIPEndpoint+Wireguard.swift */; }; 58C6B36722C106FC003C19AD /* WireguardCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B36622C106FC003C19AD /* WireguardCommand.swift */; }; + 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; }; + 58CB0EE124B86751001EF0D8 /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; }; 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; 58CC40F024A602780019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* ConnectViewController.swift */; }; @@ -286,7 +295,6 @@ 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusIndicatorView.swift; sourceTree = "<group>"; }; 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; }; 5888AD86227B17950051EB06 /* SelectLocationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationController.swift; sourceTree = "<group>"; }; - 5888AD88227B18C40051EB06 /* RelayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayList.swift; sourceTree = "<group>"; }; 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEProviderStopReason+Debug.swift"; sourceTree = "<group>"; }; 5894E725236B2801008A2793 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; }; @@ -295,8 +303,6 @@ 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; - 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonRpc.swift; sourceTree = "<group>"; }; - 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadRpc.swift; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -316,6 +322,7 @@ 58C6B36022C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyIPEndpoint+DNS64.swift"; sourceTree = "<group>"; }; 58C6B36422C10596003C19AD /* AnyIPEndpoint+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyIPEndpoint+Wireguard.swift"; sourceTree = "<group>"; }; 58C6B36622C106FC003C19AD /* WireguardCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardCommand.swift; sourceTree = "<group>"; }; + 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadRest.swift; sourceTree = "<group>"; }; 58CC40EE24A601900019D96E /* ObserverList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverList.swift; sourceTree = "<group>"; }; 58CCA00F224249A1004F3011 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = "<group>"; }; 58CCA01122424D11004F3011 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; }; @@ -518,7 +525,7 @@ 58CE5E65224146200008646E /* LoginViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */, - 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */, + 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */, 58FBDAAA22A52DC500EB69A3 /* MullvadVPN-Bridging-Header.h */, 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */, 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, @@ -526,10 +533,8 @@ 58CC40EE24A601900019D96E /* ObserverList.swift */, 580EE1FF24B3218800F9D8A1 /* Operations */, 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */, - 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */, 58BFA5C522A7C97F00A6173D /* RelayCache.swift */, 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, - 5888AD88227B18C40051EB06 /* RelayList.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, @@ -860,23 +865,36 @@ 5896AE81246ACE81005B36CB /* KeychainMatchLimit.swift in Sources */, 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */, 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */, + 5857F23724C8446400CF6F47 /* AssociatedValue.swift in Sources */, + 5857F23B24C8448600CF6F47 /* OperationProtocol.swift in Sources */, + 5857F22F24C8404C00CF6F47 /* MullvadRest.swift in Sources */, 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, + 5857F23924C8446A00CF6F47 /* AnyOperationObserver.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, + 5857F23324C8442800CF6F47 /* IPAddressRange.swift in Sources */, 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, + 5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */, + 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, + 5857F23624C8445300CF6F47 /* OutputOperation.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IpAddress+Codable.swift in Sources */, - 58B0A2AB238EE6BF00BC001D /* RelayList.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */, + 5857F23D24C8449A00CF6F47 /* TransformOperationObserver.swift in Sources */, + 5857F23524C8444E00CF6F47 /* InputOperation.swift in Sources */, + 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, + 5857F23E24C844A000CF6F47 /* OperationBlockObserver.swift in Sources */, + 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */, 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */, 5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */, + 5857F23C24C8449500CF6F47 /* OperationObserver.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -904,6 +922,7 @@ 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58C6B35422BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, + 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */, 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */, @@ -926,7 +945,6 @@ 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */, 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */, 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, - 58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */, 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, 58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */, @@ -947,7 +965,6 @@ 5877152E23981C5B001F8237 /* SettingsBasicCell.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, - 58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */, 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 58C6B35E22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, @@ -966,7 +983,6 @@ 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, 580EE20124B321D500F9D8A1 /* OperationProtocol.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, - 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */, 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */, 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, @@ -986,19 +1002,18 @@ files = ( 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */, 58F3C09D249B99DD003E76BE /* Curve25519.swift in Sources */, + 58CB0EE124B86751001EF0D8 /* MullvadRest.swift in Sources */, 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */, 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, - 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */, 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */, 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */, 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */, 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */, - 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */, @@ -1021,7 +1036,6 @@ 58C6B35F22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */, - 58BFA5C022A7C8A900A6173D /* MullvadRpc.swift in Sources */, 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift index 574a2af656..2dee095be1 100644 --- a/ios/MullvadVPN/Account.swift +++ b/ios/MullvadVPN/Account.swift @@ -23,10 +23,10 @@ class Account { enum Error: ChainedError { /// A failure to create the new account token - case createAccount(MullvadRpc.Error) + case createAccount(RestError) /// A failure to verify the account token - case verifyAccount(MullvadRpc.Error) + case verifyAccount(RestError) /// A failure to configure a tunnel case tunnelConfiguration(TunnelManager.Error) @@ -74,7 +74,7 @@ class Account { case exclusive } - private let rpc = MullvadRpc.withEphemeralURLSession() + private let rest = MullvadRest() private let operationQueue = OperationQueue() private lazy var exclusivityController = ExclusivityController<ExclusivityCategory>(operationQueue: operationQueue) @@ -87,44 +87,39 @@ class Account { UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) } - func loginWithNewAccount(completionHandler: @escaping (Result<(String, Date), Error>) -> Void) { - let operation = rpc.createAccount().operation() + func loginWithNewAccount(completionHandler: @escaping (Result<AccountResponse, Error>) -> Void) { + let operation = rest.createAccount().operation(payload: EmptyPayload()) - operation.addDidFinishBlockObserver({ (operation, result) in - DispatchQueue.main.async { - switch result { - case .success(let newAccountToken): - let expiry = Date() - self.setupTunnel(accountToken: newAccountToken, expiry: expiry) { (result) in - completionHandler(result.map { (newAccountToken, expiry) }) - } - - case .failure(let error): - completionHandler(.failure(.createAccount(error))) + operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in + switch result { + case .success(let response): + self.setupTunnel(accountToken: response.token, expiry: response.expires) { (result) in + completionHandler(result.map { response }) } + + case .failure(let error): + completionHandler(.failure(.createAccount(error))) } - }) + } exclusivityController.addOperation(operation, categories: [.exclusive]) } /// Perform the login and save the account token along with expiry (if available) to the /// application preferences. - func login(with accountToken: String, completionHandler: @escaping (Result<Date, Error>) -> Void) { - let operation = rpc.getAccountExpiry(accountToken: accountToken) - .operation() - - operation.addDidFinishBlockObserver { (operation, result) in - DispatchQueue.main.async { - switch result { - case .success(let expiry): - self.setupTunnel(accountToken: accountToken, expiry: expiry) { (result) in - completionHandler(result.map { expiry }) - } + func login(with accountToken: String, completionHandler: @escaping (Result<AccountResponse, Error>) -> Void) { + let operation = rest.getAccountExpiry() + .operation(payload: .init(token: accountToken, payload: EmptyPayload())) - case .failure(let error): - completionHandler(.failure(.verifyAccount(error))) + operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in + switch result { + case .success(let response): + self.setupTunnel(accountToken: response.token, expiry: response.expires) { (result) in + completionHandler(result.map { response }) } + + case .failure(let error): + completionHandler(.failure(.verifyAccount(error))) } } @@ -149,35 +144,32 @@ class Account { } } - operation.addDidFinishBlockObserver { (operation, result) in - DispatchQueue.main.async { - completionHandler(result) - } + operation.addDidFinishBlockObserver(queue: .main) { (operation, result) in + completionHandler(result) } exclusivityController.addOperation(operation, categories: [.exclusive]) } func updateAccountExpiry() { - let makeRequest = ResultOperation { () -> MullvadRpc.Request<Date>? in - return self.token.flatMap { (accountToken) -> MullvadRpc.Request<Date>? in - self.rpc.getAccountExpiry(accountToken: accountToken) + let makeRequest = ResultOperation { () -> TokenPayload<EmptyPayload>? in + return self.token.flatMap { (token) in + return TokenPayload(token: token, payload: EmptyPayload()) } } - let sendRequest = rpc.getAccountExpiry() + let sendRequest = rest.getAccountExpiry() + .operation(payload: nil) .injectResult(from: makeRequest) - sendRequest.addDidFinishBlockObserver { (operation, result) in - DispatchQueue.main.async { - switch result { - case .success(let expiry): - self.expiry = expiry - self.postExpiryUpdateNotification(newExpiry: expiry) + sendRequest.addDidFinishBlockObserver(queue: .main) { (operation, result) in + switch result { + case .success(let response): + self.expiry = response.expires + self.postExpiryUpdateNotification(newExpiry: response.expires) - case .failure(let error): - error.logChain(message: "Failed to update account expiry") - } + case .failure(let error): + error.logChain(message: "Failed to update account expiry") } } @@ -227,7 +219,7 @@ extension Account: AppStorePaymentObserver { // no-op } - func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: SendAppStoreReceiptResponse) { + func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: CreateApplePaymentResponse) { let newExpiry = response.newExpiry let operation = AsyncBlockOperation { (finish) in diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index d95adcf738..1c0ed4177c 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -176,8 +176,8 @@ class AccountViewController: UIViewController, AppStorePaymentObserver { } private func showTimeAddedConfirmationAlert( - with response: SendAppStoreReceiptResponse, - context: SendAppStoreReceiptResponse.Context) + with response: CreateApplePaymentResponse, + context: CreateApplePaymentResponse.Context) { let alertController = UIAlertController( title: response.alertTitle(context: context), @@ -282,7 +282,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver { } } - func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: SendAppStoreReceiptResponse) { + func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: CreateApplePaymentResponse) { DispatchQueue.main.async { self.showTimeAddedConfirmationAlert(with: response, context: .purchase) @@ -361,7 +361,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver { } -private extension SendAppStoreReceiptResponse { +private extension CreateApplePaymentResponse { enum Context { case purchase @@ -385,7 +385,7 @@ private extension SendAppStoreReceiptResponse { formattedTimeAdded ?? "" ) case .restoration: - return timeAdded.isZero + return timeAdded == 0 ? NSLocalizedString( "Your previous purchases have already been added to this account.", comment: "") diff --git a/ios/MullvadVPN/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager.swift index 55a22f8cae..690c6bb1cd 100644 --- a/ios/MullvadVPN/AppStorePaymentManager.swift +++ b/ios/MullvadVPN/AppStorePaymentManager.swift @@ -45,7 +45,7 @@ protocol AppStorePaymentObserver: class { _ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, - didFinishWithResponse response: SendAppStoreReceiptResponse) + didFinishWithResponse response: CreateApplePaymentResponse) } /// A type-erasing weak container for `AppStorePaymentObserver` @@ -71,7 +71,7 @@ private class AnyAppStorePaymentObserver: WeakObserverBox, Equatable { func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, - didFinishWithResponse response: SendAppStoreReceiptResponse) + didFinishWithResponse response: CreateApplePaymentResponse) { self.inner?.appStorePaymentManager( manager, @@ -99,7 +99,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { case noAccountSet case storePayment(Swift.Error) case readReceipt(AppStoreReceipt.Error) - case sendReceipt(MullvadRpc.Error) + case sendReceipt(RestError) var errorDescription: String? { switch self { @@ -115,12 +115,18 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { } } + private enum ExlcusivityCategory { + case sendReceipt + } + /// A shared instance of `AppStorePaymentManager` static let shared = AppStorePaymentManager(queue: SKPaymentQueue.default()) private let operationQueue = OperationQueue() + private lazy var exclusivityController = ExclusivityController<ExlcusivityCategory>(operationQueue: operationQueue) + + private let rest = MullvadRest(session: URLSession(configuration: .ephemeral)) private let queue: SKPaymentQueue - private let rpc = MullvadRpc.withEphemeralURLSession() private var observerList = ObserverList<AnyAppStorePaymentObserver>() private let lock = NSRecursiveLock() @@ -225,7 +231,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { func restorePurchases( for accountToken: String, - completionHandler: @escaping (Result<SendAppStoreReceiptResponse, AppStorePaymentManager.Error>) -> Void) { + completionHandler: @escaping (Result<CreateApplePaymentResponse, AppStorePaymentManager.Error>) -> Void) { return sendAppStoreReceipt( accountToken: accountToken, forceRefresh: true, @@ -235,23 +241,21 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { // MARK: - Private methods - private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool, completionHandler: @escaping (Result<SendAppStoreReceiptResponse, Error>) -> Void) + private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool, completionHandler: @escaping (Result<CreateApplePaymentResponse, Error>) -> Void) { AppStoreReceipt.fetch(forceRefresh: forceRefresh) { (result) in switch result { case .success(let receiptData): - let request = self.rpc.sendAppStoreReceipt( - accountToken: accountToken, - receiptData: receiptData - ) + let payload = TokenPayload<CreateApplePaymentRequest>(token: accountToken, payload: CreateApplePaymentRequest(receiptString: receiptData)) - request.start { (result) in + let createApplePaymentOperation = self.rest.createApplePayment() + .operation(payload: payload) + + createApplePaymentOperation.addDidFinishBlockObserver { (operation, result) in switch result { case .success(let response): - os_log( - .info, - "AppStore Receipt was processed. Time added: %{public}.2f, New expiry: %{private}s", - response.timeAdded, "\(response.newExpiry)") + os_log(.info, "AppStore Receipt was processed. Time added: %{public}.2f, New expiry: %{private}s", + response.timeAdded, "\(response.newExpiry)") completionHandler(.success(response)) @@ -260,6 +264,8 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { } } + self.exclusivityController.addOperation(createApplePaymentOperation, categories: [.sendReceipt]) + case .failure(let error): completionHandler(.failure(.readReceipt(error))) } diff --git a/ios/MullvadVPN/AutomaticKeyRotationManager.swift b/ios/MullvadVPN/AutomaticKeyRotationManager.swift index 1755c6f4af..b9f706d11d 100644 --- a/ios/MullvadVPN/AutomaticKeyRotationManager.swift +++ b/ios/MullvadVPN/AutomaticKeyRotationManager.swift @@ -25,8 +25,8 @@ struct KeyRotationResult { class AutomaticKeyRotationManager { enum Error: ChainedError { - /// An RPC failure - case rpc(MullvadRpc.Error) + /// REST error + case rest(RestError) /// A failure to read the tunnel settings case readTunnelSettings(TunnelSettingsManager.Error) @@ -36,8 +36,8 @@ class AutomaticKeyRotationManager { var errorDescription: String? { switch self { - case .rpc: - return "RPC error" + case .rest: + return "REST error" case .readTunnelSettings: return "Read tunnel settings error" case .updateTunnelSettings: @@ -46,7 +46,7 @@ class AutomaticKeyRotationManager { } } - private let rpc = MullvadRpc.withEphemeralURLSession() + private let rest = MullvadRest(session: URLSession(configuration: .ephemeral)) private let persistentKeychainReference: Data /// A dispatch queue used for synchronization @@ -61,8 +61,8 @@ class AutomaticKeyRotationManager { /// Internal variable indicating that the key rotation has already started private var isAutomaticRotationEnabled = false - /// An RPC request for replacing the key on server - private var request: MullvadRpc.Request<WireguardAssociatedAddresses>? + /// A REST request for replacing the key on server + private var dataTask: URLSessionTask? /// A variable backing the `eventHandler` public property private var _eventHandler: ((KeyRotationResult) -> Void)? @@ -106,8 +106,8 @@ class AutomaticKeyRotationManager { self.isAutomaticRotationEnabled = false - self.request?.cancel() - self.request = nil + self.dataTask?.cancel() + self.dataTask = nil self.timerSource?.cancel() @@ -123,7 +123,7 @@ class AutomaticKeyRotationManager { let currentPrivateKey = keychainEntry.tunnelSettings.interface.privateKey if Self.shouldRotateKey(creationDate: currentPrivateKey.creationDate) { - let request = replaceKey(accountToken: keychainEntry.accountToken, oldPublicKey: currentPrivateKey.publicKey) { (result) in + let result = makeReplaceKeyTask(accountToken: keychainEntry.accountToken, oldPublicKey: currentPrivateKey.publicKey) { (result) in let result = result.map { (tunnelSettings) -> KeyRotationResult in let newPrivateKey = tunnelSettings.interface.privateKey @@ -137,7 +137,15 @@ class AutomaticKeyRotationManager { self.didCompleteKeyRotation(result: result) } - self.request = request + switch result { + case .success(let newTask): + self.dataTask = newTask + newTask.resume() + + case .failure(let error): + self.dataTask = nil + self.didCompleteKeyRotation(result: .failure(.rest(error))) + } } else { let event = KeyRotationResult( isNew: false, @@ -153,31 +161,35 @@ class AutomaticKeyRotationManager { } } - private func replaceKey( + private func makeReplaceKeyTask( accountToken: String, oldPublicKey: WireguardPublicKey, - completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) -> MullvadRpc.Request<WireguardAssociatedAddresses> + completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) -> Result<URLSessionDataTask, RestError> { let newPrivateKey = WireguardPrivateKey() - - let request = rpc.replaceWireguardKey( - accountToken: accountToken, - oldPublicKey: oldPublicKey.rawRepresentation, - newPublicKey: newPrivateKey.publicKey.rawRepresentation + let payload = TokenPayload( + token: accountToken, + payload: ReplaceWireguardKeyRequest( + old: oldPublicKey.rawRepresentation, + new: newPrivateKey.publicKey.rawRepresentation + ) ) - request.start { (result) in + return rest.replaceWireguardKey().dataTask(payload: payload) { (result) in self.dispatchQueue.async { let updateResult = result.mapError { (error) -> Error in - return .rpc(error) - }.flatMap { (addresses) -> Result<TunnelSettings, Error> in - self.updateTunnelSettings(privateKey: newPrivateKey, addresses: addresses) + return .rest(error) + }.flatMap { (response) -> Result<TunnelSettings, Error> in + let addresses = WireguardAssociatedAddresses( + ipv4Address: response.ipv4Address, + ipv6Address: response.ipv6Address + ) + + return self.updateTunnelSettings(privateKey: newPrivateKey, addresses: addresses) } completionHandler(updateResult) } } - - return request } private func updateTunnelSettings(privateKey: WireguardPrivateKey, addresses: WireguardAssociatedAddresses) -> Result<TunnelSettings, Error> { @@ -218,7 +230,7 @@ class AutomaticKeyRotationManager { nextRotationTime = .now() + .seconds(kRetryIntervalOnFailure) } - case .failure(.rpc(.network(let urlError))) where urlError.code == .cancelled: + case .failure(.rest(.network(URLError.cancelled))): os_log(.default, log: tunnelProviderLog, "Key rotation was cancelled") break diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index db5a927b89..164513734b 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -280,7 +280,6 @@ <outlet property="connectionPanel" destination="ocV-9f-WDZ" id="Uad-bl-KFU"/> <outlet property="countryLabel" destination="9Yf-sl-l3q" id="L3N-Jn-zlr"/> <outlet property="secureLabel" destination="HNy-mU-nui" id="QBg-mR-Z6g"/> - <segue destination="hOC-Ab-N3D" kind="presentation" identifier="ShowRelaySelector" id="mui-V1-CK4"/> </connections> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="gkg-dm-hcG" userLabel="First Responder" sceneMemberID="firstResponder"/> @@ -299,20 +298,20 @@ <color key="separatorColor" name="Secondary"/> <prototypes> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Account" id="ghE-jC-RWf" customClass="SettingsAccountCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="55.5" width="375" height="43"/> + <rect key="frame" x="0.0" y="55.5" width="375" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ghE-jC-RWf" id="sTl-gI-g2a"> - <rect key="frame" x="0.0" y="0.0" width="348" height="43"/> + <rect key="frame" x="0.0" y="0.0" width="348" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lve-Kd-qTr"> - <rect key="frame" x="16" y="11" width="63.5" height="21"/> + <rect key="frame" x="16" y="11" width="63.5" height="21.5"/> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> </label> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="A YEAR LEFT" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QeD-EQ-Ruo"> - <rect key="frame" x="259" y="11" width="81" height="21"/> + <rect key="frame" x="259" y="11" width="81" height="21.5"/> <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> @@ -337,20 +336,20 @@ </connections> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppVersion" id="pbd-iC-Emm" customClass="SettingsAppVersionCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="98.5" width="375" height="43"/> + <rect key="frame" x="0.0" y="99" width="375" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pbd-iC-Emm" id="lYp-Z8-1sN"> - <rect key="frame" x="0.0" y="0.0" width="375" height="43"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App version" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pYC-Zb-8N9"> - <rect key="frame" x="16" y="11" width="91" height="21"/> + <rect key="frame" x="16" y="11" width="91" height="21.5"/> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> </label> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="2018.3" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sOr-vj-cg7"> - <rect key="frame" x="316.5" y="11" width="42.5" height="21"/> + <rect key="frame" x="316.5" y="11" width="42.5" height="21.5"/> <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> @@ -372,14 +371,14 @@ </connections> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="BasicDisclosure" id="Ahs-gu-nTM" customClass="SettingsBasicCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="141.5" width="375" height="43"/> + <rect key="frame" x="0.0" y="142.5" width="375" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Ahs-gu-nTM" id="Drq-vk-8F2"> - <rect key="frame" x="0.0" y="0.0" width="348" height="43"/> + <rect key="frame" x="0.0" y="0.0" width="348" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Amw-A3-ePS"> - <rect key="frame" x="16" y="11" width="324" height="21"/> + <rect key="frame" x="16" y="11" width="324" height="21.5"/> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> @@ -1012,32 +1011,12 @@ </objects> <point key="canvasLocation" x="-551" y="27"/> </scene> - <!--Navigation Controller--> - <scene sceneID="oT4-Ap-qrZ"> - <objects> - <navigationController id="hOC-Ab-N3D" sceneMemberID="viewController"> - <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="kmu-Ab-x1c" customClass="CustomNavigationBar" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="375" height="108"/> - <autoresizingMask key="autoresizingMask"/> - <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <textAttributes key="largeTitleTextAttributes"> - <offsetWrapper key="textShadowOffset" horizontal="0.0" vertical="0.0"/> - </textAttributes> - </navigationBar> - <connections> - <segue destination="FxZ-7F-3yi" kind="relationship" relationship="rootViewController" id="cFv-eb-G19"/> - </connections> - </navigationController> - <placeholder placeholderIdentifier="IBFirstResponder" id="GCK-Z5-Jwh" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="1690" y="841"/> - </scene> <!--Select location--> <scene sceneID="Kar-Ys-a6u"> <objects> - <tableViewController id="FxZ-7F-3yi" customClass="SelectLocationController" customModule="MullvadVPN" customModuleProvider="target" sceneMemberID="viewController"> + <tableViewController storyboardIdentifier="SelectLocation" id="FxZ-7F-3yi" customClass="SelectLocationController" customModule="MullvadVPN" customModuleProvider="target" sceneMemberID="viewController"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="56" sectionHeaderHeight="28" sectionFooterHeight="28" id="LKX-4h-vIx"> - <rect key="frame" x="0.0" y="0.0" width="375" height="647"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <color key="backgroundColor" name="Secondary"/> <color key="separatorColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> @@ -1130,6 +1109,7 @@ </connections> </barButtonItem> </navigationItem> + <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> <connections> <segue destination="6Lc-ZQ-E4P" kind="unwind" identifier="ReturnToConnectWithNewRelay" unwindAction="unwindFromSelectLocationWithSegue:" id="SPT-Ay-Cy3"/> </connections> @@ -1137,7 +1117,7 @@ <placeholder placeholderIdentifier="IBFirstResponder" id="EvX-LH-gOg" userLabel="First Responder" sceneMemberID="firstResponder"/> <exit id="6Lc-ZQ-E4P" userLabel="Exit" sceneMemberID="exit"/> </objects> - <point key="canvasLocation" x="2649" y="841"/> + <point key="canvasLocation" x="1689" y="779"/> </scene> <!--Tunnel Control View Controller--> <scene sceneID="Zzn-S1-fVu"> diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index c2bcaf0e95..ace0eb198b 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -97,9 +97,7 @@ class ConnectViewController: UIViewController, disconnectTunnel() case .selectLocation: - performSegue( - withIdentifier: SegueIdentifier.Connect.showRelaySelector.rawValue, - sender: self) + showSelectLocation() } } @@ -194,6 +192,28 @@ class ConnectViewController: UIViewController, } } + private func showSelectLocation() { + let contentController = self.storyboard?.instantiateViewController(withIdentifier: ViewControllerIdentifier.selectLocation.rawValue) as! SelectLocationController + contentController.navigationItem.title = NSLocalizedString("Select location", comment: "") + contentController.navigationItem.largeTitleDisplayMode = .always + + let navController = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil) + navController.viewControllers = [contentController] + navController.navigationBar.prefersLargeTitles = true + navController.navigationBar.barStyle = .black + navController.navigationBar.tintColor = .white + + // Disable root controller interaction + rootContainerController?.view.isUserInteractionEnabled = false + + contentController.prefetchData { + self.present(navController, animated: true) + + // Re-enable root controller interaction + self.rootContainerController?.view.isUserInteractionEnabled = true + } + } + // MARK: - Actions @objc func handleConnectionPanelButton(_ sender: Any) { diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index ff6bd69528..4fa2883e6b 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -12,27 +12,26 @@ protocol DisplayChainedError { var errorChainDescription: String? { get } } -extension MullvadRpc.Error: DisplayChainedError { +extension RestError: DisplayChainedError { var errorChainDescription: String? { switch self { case .network(let urlError): return urlError.localizedDescription - case .server(let serverError): if let knownErrorDescription = serverError.errorDescription { return knownErrorDescription } else { return String( format: NSLocalizedString("Server error: %@", comment: ""), - serverError.message + serverError.error ?? "(empty)" ) } - - case .encoding: + case .encodePayload: return NSLocalizedString("Server request encoding error", comment: "") - - case .decoding: - return NSLocalizedString("Server response decoding error", comment: "") + case .decodeSuccessResponse: + return NSLocalizedString("Server success response decoding error", comment: "") + case .decodeErrorResponse: + return NSLocalizedString("Server error response decoding error", comment: "") } } } @@ -73,22 +72,22 @@ extension TunnelManager.Error: DisplayChainedError { case .removeTunnelSettings(_): return NSLocalizedString("Failed to remove tunnel settings", comment: "") - case .pushWireguardKey(let rpcError): - let reason = rpcError.errorChainDescription ?? "" + case .pushWireguardKey(let restError): + let reason = restError.errorChainDescription ?? "" var message = String(format: NSLocalizedString("Failed to send the WireGuard key to server: %@", comment: ""), reason) - if case .server(let serverError) = rpcError, serverError.code == .tooManyWireguardKeys { + if case .server(.keyLimitReached) = restError { message.append("\n\n") message.append(NSLocalizedString("Remove unused WireGuard keys and try again", comment: "")) } return message - case .replaceWireguardKey(let rpcError): - let reason = rpcError.errorChainDescription ?? "" + case .replaceWireguardKey(let restError): + let reason = restError.errorChainDescription ?? "" var message = String(format: NSLocalizedString("Failed to replace the WireGuard key on server: %@", comment: ""), reason) - if case .server(let serverError) = rpcError, serverError.code == .tooManyWireguardKeys { + if case .server(.keyLimitReached) = restError { message.append("\n\n") message.append(NSLocalizedString("Remove unused WireGuard keys and try again", comment: "")) } @@ -99,8 +98,8 @@ extension TunnelManager.Error: DisplayChainedError { // This error is never displayed anywhere return nil - case .verifyWireguardKey(let rpcError): - let reason = rpcError.errorChainDescription ?? "" + case .verifyWireguardKey(let restError): + let reason = restError.errorChainDescription ?? "" return String(format: NSLocalizedString("Failed to verify the WireGuard key on server: %@", comment: ""), reason) @@ -114,8 +113,8 @@ extension Account.Error: DisplayChainedError { var errorChainDescription: String? { switch self { - case .createAccount(let rpcError), .verifyAccount(let rpcError): - return rpcError.errorChainDescription + case .createAccount(let restError), .verifyAccount(let restError): + return restError.errorChainDescription case .tunnelConfiguration(let tunnelError): return tunnelError.errorChainDescription @@ -133,8 +132,8 @@ extension AppStorePaymentManager.Error: DisplayChainedError { case .readReceipt(let readReceiptError): return String(format: NSLocalizedString("Cannot read the receipt: %@", comment: ""), readReceiptError.errorChainDescription ?? "") - case .sendReceipt(let rpcError): - let reason = rpcError.errorChainDescription ?? "" + case .sendReceipt(let restError): + let reason = restError.errorChainDescription ?? "" return String(format: NSLocalizedString(#"Failed to send the receipt to server: %@\n\nPlease retry by using the "Restore purchases" button."#, comment: ""), reason) diff --git a/ios/MullvadVPN/JsonRpc.swift b/ios/MullvadVPN/JsonRpc.swift deleted file mode 100644 index a41678fa1e..0000000000 --- a/ios/MullvadVPN/JsonRpc.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// JsonRpc.swift -// MullvadVPN -// -// Created by pronebird on 02/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension Encodable { - fileprivate func encode(to container: inout SingleValueEncodingContainer) throws { - try container.encode(self) - } -} - -struct AnyEncodable: Encodable { - let value: Encodable - - init(_ value: Encodable) { - self.value = value - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try value.encode(to: &container) - } -} - -struct JsonRpcRequest: Encodable { - let version = "2.0" - let id = UUID().uuidString - let method: String - let params: [AnyEncodable] - - fileprivate enum CodingKeys: String, CodingKey { - case version = "jsonrpc", id, method, params - } -} - -class JsonRpcResponseError<ResponseCode>: Error, Decodable - where ResponseCode: Decodable -{ - let code: ResponseCode - let message: String - - var localizedDescription: String { - return message - } - - private enum CodingKeys: String, CodingKey { - case code, message - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - code = try container.decode(ResponseCode.self, forKey: .code) - message = try container.decode(String.self, forKey: .message) - } -} - -struct JsonRpcResponse<T, ResponseCode>: Decodable - where - T: Decodable, ResponseCode: Decodable -{ - let version: String - let id: String - let result: Result<T, JsonRpcResponseError<ResponseCode>> - - private enum CodingKeys: String, CodingKey { - case version = "jsonrpc", id, result, error - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.version = try container.decode(String.self, forKey: .version) - self.id = try container.decode(String.self, forKey: .id) - - if container.contains(.result) { - self.result = .success(try container.decode(T.self, forKey: .result)) - } else { - self.result = .failure(try container.decode(JsonRpcResponseError<ResponseCode>.self, forKey: .error)) - } - } -} diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index bc21068988..20cb90d387 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -174,8 +174,8 @@ class LoginViewController: UIViewController, RootContainment { Account.shared.loginWithNewAccount { (result) in switch result { - case .success(let (newAccountToken, _)): - self.accountTextField.autoformattingText = newAccountToken + case .success(let response): + self.accountTextField.autoformattingText = response.token self.endLogin(.success(.newAccount)) case .failure(let error): diff --git a/ios/MullvadVPN/MullvadRest.swift b/ios/MullvadVPN/MullvadRest.swift new file mode 100644 index 0000000000..de1b51067f --- /dev/null +++ b/ios/MullvadVPN/MullvadRest.swift @@ -0,0 +1,549 @@ +// +// MullvadRest.swift +// MullvadVPN +// +// Created by pronebird on 10/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Network + +/// REST API v1 base URL +private let kRestBaseURL = URL(string: "https://api.mullvad.net/app/v1")! + +/// Network request timeout in seconds +private let kNetworkTimeout: TimeInterval = 10 + +/// HTTP method +enum HttpMethod: String { + case get = "GET" + case post = "POST" + case delete = "DELETE" +} + +/// A struct that represents a server response in case of error (any HTTP status code except 2xx). +struct ServerErrorResponse: LocalizedError, Decodable, RestResponse, Equatable { + /// A list of known server error codes + enum Code: String, Equatable { + case invalidAccount = "INVALID_ACCOUNT" + case keyLimitReached = "KEY_LIMIT_REACHED" + case pubKeyNotFound = "PUBKEY_NOT_FOUND" + + static func ~= (pattern: Self, value: ServerErrorResponse) -> Bool { + return pattern.rawValue == value.code + } + } + + static var invalidAccount: Code { + return .invalidAccount + } + static var keyLimitReached: Code { + return .keyLimitReached + } + static var pubKeyNotFound: Code { + return .pubKeyNotFound + } + + let code: String + let error: String? + + var errorDescription: String? { + switch code { + case Code.keyLimitReached.rawValue: + return NSLocalizedString("Too many public WireGuard keys", comment: "") + case Code.invalidAccount.rawValue: + return NSLocalizedString("Invalid account", comment: "") + default: + return nil + } + } + + var recoverySuggestion: String? { + switch code { + case Code.keyLimitReached.rawValue: + return NSLocalizedString("Remove unused WireGuard keys", comment: "") + default: + return nil + } + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.code == rhs.code + } +} + +/// An error type returned by `MullvadRest` +enum RestError: ChainedError { + /// A failure to encode the payload + case encodePayload(Error) + + /// A failure during networking + case network(URLError) + + /// A failure reported by server + case server(ServerErrorResponse) + + /// A failure to decode the error response from server + case decodeErrorResponse(Error) + + /// A failure to decode the success response from server + case decodeSuccessResponse(Error) + + var errorDescription: String? { + switch self { + case .encodePayload: + return "Failure to encode the payload" + case .network: + return "Network error" + case .server: + return "Server error" + case .decodeErrorResponse: + return "Failure to decode error response from server" + case .decodeSuccessResponse: + return "Failure to decode success response from server" + } + } +} + +/// Types conforming to this protocol can participate in forming the `URLRequest` created by +/// `RestEndpoint`. +protocol RestPayload { + func inject(into request: inout URLRequest) throws +} + +/// Types conforming to this protocol can act as REST response types. +protocol RestResponse { + associatedtype Output + + static func decodeResponse(_ data: Data) throws -> Output +} + +/// Any `Decodable` can be REST response +extension Decodable where Self: RestResponse { + static func decodeResponse(_ data: Data) throws -> Self { + try MullvadRest.makeJSONDecoder().decode(Self.self, from: data) + } +} + +/// An empty REST response type that cannot be instantiated and is only used to produce an empty +/// output. +enum EmptyResponse {} +extension EmptyResponse: RestResponse { + static func decodeResponse(_ data: Data) throws -> () { + return () + } +} + +/// Any `Encodable` type can be injected as JSON payload +extension RestPayload where Self: Encodable { + func inject(into request: inout URLRequest) throws { + request.httpBody = try MullvadRest.makeJSONEncoder().encode(self) + } +} + +// MARK: - Operations + +final class RestOperation<Input, Response>: AsyncOperation, InputOperation, OutputOperation + where Input: RestPayload, Response: RestResponse +{ + typealias Output = Result<Response.Output, RestError> + + private let endpoint: RestEndpoint<Input, Response> + private let session: URLSession + private var task: URLSessionTask? + + init(endpoint: RestEndpoint<Input, Response>, session: URLSession, input: Input? = nil) { + self.endpoint = endpoint + self.session = session + + super.init() + self.input = input + } + + override func main() { + guard let payload = self.input else { + finish() + return + } + + let result = endpoint.dataTask(session: session, payload: payload) { [weak self] (result) in + self?.finish(with: result) + } + + switch result { + case .success(let task): + self.task = task + task.resume() + case .failure(let error): + finish(with: .failure(error)) + } + } + + override func operationDidCancel() { + task?.cancel() + task = nil + } +} + +// MARK: - Endpoints + +/// A struct that describes the REST endpoint, including the expected input and output +struct RestEndpoint<Input, Response> where Input: RestPayload, Response: RestResponse { + let endpointURL: URL + let httpMethod: HttpMethod + + init(endpointURL: URL, httpMethod: HttpMethod) { + self.endpointURL = endpointURL + self.httpMethod = httpMethod + } + + /// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the + /// expected response type or error upon completion. + func dataTask(session: URLSession, payload: Input, completionHandler: @escaping (Result<Response.Output, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> { + return makeURLRequest(payload: payload).map { (request) -> URLSessionDataTask in + return session.dataTask(with: request) { (responseData, urlResponse, error) in + let result = Self.handleURLResponse(urlResponse, data: responseData, error: error) + completionHandler(result) + } + } + } + + /// Create `RestOperation` that automatically parses the response and sets the expected output + /// type or error upon completion. + func operation(session: URLSession, payload: Input?) -> RestOperation<Input, Response> { + return RestOperation(endpoint: self, session: session, input: payload) + } + + /// Create `URLRequest` that can be used to send an HTTP request + private func makeURLRequest(payload: Input) -> Result<URLRequest, RestError> { + var request = makeEndpointURLRequest() + do { + try payload.inject(into: &request) + + return .success(request) + } catch { + return .failure(.encodePayload(error)) + } + } + + /// Create a boilerplate `URLRequest` before injecting the payload + private func makeEndpointURLRequest() -> URLRequest { + var request = URLRequest( + url: endpointURL, + cachePolicy: .useProtocolCachePolicy, + timeoutInterval: kNetworkTimeout + ) + request.httpShouldHandleCookies = false + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = httpMethod.rawValue + return request + } + + /// A private HTTP response handler + private static func handleURLResponse(_ urlResponse: URLResponse?, data: Data?, error: Error?) -> Result<Response.Output, RestError> { + if let error = error { + let networkError = error as? URLError ?? URLError(.unknown) + + return .failure(.network(networkError)) + } + + guard let httpResponse = urlResponse as? HTTPURLResponse else { + return .failure(.network(URLError(.unknown))) + } + + let data = data ?? Data() + + // Treat all 2xx responses as success despite the subtle meaning they may convey + if (200..<300).contains(httpResponse.statusCode) { + return Self.decodeSuccessResponse(data) + } else { + return Self.decodeErrorResponse(data) + .flatMap { (serverErrorResponse) -> Result<Response.Output, RestError> in + return .failure(.server(serverErrorResponse)) + } + } + } + + /// A private helper that parses the JSON response in case of success (HTTP 2xx) + private static func decodeSuccessResponse(_ responseData: Data) -> Result<Response.Output, RestError> { + return Result { () -> Response.Output in + return try Response.decodeResponse(responseData) + }.mapError({ (error) -> RestError in + return .decodeSuccessResponse(error) + }) + } + + /// A private helper that parses the JSON response in case of error (Any HTTP code except 2xx) + private static func decodeErrorResponse(_ responseData: Data) -> Result<ServerErrorResponse, RestError> { + return Result { () -> ServerErrorResponse in + return try ServerErrorResponse.decodeResponse(responseData) + }.mapError({ (error) -> RestError in + return .decodeErrorResponse(error) + }) + } +} + +/// A convenience class for `RestEndpoint` that transparently provides it with the `URLSession` +struct RestSessionEndpoint<Input, Response> where Input: RestPayload, Response: RestResponse { + let session: URLSession + let endpoint: RestEndpoint<Input, Response> + + init(session: URLSession, endpoint: RestEndpoint<Input, Response>) { + self.session = session + self.endpoint = endpoint + } + + /// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the + /// expected response type or error upon completion. + func dataTask(payload: Input, completionHandler: @escaping (Result<Response.Output, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> { + return endpoint.dataTask(session: session, payload: payload, completionHandler: completionHandler) + } + + /// Create `RestOperation` that automatically parses the response and sets the expected output + /// type or error upon completion. + func operation(payload: Input?) -> RestOperation<Input, Response> { + return endpoint.operation(session: session, payload: payload) + } +} + +// MARK: - REST interface + +struct MullvadRest { + let session: URLSession + + init(session: URLSession = URLSession(configuration: .ephemeral)) { + self.session = session + } + + func createAccount() -> RestSessionEndpoint<EmptyPayload, AccountResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.createAccount()) + } + + func getRelays() -> RestSessionEndpoint<EmptyPayload, ServerRelaysResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.getRelays()) + } + + func getAccountExpiry() -> RestSessionEndpoint<TokenPayload<EmptyPayload>, AccountResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.getAccountExpiry()) + } + + func getWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, WireguardAddressesResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.getWireguardKey()) + } + + func pushWireguardKey() -> RestSessionEndpoint<TokenPayload<PushWireguardKeyRequest>, WireguardAddressesResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.pushWireguardKey()) + } + + func replaceWireguardKey() -> RestSessionEndpoint<TokenPayload<ReplaceWireguardKeyRequest>, WireguardAddressesResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.replaceWireguardKey()) + } + + func deleteWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, EmptyResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.deleteWireguardKey()) + } + + func createApplePayment() -> RestSessionEndpoint<TokenPayload<CreateApplePaymentRequest>, CreateApplePaymentResponse> { + return RestSessionEndpoint(session: session, endpoint: Self.createApplePayment()) + } +} + +extension MullvadRest { + /// POST /v1/accounts + static func createAccount() -> RestEndpoint<EmptyPayload, AccountResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("accounts"), + httpMethod: .post + ) + } + + /// GET /v1/relays + static func getRelays() -> RestEndpoint<EmptyPayload, ServerRelaysResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("relays"), + httpMethod: .get + ) + } + + /// GET /v1/me + static func getAccountExpiry() -> RestEndpoint<TokenPayload<EmptyPayload>, AccountResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("me"), + httpMethod: .get + ) + } + /// GET /v1/wireguard-keys/{pubkey} + static func getWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, WireguardAddressesResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"), + httpMethod: .get + ) + } + + /// POST /v1/wireguard-keys + static func pushWireguardKey() -> RestEndpoint<TokenPayload<PushWireguardKeyRequest>, WireguardAddressesResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"), + httpMethod: .post + ) + } + + /// POST /v1/replace-wireguard-key + static func replaceWireguardKey() -> RestEndpoint<TokenPayload<ReplaceWireguardKeyRequest>, WireguardAddressesResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("replace-wireguard-key"), + httpMethod: .post + ) + } + + /// DELETE /v1/wireguard-keys/{pubkey} + static func deleteWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, EmptyResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"), + httpMethod: .delete + ) + } + + /// POST /v1/create-apple-payment + static func createApplePayment() -> RestEndpoint<TokenPayload<CreateApplePaymentRequest>, CreateApplePaymentResponse> { + return RestEndpoint( + endpointURL: kRestBaseURL.appendingPathComponent("create-apple-payment"), + httpMethod: .post + ) + } + + /// Returns a JSON encoder used by REST API + static func makeJSONEncoder() -> JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.dateEncodingStrategy = .iso8601 + encoder.dataEncodingStrategy = .base64 + return encoder + } + + /// Returns a JSON decoder used by REST API + static func makeJSONDecoder() -> JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + decoder.dataDecodingStrategy = .base64 + return decoder + } +} + + +// MARK: - Payload types + +/// A payload that adds the authentication token into HTTP Authorization header +struct TokenPayload<Payload: RestPayload>: RestPayload { + let token: String + let payload: Payload + + init(token: String, payload: Payload) { + self.token = token + self.payload = payload + } + + func inject(into request: inout URLRequest) throws { + request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") + try payload.inject(into: &request) + } +} + +/// A payload that adds the public key into the URL path +struct PublicKeyPayload<Payload: RestPayload>: RestPayload { + let pubKey: Data + let payload: Payload + + init(pubKey: Data, payload: Payload) { + self.pubKey = pubKey + self.payload = payload + } + + func inject(into request: inout URLRequest) throws { + request.url = request.url?.appendingPathComponent(pubKey.base64EncodedString()) + try payload.inject(into: &request) + } +} + +/// An empty payload placeholder type. +/// Use it in places where the payload is not expected +struct EmptyPayload: RestPayload { + init() {} + func inject(into request: inout URLRequest) throws {} +} + + +// MARK: - Response types + +struct AccountResponse: Decodable, RestResponse { + let token: String + let expires: Date +} + +struct ServerLocation: Codable { + let country: String + let city: String + let latitude: Double + let longitude: Double +} + +struct ServerRelay: Codable { + let hostname: String + let active: Bool + let owned: Bool + let location: String + let provider: String + let weight: Int32 + let ipv4AddrIn: IPv4Address + let ipv6AddrIn: IPv6Address + let publicKey: Data + let includeInCountry: Bool +} + +struct ServerWireguardTunnels: Codable { + let ipv4Gateway: IPv4Address + let ipv6Gateway: IPv6Address + let portRanges: [ClosedRange<UInt16>] + let relays: [ServerRelay] +} + +struct ServerRelaysResponse: Codable, RestResponse { + let locations: [String: ServerLocation] + let wireguard: ServerWireguardTunnels +} + +struct PushWireguardKeyRequest: Encodable, RestPayload { + let pubkey: Data +} + +struct WireguardAddressesResponse: Decodable, RestResponse { + let id: String + let pubkey: Data + let ipv4Address: IPAddressRange + let ipv6Address: IPAddressRange +} + +struct ReplaceWireguardKeyRequest: Encodable, RestPayload { + let old: Data + let new: Data +} + +struct CreateApplePaymentRequest: Encodable, RestPayload { + let receiptString: Data +} + +struct CreateApplePaymentResponse: Decodable, RestResponse { + let timeAdded: Int + let newExpiry: Date + + /// Returns a formatted string for the `timeAdded` interval, i.e "30 days" + var formattedTimeAdded: String? { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + + return formatter.string(from: TimeInterval(timeAdded)) + } +} diff --git a/ios/MullvadVPN/MullvadRpc.swift b/ios/MullvadVPN/MullvadRpc.swift deleted file mode 100644 index 59c53348fe..0000000000 --- a/ios/MullvadVPN/MullvadRpc.swift +++ /dev/null @@ -1,347 +0,0 @@ -// -// MullvadRpc.swift -// MullvadVPN -// -// Created by pronebird on 02/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Network - -/// API server URL -private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")! - -/// Network request timeout in seconds -private let kNetworkTimeout: TimeInterval = 10 - -/// A response received when sending the AppStore receipt to the backend -struct SendAppStoreReceiptResponse: Codable { - let timeAdded: TimeInterval - let newExpiry: Date - - /// Returns a formatted string for the `timeAdded` interval, i.e "30 days" - var formattedTimeAdded: String? { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - - return formatter.string(from: timeAdded) - } -} - -class MullvadRpc { - private let session: URLSession - - /// A enum mapping the integer codes returned by Mullvad API with the corresponding enum - /// variants - private enum RawResponseCode: Int { - case accountDoesNotExist = -200 - case tooManyWireguardKeys = -703 - } - - /// A enum describing the Mullvad API response code - enum ResponseCode: RawRepresentable, Codable { - var rawValue: Int { - switch self { - case .accountDoesNotExist: - return RawResponseCode.accountDoesNotExist.rawValue - - case .tooManyWireguardKeys: - return RawResponseCode.tooManyWireguardKeys.rawValue - - case .other(let value): - return value - } - } - - init?(rawValue: Int) { - switch RawResponseCode(rawValue: rawValue) { - case .accountDoesNotExist: - self = .accountDoesNotExist - case .tooManyWireguardKeys: - self = .tooManyWireguardKeys - case .none: - self = ResponseCode.other(rawValue) - } - } - - case accountDoesNotExist - case tooManyWireguardKeys - case other(Int) - } - - /// An error type emitted by `MullvadRpc` - enum Error: ChainedError { - /// A network communication error - case network(URLError) - - /// A server error - case server(JsonRpcResponseError<ResponseCode>) - - /// An error occured when decoding the JSON response - case decoding(Swift.Error) - - /// An error occured when encoding the JSON request - case encoding(Swift.Error) - - var errorDescription: String? { - switch self { - case .network: - return "Network error" - - case .server: - return "Server error" - - case .encoding: - return "Encoding error" - - case .decoding: - return "Decoding error" - } - } - } - - /// Returns an instance of `MullvadRpc` configured with ephemeral `URLSession` configuration - class func withEphemeralURLSession() -> MullvadRpc { - return MullvadRpc(session: URLSession(configuration: .ephemeral)) - } - - class func makeJSONEncoder() -> JSONEncoder { - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - encoder.dateEncodingStrategy = .iso8601 - encoder.dataEncodingStrategy = .base64 - return encoder - } - - class func makeJSONDecoder() -> JSONDecoder { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 - decoder.dataDecodingStrategy = .base64 - return decoder - } - - init(session: URLSession) { - self.session = session - } - - func createAccount() -> MullvadRpc.Request<String> { - let request = JsonRpcRequest(method: "create_account", params: []) - - return MullvadRpc.Request(session: session, request: request) - } - - func getRelayList() -> MullvadRpc.Request<RelayList> { - let request = JsonRpcRequest(method: "relay_list_v3", params: []) - - return MullvadRpc.Request(session: session, request: request) - } - - func getAccountExpiry(accountToken: String) -> MullvadRpc.Request<Date> { - let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)]) - - return MullvadRpc.Request(session: session, request: request) - } - - func getAccountExpiry(request: MullvadRpc.Request<Date>? = nil) -> MullvadRpc.Operation<Date> { - return MullvadRpc.Operation(request: request) - } - - func pushWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<WireguardAssociatedAddresses> { - let request = JsonRpcRequest(method: "push_wg_key", params: [ - AnyEncodable(accountToken), - AnyEncodable(publicKey) - ]) - - return MullvadRpc.Request(session: session, request: request) - } - - func replaceWireguardKey(accountToken: String, oldPublicKey: Data, newPublicKey: Data) -> MullvadRpc.Request<WireguardAssociatedAddresses> { - let request = JsonRpcRequest(method: "replace_wg_key", params: [ - AnyEncodable(accountToken), - AnyEncodable(oldPublicKey), - AnyEncodable(newPublicKey) - ]) - - return MullvadRpc.Request(session: session, request: request) - } - - func checkWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<Bool> { - let request = JsonRpcRequest(method: "check_wg_key", params: [ - AnyEncodable(accountToken), - AnyEncodable(publicKey) - ]) - - return MullvadRpc.Request(session: session, request: request) - } - - func checkWireguardKey(request: MullvadRpc.Request<Bool>? = nil) -> MullvadRpc.Operation<Bool> { - return MullvadRpc.Operation(request: request) - } - - func removeWireguardKey(accountToken: String, publicKey: Data) -> MullvadRpc.Request<Bool> { - let request = JsonRpcRequest(method: "remove_wg_key", params: [ - AnyEncodable(accountToken), - AnyEncodable(publicKey) - ]) - - return MullvadRpc.Request(session: session, request: request) - } - - func sendAppStoreReceipt(accountToken: String, receiptData: Data) -> MullvadRpc.Request<SendAppStoreReceiptResponse> { - let request = JsonRpcRequest(method: "apple_payment", params: [ - AnyEncodable(accountToken), - AnyEncodable(receiptData) - ]) - - return MullvadRpc.Request(session: session, request: request) - } -} - - -extension JsonRpcResponseError: LocalizedError - where - ResponseCode == MullvadRpc.ResponseCode -{ - var errorDescription: String? { - switch code { - case .accountDoesNotExist: - return NSLocalizedString("Invalid account", comment: "") - - case .tooManyWireguardKeys: - return NSLocalizedString("Too many public WireGuard keys", comment: "") - - case .other: - return nil - } - } - - var recoverySuggestion: String? { - switch code { - case .tooManyWireguardKeys: - return NSLocalizedString("Remove unused WireGuard keys", comment: "") - - default: - return nil - } - } -} - - -extension MullvadRpc { - - class Request<Response: Decodable> { - typealias RequestCompletionHandler = (Result<Response, MullvadRpc.Error>) -> Void - - private let session: URLSession - private let request: JsonRpcRequest - - private let lock = NSLock() - private var urlSessionTask: URLSessionTask? - - fileprivate init(session: URLSession, request: JsonRpcRequest) { - self.session = session - self.request = request - } - - func start(completionHandler: @escaping RequestCompletionHandler) { - lock.withCriticalBlock { - assert(self.urlSessionTask == nil) - - switch makeURLRequest() { - case .success(let urlRequest): - let task = session.dataTask(with: urlRequest) { (responseData, urlResponse, error) in - switch (responseData, error) { - case (.some(let data), .none): - completionHandler(Self.decodeResponse(data)) - - case (.none, .some(let urlError as URLError)): - completionHandler(.failure(.network(urlError))) - - default: - fatalError() - } - } - self.urlSessionTask = task - task.resume() - - case .failure(let error): - completionHandler(.failure(error)) - } - } - } - - func cancel() { - lock.withCriticalBlock { - self.urlSessionTask?.cancel() - } - } - - func operation() -> MullvadRpc.Operation<Response> { - return MullvadRpc.Operation(request: self) - } - - private func makeURLRequest() -> Result<URLRequest, MullvadRpc.Error> { - do { - let data = try MullvadRpc.makeJSONEncoder().encode(request) - - return .success(Self.makeURLRequest(httpBody: data)) - } catch { - return .failure(.encoding(error)) - } - } - - private static func decodeResponse(_ responseData: Data) -> Result<Response, MullvadRpc.Error> { - do { - let serverResponse = try MullvadRpc.makeJSONDecoder() - .decode(JsonRpcResponse<Response, MullvadRpc.ResponseCode>.self, from: responseData) - - // unwrap JsonRpcResponse.result - return serverResponse.result - .mapError { .server($0) } - } catch { - return .failure(.decoding(error)) - } - } - - private static func makeURLRequest(httpBody: Data) -> URLRequest { - var request = URLRequest( - url: kMullvadAPIURL, - cachePolicy: .useProtocolCachePolicy, - timeoutInterval: kNetworkTimeout - ) - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - request.httpBody = httpBody - - return request - } - } - - class Operation<Response>: AsyncOperation, InputOperation, OutputOperation where Response: Decodable { - typealias Input = Request<Response> - typealias Output = Result<Response, MullvadRpc.Error> - - init(request: Input? = nil) { - super.init() - self.input = request - } - - override func main() { - guard let request = self.input else { - self.finish() - return - } - - request.start { [weak self] (result) in - self?.finish(with: result) - } - } - - override func operationDidCancel() { - input?.cancel() - } - } -} diff --git a/ios/MullvadVPN/Operations/OperationBlockObserver.swift b/ios/MullvadVPN/Operations/OperationBlockObserver.swift index 728e9a5a02..eccb7f63ab 100644 --- a/ios/MullvadVPN/Operations/OperationBlockObserver.swift +++ b/ios/MullvadVPN/Operations/OperationBlockObserver.swift @@ -12,22 +12,41 @@ class OperationBlockObserver<OperationType: OperationProtocol>: OperationObserve private var willFinish: ((OperationType) -> Void)? private var didFinish: ((OperationType) -> Void)? - init(willFinish: ((OperationType) -> Void)? = nil, didFinish: ((OperationType) -> Void)? = nil) { + let queue: DispatchQueue? + + init(queue: DispatchQueue? = nil, willFinish: ((OperationType) -> Void)? = nil, didFinish: ((OperationType) -> Void)? = nil) { + self.queue = queue self.willFinish = willFinish self.didFinish = didFinish } func operationWillFinish(_ operation: OperationType) { - self.willFinish?(operation) + if let willFinish = self.willFinish { + scheduleEvent { + willFinish(operation) + } + } } func operationDidFinish(_ operation: OperationType) { - self.didFinish?(operation) + if let didFinish = self.didFinish { + scheduleEvent { + didFinish(operation) + } + } + } + + private func scheduleEvent(_ body: @escaping () -> Void) { + if let queue = queue { + queue.async(execute: body) + } else { + body() + } } } extension OperationProtocol { - func addDidFinishBlockObserver(_ block: @escaping (Self) -> Void) { - addObserver(OperationBlockObserver(didFinish: block)) + func addDidFinishBlockObserver(queue: DispatchQueue? = nil, _ block: @escaping (Self) -> Void) { + addObserver(OperationBlockObserver(queue: queue, didFinish: block)) } } diff --git a/ios/MullvadVPN/Operations/OutputOperation.swift b/ios/MullvadVPN/Operations/OutputOperation.swift index 533d5e5151..064d54a2e9 100644 --- a/ios/MullvadVPN/Operations/OutputOperation.swift +++ b/ios/MullvadVPN/Operations/OutputOperation.swift @@ -40,8 +40,8 @@ extension OutputOperation where Self: OperationSubclassing { } extension OperationProtocol where Self: OutputOperation { - func addDidFinishBlockObserver(_ block: @escaping (Self, Output) -> Void) { - addDidFinishBlockObserver { (operation) in + func addDidFinishBlockObserver(queue: DispatchQueue? = nil, _ block: @escaping (Self, Output) -> Void) { + addDidFinishBlockObserver(queue: queue) { (operation) in if let output = operation.output { block(operation, output) } diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift index 7444078e29..bf15933187 100644 --- a/ios/MullvadVPN/RelayCache.swift +++ b/ios/MullvadVPN/RelayCache.swift @@ -20,7 +20,7 @@ enum RelayCacheError: ChainedError { case writeCache(Error) case encodeCache(Error) case decodeCache(Error) - case rpc(MullvadRpc.Error) + case rest(RestError) var errorDescription: String? { switch self { @@ -36,14 +36,14 @@ enum RelayCacheError: ChainedError { return "Decode pre-bundled relays error" case .writeCache: return "Write cache error" - case .rpc: - return "RPC error" + case .rest: + return "REST error" } } } protocol RelayCacheObserver: class { - func relayCache(_ relayCache: RelayCache, didUpdateCachedRelayList cachedRelayList: CachedRelayList) + func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays) } private class AnyRelayCacheObserver: WeakObserverBox, RelayCacheObserver { @@ -56,8 +56,8 @@ private class AnyRelayCacheObserver: WeakObserverBox, RelayCacheObserver { self.inner = inner } - func relayCache(_ relayCache: RelayCache, didUpdateCachedRelayList cachedRelayList: CachedRelayList) { - inner?.relayCache(relayCache, didUpdateCachedRelayList: cachedRelayList) + func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays) { + inner?.relayCache(relayCache, didUpdateCachedRelays: cachedRelays) } static func == (lhs: AnyRelayCacheObserver, rhs: AnyRelayCacheObserver) -> Bool { @@ -66,8 +66,8 @@ private class AnyRelayCacheObserver: WeakObserverBox, RelayCacheObserver { } class RelayCache { - /// Mullvad Rpc client - private let rpc: MullvadRpc + /// Mullvad REST client + private let rest: MullvadRest /// The cache location used by the class instance private let cacheFileURL: URL @@ -82,7 +82,7 @@ class RelayCache { private var isPeriodicUpdatesEnabled = false /// A download task used for relay RPC request - private var downloadRequest: MullvadRpc.Request<RelayList>? + private var downloadTask: URLSessionTask? /// The default cache file location static var defaultCacheFileURL: URL { @@ -104,7 +104,7 @@ class RelayCache { static let shared = RelayCache(cacheFileURL: defaultCacheFileURL, networkSession: URLSession(configuration: .ephemeral)) private init(cacheFileURL: URL, networkSession: URLSession) { - rpc = MullvadRpc(session: networkSession) + rest = MullvadRest(session: networkSession) self.cacheFileURL = cacheFileURL } @@ -142,7 +142,7 @@ class RelayCache { self.timerSource?.cancel() self.timerSource = nil - self.downloadRequest?.cancel() + self.downloadTask?.cancel() completionHandler?() } @@ -155,13 +155,14 @@ class RelayCache { } /// Read the relay cache from disk - func read(completionHandler: @escaping (Result<CachedRelayList, RelayCacheError>) -> Void) { + func read(completionHandler: @escaping (Result<CachedRelays, RelayCacheError>) -> Void) { dispatchQueue.async { let result = Self.read(cacheFileURL: self.cacheFileURL) - .flatMapError { (error) -> Result<CachedRelayList, RelayCacheError> in - if case .readCache(let ioError as CocoaError) = error, ioError.code == .fileReadNoSuchFile { + .flatMapError { (error) -> Result<CachedRelays, RelayCacheError> in + switch error { + case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile): return Self.readPrebundledRelays(fileURL: Self.preBundledRelaysFileURL) - } else { + default: return .failure(error) } } @@ -200,20 +201,20 @@ class RelayCache { } private func downloadRelays() { - let newDownloadRequest = startDownloadTask { (result) in - let result = result.flatMap { (relayList) -> Result<CachedRelayList, RelayCacheError> in - let cachedRelayList = CachedRelayList(relayList: relayList, updatedAt: Date()) + let taskResult = makeDownloadTask { (result) in + let result = result.flatMap { (relays) -> Result<CachedRelays, RelayCacheError> in + let cachedRelays = CachedRelays(relays: relays, updatedAt: Date()) - return Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelayList) - .map { cachedRelayList } + return Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) + .map { cachedRelays } } switch result { - case .success(let cachedRelayList): - os_log(.default, "Downloaded %d relays", cachedRelayList.relayList.numRelays) + case .success(let cachedRelays): + os_log(.default, "Downloaded %d relays", cachedRelays.relays.wireguard.relays.count) self.observerList.forEach { (observer) in - observer.relayCache(self, didUpdateCachedRelayList: cachedRelayList) + observer.relayCache(self, didUpdateCachedRelays: cachedRelays) } case .failure(let error): @@ -221,8 +222,17 @@ class RelayCache { } } - downloadRequest?.cancel() - downloadRequest = newDownloadRequest + downloadTask?.cancel() + + switch taskResult { + case .success(let newDownloadTask): + downloadTask = newDownloadTask + newDownloadTask.resume() + + case .failure(let restError): + restError.logChain(message: "Failed to create a REST request for updating relays", log: .default) + downloadTask = nil + } } private func scheduleRepeatingTimer(startTime: DispatchWallTime) { @@ -241,56 +251,19 @@ class RelayCache { self.timerSource = timerSource } - private func startDownloadTask(completionHandler: @escaping (Result<RelayList, RelayCacheError>) -> Void) -> MullvadRpc.Request<RelayList>? { - let request = rpc.getRelayList() - - request.start { (result) in + private func makeDownloadTask(completionHandler: @escaping (Result<ServerRelaysResponse, RelayCacheError>) -> Void) -> Result<URLSessionDataTask, RestError> { + return rest.getRelays().dataTask(payload: EmptyPayload()) { (result) in self.dispatchQueue.async { - let result = result - .map(Self.filterRelayList) - .mapError { RelayCacheError.rpc($0) } - - completionHandler(result) + completionHandler(result.mapError { RelayCacheError.rest($0) }) } } - - return request } // MARK: - Private class methods - /// Filters the given `RelayList` removing empty leaf nodes, relays without Wireguard tunnels or - /// Wireguard tunnels without any available ports. - private class func filterRelayList(_ relayList: RelayList) -> RelayList { - let filteredCountries = relayList.countries - .map { (country) -> RelayList.Country in - var filteredCountry = country - - filteredCountry.cities = country.cities.map { (city) -> RelayList.City in - var filteredCity = city - - filteredCity.relays = city.relays - .map { (relay) -> RelayList.Relay in - var filteredRelay = relay - - // filter out tunnels without ports - filteredRelay.tunnels?.wireguard = relay.tunnels?.wireguard? - .filter { !$0.portRanges.isEmpty } - - return filteredRelay - }.filter { $0.tunnels?.wireguard.flatMap { !$0.isEmpty } ?? false } - - return filteredCity - }.filter { !$0.relays.isEmpty } - - return filteredCountry - }.filter { !$0.cities.isEmpty } - - return RelayList(countries: filteredCountries) - } /// Safely read the cache file from disk using file coordinator - private class func read(cacheFileURL: URL) -> Result<CachedRelayList, RelayCacheError> { - var result: Result<CachedRelayList, RelayCacheError>? + private class func read(cacheFileURL: URL) -> Result<CachedRelays, RelayCacheError> { + var result: Result<CachedRelays, RelayCacheError>? let fileCoordinator = NSFileCoordinator(filePresenter: nil) let accessor = { (fileURLForReading: URL) -> Void in @@ -298,7 +271,7 @@ class RelayCache { result = Result { try Data(contentsOf: fileURLForReading) } .mapError { RelayCacheError.readCache($0) } .flatMap { (data) in - Result { try JSONDecoder().decode(CachedRelayList.self, from: data) } + Result { try JSONDecoder().decode(CachedRelays.self, from: data) } .mapError { RelayCacheError.decodeCache($0) } } } @@ -316,15 +289,15 @@ class RelayCache { return result! } - private class func readPrebundledRelays(fileURL: URL) -> Result<CachedRelayList, RelayCacheError> { + private class func readPrebundledRelays(fileURL: URL) -> Result<CachedRelays, RelayCacheError> { return Result { try Data(contentsOf: fileURL) } .mapError { RelayCacheError.readPrebundledRelays($0) } - .flatMap { (data) -> Result<CachedRelayList, RelayCacheError> in - return Result { try MullvadRpc.makeJSONDecoder().decode(RelayList.self, from: data) } + .flatMap { (data) -> Result<CachedRelays, RelayCacheError> in + return Result { try MullvadRest.makeJSONDecoder().decode(ServerRelaysResponse.self, from: data) } .mapError { RelayCacheError.decodePrebundledRelays($0) } - .map { (relayList) -> CachedRelayList in - return CachedRelayList( - relayList: Self.filterRelayList(relayList), + .map { (relays) -> CachedRelays in + return CachedRelays( + relays: relays, updatedAt: Date(timeIntervalSince1970: 0) ) } @@ -332,7 +305,7 @@ class RelayCache { } /// Safely write the cache file on disk using file coordinator - private class func write(cacheFileURL: URL, record: CachedRelayList) -> Result<(), RelayCacheError> { + private class func write(cacheFileURL: URL, record: CachedRelays) -> Result<(), RelayCacheError> { var result: Result<(), RelayCacheError>? let fileCoordinator = NSFileCoordinator(filePresenter: nil) @@ -380,7 +353,7 @@ class RelayCache { case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache: return true - case .readCache(let error as CocoaError) where error.code == .fileReadNoSuchFile: + case .readCache(CocoaError.fileReadNoSuchFile): return true default: @@ -390,9 +363,9 @@ class RelayCache { } /// A struct that represents the relay cache on disk -struct CachedRelayList: Codable { +struct CachedRelays: Codable { /// The relay list stored within the cache entry - var relayList: RelayList + var relays: ServerRelaysResponse /// The date when this cache was last updated var updatedAt: Date diff --git a/ios/MullvadVPN/RelayList.swift b/ios/MullvadVPN/RelayList.swift deleted file mode 100644 index cf9fe7edd7..0000000000 --- a/ios/MullvadVPN/RelayList.swift +++ /dev/null @@ -1,89 +0,0 @@ - -// -// RelayList.swift -// MullvadVPN -// -// Created by pronebird on 02/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Network - -struct RelayList: Codable { - struct Country: Codable { - var name: String - var code: String - var cities: [City] - } - - struct City: Codable { - var name: String - var code: String - var latitude: Double - var longitude: Double - var relays: [Relay] - } - - struct Relay: Codable { - var hostname: String - var ipv4AddrIn: IPv4Address - var includeInCountry: Bool - var active: Bool - var weight: Int32 - var tunnels: Tunnels? - } - - struct Tunnels: Codable { - var wireguard: [WireguardTunnel]? - } - - struct WireguardTunnel: Codable { - var ipv4Gateway: IPv4Address - var ipv6Gateway: IPv6Address - var publicKey: Data - var portRanges: [ClosedRange<UInt16>] - } - - var countries: [Country] -} - -extension RelayList { - - /// Returns the total number of relays - var numRelays: Int { - return countries.reduce(0) { (accum, country) -> Int in - return country.cities.reduce(accum, { (accum, city) -> Int in - return accum + city.relays.count - }) - } - } - - /// Returns an alphabetically sorted `RelayList` - func sorted() -> Self { - let lexicalComparator = { (a: String, b: String) -> Bool in - return a.localizedCaseInsensitiveCompare(b) == .orderedAscending - } - - let fileComparator = { (a: String, b: String) -> Bool in - return a.localizedStandardCompare(b) == .orderedAscending - } - - let sortedCountries = countries - .sorted { lexicalComparator($0.name, $1.name) } - .map { (country) -> RelayList.Country in - var sortedCountry = country - sortedCountry.cities = country.cities.sorted { lexicalComparator($0.name, $1.name) } - .map({ (city) -> RelayList.City in - var sortedCity = city - sortedCity.relays = city.relays - .sorted { fileComparator($0.hostname, $1.hostname) } - return sortedCity - }) - return sortedCountry - } - - return RelayList(countries: sortedCountries) - } - -} diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift index 716a62fc9a..baf0595518 100644 --- a/ios/MullvadVPN/RelaySelector.swift +++ b/ios/MullvadVPN/RelaySelector.swift @@ -10,57 +10,51 @@ import Foundation import Network struct RelaySelectorResult { - var relay: RelayList.Relay - var tunnel: RelayList.WireguardTunnel var endpoint: MullvadEndpoint + var relay: ServerRelay var location: Location } private struct RelayWithLocation { - var relay: RelayList.Relay + var relay: ServerRelay var location: Location } struct RelaySelector { - private let relayList: RelayList + private let relays: ServerRelaysResponse - init(relayList: RelayList) { - self.relayList = relayList + init(relays: ServerRelaysResponse) { + self.relays = relays } func evaluate(with constraints: RelayConstraints) -> RelaySelectorResult? { - let relays = Self.applyConstraints(constraints, relays: Self.parseRelayList(self.relayList)) - let totalWeight = relays.reduce(0) { $0 + $1.relay.weight } + let filteredRelays = Self.applyConstraints(constraints, relays: Self.parseRelaysResponse(self.relays)) + let totalWeight = filteredRelays.reduce(0) { $0 + $1.relay.weight } guard totalWeight > 0 else { return nil } guard var i = (0...totalWeight).randomElement() else { return nil } - let relayWithLocation = relays.first { (relayWithLocation) -> Bool in + let relayWithLocation = filteredRelays.first { (relayWithLocation) -> Bool in i -= relayWithLocation.relay.weight return i <= 0 }.unsafelyUnwrapped - guard let tunnel = relayWithLocation.relay.tunnels?.wireguard?.randomElement() else { - return nil - } - - guard let port = tunnel.portRanges.randomElement()?.randomElement() else { + guard let port = relays.wireguard.portRanges.randomElement()?.randomElement() else { return nil } let endpoint = MullvadEndpoint( ipv4Relay: IPv4Endpoint(ip: relayWithLocation.relay.ipv4AddrIn, port: port), ipv6Relay: nil, - ipv4Gateway: tunnel.ipv4Gateway, - ipv6Gateway: tunnel.ipv6Gateway, - publicKey: tunnel.publicKey + ipv4Gateway: relays.wireguard.ipv4Gateway, + ipv6Gateway: relays.wireguard.ipv6Gateway, + publicKey: relayWithLocation.relay.publicKey ) return RelaySelectorResult( - relay: relayWithLocation.relay, - tunnel: tunnel, endpoint: endpoint, + relay: relayWithLocation.relay, location: relayWithLocation.location ) } @@ -87,45 +81,29 @@ struct RelaySelector { relayWithLocation.relay.hostname == hostname } } - }.map({ (relayWithLocation) -> RelayWithLocation in - var filteredRelay = relayWithLocation - let wireguardTunnels = filteredRelay.relay.tunnels?.wireguard? - .filter { !$0.portRanges.isEmpty } - - filteredRelay.relay.tunnels?.wireguard = wireguardTunnels - - return filteredRelay - }).filter { (relayWithLocation) -> Bool in - guard let wireguardTunnels = relayWithLocation.relay.tunnels?.wireguard else { return false } - - return relayWithLocation.relay.active && !wireguardTunnels.isEmpty + }.filter { (relayWithLocation) -> Bool in + return relayWithLocation.relay.active } } - private static func parseRelayList(_ relayList: RelayList) -> [RelayWithLocation] { - var relays = [RelayWithLocation]() + private static func parseRelaysResponse(_ response: ServerRelaysResponse) -> [RelayWithLocation] { + return response.wireguard.relays.compactMap { (serverRelay) -> RelayWithLocation? in + guard let serverLocation = response.locations[serverRelay.location] else { return nil } - for country in relayList.countries { - for city in country.cities { - for relay in city.relays { - let location = Location( - country: country.name, - countryCode: country.code, - city: city.name, - cityCode: city.code, - latitude: city.latitude, - longitude: city.longitude - ) - let relayWithLocation = RelayWithLocation( - relay: relay, - location: location - ) - relays.append(relayWithLocation) - } - } - } + let locationComponents = serverRelay.location.split(separator: "-") + guard locationComponents.count > 1 else { return nil } + + let location = Location( + country: serverLocation.country, + countryCode: String(locationComponents[0]), + city: serverLocation.city, + cityCode: String(locationComponents[1]), + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) - return relays + return RelayWithLocation(relay: serverRelay, location: location) + } } } diff --git a/ios/MullvadVPN/SegueIdentifier.swift b/ios/MullvadVPN/SegueIdentifier.swift index fa7a3cbc33..d6345a4e06 100644 --- a/ios/MullvadVPN/SegueIdentifier.swift +++ b/ios/MullvadVPN/SegueIdentifier.swift @@ -24,7 +24,6 @@ extension SegueIdentifier { enum Connect: String, SegueConvertible { case embedTunnelControls = "EmbedTunnelControls" - case showRelaySelector = "ShowRelaySelector" } enum SelectLocation: String, SegueConvertible { diff --git a/ios/MullvadVPN/SelectLocationController.swift b/ios/MullvadVPN/SelectLocationController.swift index c1c8b7d938..affb127003 100644 --- a/ios/MullvadVPN/SelectLocationController.swift +++ b/ios/MullvadVPN/SelectLocationController.swift @@ -28,7 +28,7 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { } } - private var relayList: RelayList? + private var cachedRelays: CachedRelays? private var relayConstraints: RelayConstraints? private var expandedItems = [RelayLocation]() private var dataSource: DataSource? @@ -64,7 +64,10 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { tableView.dataSource = dataSource RelayCache.shared.addObserver(self) - loadData() + + updateDataSource(animateDifferences: false) { + self.updateTableViewSelection(scroll: true, animated: false) + } } override func viewDidLayoutSubviews() { @@ -99,12 +102,12 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { // MARK: - RelayCacheObserver - func relayCache(_ relayCache: RelayCache, didUpdateCachedRelayList cachedRelayList: CachedRelayList) { - self.didReceiveCachedRelays(cachedRelayList: cachedRelayList) { (result) in + func relayCache(_ relayCache: RelayCache, didUpdateCachedRelays cachedRelays: CachedRelays) { + self.didReceiveCachedRelays(cachedRelays) { (result) in DispatchQueue.main.async { switch result { - case .success(let (cachedRelayList, relayConstraints)): - self.didReceive(relayList: cachedRelayList.relayList, relayConstraints: relayConstraints) + case .success(let (cachedRelays, relayConstraints)): + self.didReceiveCachedRelays(cachedRelays, relayConstraints: relayConstraints) case .failure(let error): error.logChain() @@ -113,27 +116,31 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { } } - // MARK: - Relay list handling + // MARK: - Public - private func loadData() { + func prefetchData(completionHandler: @escaping () -> Void) { fetchRelays { (result) in DispatchQueue.main.async { switch result { - case .success(let (cachedRelayList, relayConstraints)): - self.didReceive(relayList: cachedRelayList.relayList, relayConstraints: relayConstraints) + case .success(let (cachedRelays, relayConstraints)): + self.didReceiveCachedRelays(cachedRelays, relayConstraints: relayConstraints) case .failure(let error): error.logChain() } + + completionHandler() } } } - private func fetchRelays(completionHandler: @escaping (Result<(CachedRelayList, RelayConstraints), Error>) -> Void) { + // MARK: - Relay list handling + + private func fetchRelays(completionHandler: @escaping (Result<(CachedRelays, RelayConstraints), Error>) -> Void) { RelayCache.shared.read { (result) in switch result { - case .success(let cachedRelayList): - self.didReceiveCachedRelays(cachedRelayList: cachedRelayList, completionHandler: completionHandler) + case .success(let cachedRelays): + self.didReceiveCachedRelays(cachedRelays, completionHandler: completionHandler) case .failure(let error): completionHandler(.failure(.loadRelayList(error))) @@ -141,18 +148,18 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { } } - private func didReceiveCachedRelays(cachedRelayList: CachedRelayList, completionHandler: @escaping (Result<(CachedRelayList, RelayConstraints), Error>) -> Void) { + private func didReceiveCachedRelays(_ cachedRelays: CachedRelays, completionHandler: @escaping (Result<(CachedRelays, RelayConstraints), Error>) -> Void) { TunnelManager.shared.getRelayConstraints { (result) in let result = result - .map { (cachedRelayList, $0) } + .map { (cachedRelays, $0) } .mapError { Error.getRelayConstraints($0) } completionHandler(result) } } - private func didReceive(relayList: RelayList, relayConstraints: RelayConstraints) { - self.relayList = relayList.sorted() + private func didReceiveCachedRelays(_ cachedRelays: CachedRelays, relayConstraints: RelayConstraints) { + self.cachedRelays = cachedRelays self.relayConstraints = relayConstraints let relayLocation = relayConstraints.location.value @@ -186,9 +193,11 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { } private func updateDataSource(animateDifferences: Bool, completion: (() -> Void)? = nil) { - let items = relayList?.intoRelayDataSourceItemList(using: { (item) -> Bool in - return expandedItems.contains(item.relayLocation) - }) ?? [] + let items = self.cachedRelays.map { (cachedRelays) -> [DataSourceItem] in + return cachedRelays.relays.makeDataSource { (item) -> Bool in + return expandedItems.contains(item.relayLocation) + } + } ?? [] var snapshot = DataSourceSnapshot() snapshot.appendSections([.locations]) @@ -245,62 +254,6 @@ class SelectLocationController: UITableViewController, RelayCacheObserver { } } -private extension RelayList { - - typealias EvaluatorFn = (DataSourceItem) -> Bool - - /// Turn `RelayList` into a flat list of `DataSourceItem`s. - /// - /// - Parameters evaluator: A closure that determines if the sub-tree should be rendered when it - /// returns `true`, or dropped when it returns `false` - func intoRelayDataSourceItemList(using evaluator: EvaluatorFn) -> [DataSourceItem] { - var items = [DataSourceItem]() - - for country in countries { - let wrappedCountry = DataSourceItem.Country( - countryCode: country.code, - name: country.name, - hasActiveRelays: country.cities.contains(where: { (city) -> Bool in - return city.relays.contains { (host) -> Bool in - return host.active - } - }) - ) - - let countryItem = DataSourceItem.country(wrappedCountry) - items.append(countryItem) - - if evaluator(countryItem) { - for city in country.cities { - let wrappedCity = DataSourceItem.City( - countryCode: country.code, - cityCode: city.code, - name: city.name, - hasActiveRelays: city.relays.contains(where: { $0.active }) - ) - - let cityItem = DataSourceItem.city(wrappedCity) - items.append(cityItem) - - if evaluator(cityItem) { - for host in city.relays { - let wrappedHost = DataSourceItem.Hostname( - countryCode: country.code, - cityCode: city.code, - hostname: host.hostname, - active: host.active) - items.append(.hostname(wrappedHost)) - } - } - } - } - } - - return items - } - -} - private extension RelayLocation { /// A list of `RelayLocation` items preceding the given one in the relay tree @@ -334,21 +287,19 @@ private typealias DataSourceSnapshot = DiffableDataSourceSnapshot<DataSourceSect private enum DataSourceItem: Hashable { struct Country { - let countryCode: String + let location: String let name: String let hasActiveRelays: Bool } struct City { - let countryCode: String - let cityCode: String + let location: String let name: String let hasActiveRelays: Bool } struct Hostname { - let countryCode: String - let cityCode: String + let location: String let hostname: String let active: Bool } @@ -360,11 +311,13 @@ private enum DataSourceItem: Hashable { var relayLocation: RelayLocation { switch self { case .country(let country): - return .country(country.countryCode) + return .country(country.location) case .city(let city): - return .city(city.countryCode, city.cityCode) + let split = city.location.split(separator: "-", maxSplits: 2).map(String.init) + return .city(split[0], split[1]) case .hostname(let host): - return .hostname(host.countryCode, host.cityCode, host.hostname) + let split = host.location.split(separator: "-", maxSplits: 2).map(String.init) + return .hostname(split[0], split[1], host.hostname) } } @@ -419,3 +372,104 @@ private enum DataSourceItem: Hashable { } } + +extension ServerRelaysResponse { + fileprivate static func lexicalSortComparator(_ a: String, _ b: String) -> Bool { + return a.localizedCaseInsensitiveCompare(b) == .orderedAscending + } + + fileprivate static func fileSortComparator(_ a: String, _ b: String) -> Bool { + return a.localizedStandardCompare(b) == .orderedAscending + } + + fileprivate func makeDataSource(evaluator: (DataSourceItem) -> Bool) -> [DataSourceItem] { + let relaysByCountry = Dictionary(grouping: wireguard.relays) { (relay) -> String in + return relay.location.split(separator: "-").first.flatMap(String.init)! + } + + var items = [DataSourceItem]() + + var countryItems = [DataSourceItem.Country]() + var cityItems = [String: [DataSourceItem.City]]() + var relayItems = [String: [DataSourceItem.Hostname]]() + + for (countryCode, relays) in relaysByCountry { + let relaysByCity = Dictionary(grouping: relays) { (relay) -> String in + return relay.location + } + + if let (cityCode, relays) = relaysByCity.first { + guard let location = locations[cityCode] else { + continue + } + + let country = DataSourceItem.Country( + location: countryCode, + name: location.country, + hasActiveRelays: relays.contains(where: { (serverRelay) -> Bool in + return serverRelay.active + })) + + countryItems.append(country) + if !evaluator(.country(country)) { + continue + } + } + + for (cityCode, relays) in relaysByCity { + guard let location = locations[cityCode] else { + os_log(.info, "Location is not found: %{public}s", cityCode) + continue + } + + let city = DataSourceItem.City( + location: cityCode, + name: location.city, + hasActiveRelays: relays.contains(where: { (serverRelay) -> Bool in + return serverRelay.active + })) + + if var cities = cityItems[countryCode] { + cities.append(city) + cityItems[countryCode] = cities + } else { + cityItems[countryCode] = [city] + } + + if !evaluator(.city(city)) { + continue + } + + relayItems[cityCode] = relays.map { (relay) -> DataSourceItem.Hostname in + return DataSourceItem.Hostname(location: relay.location, hostname: relay.hostname, active: relay.active) + } + } + } + + countryItems.sort { (a, b) -> Bool in + return Self.lexicalSortComparator(a.name, b.name) + } + + for country in countryItems { + items.append(.country(country)) + + if var cities = cityItems[country.location] { + cities.sort { (a, b) -> Bool in + return Self.lexicalSortComparator(a.name, b.name) + } + for city in cities { + items.append(.city(city)) + + if var relays = relayItems[city.location] { + relays.sort { (a, b) -> Bool in + return Self.fileSortComparator(a.hostname, b.hostname) + } + items.append(contentsOf: relays.map { DataSourceItem.hostname($0) }) + } + } + } + } + + return items + } +} diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 3481117f20..5b85acefa9 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -46,26 +46,15 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { DispatchQueue.main.async { - let completeRequest = { (response: AnyEncodable) in - switch PacketTunnelIpcHandler.encodeResponse(response: response) { - case .success(let data): - completionHandler?(data) - - case .failure: - completionHandler?(nil) - } - - } - let result = PacketTunnelIpcHandler.decodeRequest(messageData: messageData) switch result { case .success(let request): switch request { case .reloadTunnelSettings: - return completeRequest(AnyEncodable(true)) + Self.replyAppMessage(true, completionHandler: completionHandler) case .tunnelInformation: - return completeRequest(AnyEncodable(self.connectionInfo)) + Self.replyAppMessage(self.connectionInfo, completionHandler: completionHandler) } case .failure: @@ -74,6 +63,18 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } } + private static func replyAppMessage<T: Encodable>(_ response: T, completionHandler: ((Data?) -> Void)?) + { + switch PacketTunnelIpcHandler.encodeResponse(response: response) { + case .success(let data): + completionHandler?(data) + + case .failure(let error): + error.logChain() + completionHandler?(nil) + } + } + } #endif diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift index 65c86aee45..ec234a348f 100644 --- a/ios/MullvadVPN/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager.swift @@ -179,16 +179,16 @@ class TunnelManager { case obtainPersistentKeychainReference(TunnelSettingsManager.Error) /// A failure to push the public WireGuard key - case pushWireguardKey(MullvadRpc.Error) + case pushWireguardKey(RestError) /// A failure to replace the public WireGuard key - case replaceWireguardKey(MullvadRpc.Error) + case replaceWireguardKey(RestError) /// A failure to remove the public WireGuard key - case removeWireguardKey(MullvadRpc.Error) + case removeWireguardKey(RestError) /// A failure to verify the public WireGuard key - case verifyWireguardKey(MullvadRpc.Error) + case verifyWireguardKey(RestError) var errorDescription: String? { switch self { @@ -241,7 +241,7 @@ class TunnelManager { private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.TunnelManager") - private let rpc = MullvadRpc.withEphemeralURLSession() + private let rest = MullvadRest(session: URLSession(configuration: .ephemeral)) private var tunnelProvider: TunnelProviderManagerType? private var tunnelIpc: PacketTunnelIpc? @@ -569,33 +569,40 @@ class TunnelManager { } func verifyPublicKey(completionHandler: @escaping (Result<Bool, Error>) -> Void) { - let makeRequest = ResultOperation<MullvadRpc.Request<Bool>, Error> { - () -> Result<MullvadRpc.Request<Bool>, Error> in + let makePayloadOperation = ResultOperation<PublicKeyPayload<TokenPayload<EmptyPayload>>, Error> { + () -> Result<PublicKeyPayload<TokenPayload<EmptyPayload>>, Error> in guard let accountToken = self.accountToken else { return .failure(.missingAccount) } return Self.loadTunnelSettings(accountToken: accountToken) - .map { (keychainEntry) -> MullvadRpc.Request<Bool> in + .map { (keychainEntry) -> PublicKeyPayload<TokenPayload<EmptyPayload>> in let publicKey = keychainEntry.tunnelSettings.interface .privateKey .publicKey.rawRepresentation - return self.rpc.checkWireguardKey( - accountToken: keychainEntry.accountToken, - publicKey: publicKey + return PublicKeyPayload( + pubKey: publicKey, + payload: TokenPayload(token: keychainEntry.accountToken, payload: EmptyPayload()) ) } } - let sendRequest = rpc.checkWireguardKey() - .injectResult(from: makeRequest) + let getPubkeyOperation = self.rest.getWireguardKey() + .operation(payload: nil) + .injectResult(from: makePayloadOperation) - sendRequest.addDidFinishBlockObserver { (operation, result) in - completionHandler(result.mapError { Error.verifyWireguardKey($0) }) + getPubkeyOperation.addDidFinishBlockObserver { (operation, result) in + let result = result.map { (_) -> Bool in + return true + }.mapError { (restError) -> Error in + return .verifyWireguardKey(restError) + } + + completionHandler(result) } - operationQueue.addOperations([makeRequest, sendRequest], waitUntilFinished: false) + operationQueue.addOperations([makePayloadOperation, getPubkeyOperation], waitUntilFinished: false) } func regeneratePrivateKey(completionHandler: @escaping (Result<(), Error>) -> Void) { @@ -793,42 +800,48 @@ class TunnelManager { publicKey: WireguardPublicKey, completionHandler: @escaping (Result<(), Error>) -> Void) { - let request = self.rpc.pushWireguardKey( - accountToken: accountToken, - publicKey: publicKey.rawRepresentation - ) - - request.start { (rpcResult) in - self.dispatchQueue.async { - let updateResult = rpcResult - .mapError({ (rpcError) -> Error in - return Error.pushWireguardKey(rpcError) - }) - .flatMap { (associatedAddresses) -> Result<(), Error> in - return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - }.map { _ in () } - } + let payload = TokenPayload(token: accountToken, payload: PushWireguardKeyRequest(pubkey: publicKey.rawRepresentation)) + let operation = rest.pushWireguardKey().operation(payload: payload) - completionHandler(updateResult) + operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in + let updateResult = result + .mapError({ (restError) -> Error in + return .pushWireguardKey(restError) + }) + .flatMap { (associatedAddresses) -> Result<(), Error> in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + }.map { _ in () } } + + completionHandler(updateResult) } + + operationQueue.addOperation(operation) } private func removeWireguardKeyFromServer(accountToken: String, publicKey: Data, completionHandler: @escaping (Result<Bool, Error>) -> Void) { - let request = self.rpc.removeWireguardKey( - accountToken: accountToken, - publicKey: publicKey - ) + let payload = PublicKeyPayload(pubKey: publicKey, payload: TokenPayload(token: accountToken, payload: EmptyPayload())) + let operation = rest.deleteWireguardKey().operation(payload: payload) - request.start(completionHandler: { (result) in - self.dispatchQueue.async { - completionHandler(result.mapError { Error.removeWireguardKey($0) }) + operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in + let result = result.map({ () -> Bool in + return true + }).flatMapError { (restError) -> Result<Bool, Error> in + if case .server(.pubKeyNotFound) = restError { + return .success(false) + } else { + return .failure(.removeWireguardKey(restError)) + } } - }) + + completionHandler(result) + } + + operationQueue.addOperation(operation) } private func replaceWireguardKeyAndUpdateSettings( @@ -837,31 +850,35 @@ class TunnelManager { newPrivateKey: WireguardPrivateKey, completionHandler: @escaping (Result<(), Error>) -> Void) { - let request = self.rpc.replaceWireguardKey( - accountToken: accountToken, - oldPublicKey: oldPublicKey.rawRepresentation, - newPublicKey: newPrivateKey.publicKey.rawRepresentation + let payload = TokenPayload( + token: accountToken, + payload: ReplaceWireguardKeyRequest( + old: oldPublicKey.rawRepresentation, + new: newPrivateKey.publicKey.rawRepresentation + ) ) - request.start { (rpcResult) in - self.dispatchQueue.async { - let updateResult = rpcResult - .mapError({ (rpcError) -> Error in - return Error.replaceWireguardKey(rpcError) - }) - .flatMap { (associatedAddresses) -> Result<(), Error> in - return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in - tunnelSettings.interface.privateKey = newPrivateKey - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - }.map { _ in () } - } + let operation = rest.replaceWireguardKey().operation(payload: payload) - completionHandler(updateResult) + operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in + let updateResult = result + .mapError({ (restError) -> Error in + return .replaceWireguardKey(restError) + }) + .flatMap { (associatedAddresses) -> Result<(), Error> in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.privateKey = newPrivateKey + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + }.map { _ in () } } + + completionHandler(updateResult) } + + operationQueue.addOperation(operation) } /// Initiates the `tunnelState` update diff --git a/ios/MullvadVPN/UserInterfaceInteractionRestriction.swift b/ios/MullvadVPN/UserInterfaceInteractionRestriction.swift index 865dc29366..2a07132c09 100644 --- a/ios/MullvadVPN/UserInterfaceInteractionRestriction.swift +++ b/ios/MullvadVPN/UserInterfaceInteractionRestriction.swift @@ -20,7 +20,7 @@ protocol UserInterfaceInteractionRestrictionProtocol { /// A counter based user interface interaction restriction implementation class UserInterfaceInteractionRestriction: UserInterfaceInteractionRestrictionProtocol { - typealias Action = (_ disableUserInteraction: Bool, _ animated: Bool) -> Void + typealias Action = (_ enableUserInteraction: Bool, _ animated: Bool) -> Void private let action: Action private var counter: UInt = 0 diff --git a/ios/MullvadVPN/ViewControllerIdentifier.swift b/ios/MullvadVPN/ViewControllerIdentifier.swift index dd4e8cbcff..d2eaad74bb 100644 --- a/ios/MullvadVPN/ViewControllerIdentifier.swift +++ b/ios/MullvadVPN/ViewControllerIdentifier.swift @@ -13,4 +13,5 @@ enum ViewControllerIdentifier: String { case login = "Login" case main = "Main" case settings = "Settings" + case selectLocation = "SelectLocation" } diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift index 983674edca..fb17f7eaf4 100644 --- a/ios/MullvadVPNTests/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/RelaySelectorTests.swift @@ -12,7 +12,7 @@ import Network class RelaySelectorTests: XCTestCase { func testCountryConstraint() { - let relaySelector = RelaySelector(relayList: sampleRelayList) + let relaySelector = RelaySelector(relays: sampleRelays) let constraints = RelayConstraints(location: .only(.country("es"))) let result = relaySelector.evaluate(with: constraints) @@ -21,7 +21,7 @@ class RelaySelectorTests: XCTestCase { } func testCityConstraint() { - let relaySelector = RelaySelector(relayList: sampleRelayList) + let relaySelector = RelaySelector(relays: sampleRelays) let constraints = RelayConstraints(location: .only(.city("se", "got"))) let result = relaySelector.evaluate(with: constraints) @@ -30,7 +30,7 @@ class RelaySelectorTests: XCTestCase { } func testHostnameConstraint() { - let relaySelector = RelaySelector(relayList: sampleRelayList) + let relaySelector = RelaySelector(relays: sampleRelays) let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard"))) let result = relaySelector.evaluate(with: constraints) @@ -40,83 +40,79 @@ class RelaySelectorTests: XCTestCase { } -private let sampleRelayList = RelayList(countries: [ - .init(name: "Spain", code: "es", cities: [ - .init(name: "Madrid", - code: "mad", - latitude: 40.408566, - longitude: -3.69222, - relays: [ - .init( - hostname: "es1-wireguard", - ipv4AddrIn: .loopback, - includeInCountry: true, - active: true, - weight: 500, - tunnels: .init(wireguard: [ - .init( - ipv4Gateway: .loopback, - ipv6Gateway: .loopback, - publicKey: .init(), - portRanges: [(7000...7100)] - ) - ])) - ]) - ]), - .init(name: "Sweden", code: "se", cities: [ - .init(name: "Gothenburg", - code: "got", - latitude: 57.70887, - longitude: 11.97456, - relays: [ - .init( - hostname: "se10-wireguard", - ipv4AddrIn: .loopback, - includeInCountry: true, - active: true, - weight: 1000, - tunnels: .init(wireguard: [ - .init( - ipv4Gateway: .loopback, - ipv6Gateway: .loopback, - publicKey: .init(), - portRanges: [(7000...7100)] - ) - ])) - ]), - .init(name: "Stockholm", - code: "sto", - latitude: 59.3289, - longitude: 18.0649, - relays: [ - .init( - hostname: "se2-wireguard", - ipv4AddrIn: .loopback, - includeInCountry: true, - active: true, - weight: 50, - tunnels: .init(wireguard: [ - .init( - ipv4Gateway: .loopback, - ipv6Gateway: .loopback, - publicKey: .init(), - portRanges: [(8000...8100)] - ) - ])), - .init( - hostname: "se6-wireguard", - ipv4AddrIn: IPv4Address.loopback, - includeInCountry: true, - active: true, - weight: 100, - tunnels: .init(wireguard: [ - .init( - ipv4Gateway: .loopback, - ipv6Gateway: .loopback, - publicKey: .init(), - portRanges: [(8000...9000)] - ) - ])) - ]) +private let sampleRelays = ServerRelaysResponse( + locations: [ + "es-mad": ServerLocation( + country: "Spain", + city: "Madrid", + latitude: 40.408566, + longitude: -3.69222 + ), + "se-got": ServerLocation( + country: "Sweden", + city: "Gothenburg", + latitude: 57.70887, + longitude: 11.97456 + ), + "se-sto": ServerLocation( + country: "Sweden", + city: "Stockholm", + latitude: 59.3289, + longitude: 18.0649 + ) + ], + wireguard: ServerWireguardTunnels( + ipv4Gateway: .loopback, + ipv6Gateway: .loopback, + portRanges: [53...53], + relays: [ + ServerRelay( + hostname: "es1-wireguard", + active: true, + owned: true, + location: "es-mad", + provider: "", + weight: 500, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: Data(), + includeInCountry: true + ), + ServerRelay( + hostname: "se10-wireguard", + active: true, + owned: true, + location: "se-got", + provider: "", + weight: 1000, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: Data(), + includeInCountry: true + ), + ServerRelay( + hostname: "se2-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 50, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: Data(), + includeInCountry: true + ), + ServerRelay( + hostname: "se6-wireguard", + active: true, + owned: true, + location: "se-sto", + provider: "", + weight: 100, + ipv4AddrIn: .loopback, + ipv6AddrIn: .loopback, + publicKey: Data(), + includeInCountry: true + ) ]) -]) +) diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index d9e40ed496..0e71c6aa14 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -180,22 +180,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { dispatchQueue.async { - let finishWithResult = { (result: Result<AnyEncodable, PacketTunnelProviderError>) in - let result = result.flatMap { (response) -> Result<Data, PacketTunnelProviderError> in - return PacketTunnelIpcHandler.encodeResponse(response: response) - .mapError { PacketTunnelProviderError.ipcHandler($0) } - } - - switch result { - case .success(let data): - completionHandler?(data) - - case .failure(let error): - error.logChain(log: tunnelProviderLog) - completionHandler?(nil) - } - } - let decodeResult = PacketTunnelIpcHandler.decodeRequest(messageData: messageData) .mapError { PacketTunnelProviderError.ipcHandler($0) } @@ -204,15 +188,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider { switch request { case .reloadTunnelSettings: self.reloadTunnelSettings { (result) in - finishWithResult(result.map { AnyEncodable(true) }) + Self.replyAppMessage(result.map { true }, completionHandler: completionHandler) } case .tunnelInformation: - finishWithResult(.success(AnyEncodable(self.connectionInfo))) + Self.replyAppMessage(.success(self.connectionInfo), completionHandler: completionHandler) } case .failure(let error): - finishWithResult(.failure(error)) + Self.replyAppMessage(Result<String, PacketTunnelProviderError>.failure(error), completionHandler: completionHandler) } } } @@ -353,6 +337,24 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } + private static func replyAppMessage<T: Encodable>( + _ result: Result<T, PacketTunnelProviderError>, + completionHandler: ((Data?) -> Void)?) { + let result = result.flatMap { (response) -> Result<Data, PacketTunnelProviderError> in + return PacketTunnelIpcHandler.encodeResponse(response: response) + .mapError { PacketTunnelProviderError.ipcHandler($0) } + } + + switch result { + case .success(let data): + completionHandler?(data) + + case .failure(let error): + error.logChain(log: tunnelProviderLog) + completionHandler?(nil) + } + } + private func setTunnelConnectionInfo(selectorResult: RelaySelectorResult) { self.connectionInfo = TunnelConnectionInfo( ipv4Relay: selectorResult.endpoint.ipv4Relay, @@ -417,10 +419,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } - operation.addDidFinishBlockObserver { (operation, result) in - self.dispatchQueue.async { - completionHandler(result) - } + operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in + completionHandler(result) } exclusivityController.addOperation(operation, categories: [.exclusive]) @@ -459,7 +459,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { RelayCache.shared.read { (result) in switch result { case .success(let cachedRelayList): - let relaySelector = RelaySelector(relayList: cachedRelayList.relayList) + let relaySelector = RelaySelector(relays: cachedRelayList.relays) if let selectorResult = relaySelector.evaluate(with: relayConstraints) { completionHandler(.success(selectorResult)) diff --git a/ios/update-relays.sh b/ios/update-relays.sh index 3935abe583..efb0a8a65c 100755 --- a/ios/update-relays.sh +++ b/ios/update-relays.sh @@ -14,7 +14,5 @@ fi if [ ! -f "$RELAYS_FILE" ]; then echo "Download relays file" - curl https://api.mullvad.net/rpc/ \ - -d '{"jsonrpc": "2.0", "id": "0", "method": "relay_list_v3"}' \ - --header "Content-Type: application/json" | jq -c .result > "$RELAYS_FILE" + curl https://api.mullvad.net/app/v1/relays -s -o "$RELAYS_FILE" fi |
