summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-07-22 18:47:16 +0300
committerAndrej Mihajlov <and@mullvad.net>2020-07-22 18:47:16 +0300
commitd6871530f4875ac9f4f644589cc0a69b328911f9 (patch)
treee77e58bdc99883ee443ea6ccd14fdf629b1dca89
parentf10dc9875b3d8e5d35448af2a9c58b9db07b8829 (diff)
parentb4d6cd798392957ddb50f111d4f5231b89f3defc (diff)
downloadmullvadvpn-d6871530f4875ac9f4f644589cc0a69b328911f9.tar.xz
mullvadvpn-d6871530f4875ac9f4f644589cc0a69b328911f9.zip
Merge branch 'rest'
-rw-r--r--ios/BuildInstructions.md9
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj54
-rw-r--r--ios/MullvadVPN/Account.swift88
-rw-r--r--ios/MullvadVPN/AccountViewController.swift10
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager.swift36
-rw-r--r--ios/MullvadVPN/AutomaticKeyRotationManager.swift62
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard50
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift26
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift39
-rw-r--r--ios/MullvadVPN/JsonRpc.swift87
-rw-r--r--ios/MullvadVPN/LoginViewController.swift4
-rw-r--r--ios/MullvadVPN/MullvadRest.swift549
-rw-r--r--ios/MullvadVPN/MullvadRpc.swift347
-rw-r--r--ios/MullvadVPN/Operations/OperationBlockObserver.swift29
-rw-r--r--ios/MullvadVPN/Operations/OutputOperation.swift4
-rw-r--r--ios/MullvadVPN/RelayCache.swift129
-rw-r--r--ios/MullvadVPN/RelayList.swift89
-rw-r--r--ios/MullvadVPN/RelaySelector.swift84
-rw-r--r--ios/MullvadVPN/SegueIdentifier.swift1
-rw-r--r--ios/MullvadVPN/SelectLocationController.swift222
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift27
-rw-r--r--ios/MullvadVPN/TunnelManager.swift145
-rw-r--r--ios/MullvadVPN/UserInterfaceInteractionRestriction.swift2
-rw-r--r--ios/MullvadVPN/ViewControllerIdentifier.swift1
-rw-r--r--ios/MullvadVPNTests/RelaySelectorTests.swift160
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift48
-rwxr-xr-xios/update-relays.sh4
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