summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-04-29 10:30:55 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-04-29 10:30:55 +0200
commit5937149e6a2c231dba0efaf37c5b0f9eb37224fb (patch)
treeadfd5f6a753de4e6e7c197faa90402804e9a0006
parent661bc50d67ef36e3a7f826cda5fd82568ba95a84 (diff)
parentd652c90c216752ddff6327c47cec99242f12b515 (diff)
downloadmullvadvpn-5937149e6a2c231dba0efaf37c5b0f9eb37224fb.tar.xz
mullvadvpn-5937149e6a2c231dba0efaf37c5b0f9eb37224fb.zip
Merge branch 'device-api'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj72
-rw-r--r--ios/MullvadVPN/Account.swift8
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheStore.swift150
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheTracker.swift12
-rw-r--r--ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift21
-rw-r--r--ios/MullvadVPN/AppDelegate.swift2
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift6
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift120
-rw-r--r--ios/MullvadVPN/ApplicationConfiguration.swift8
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift10
-rw-r--r--ios/MullvadVPN/Operations/AsyncBlockOperation.swift8
-rw-r--r--ios/MullvadVPN/Operations/ResultOperation.swift140
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTAPIProxy.swift498
-rw-r--r--ios/MullvadVPN/REST/RESTAccessTokenManager.swift74
-rw-r--r--ios/MullvadVPN/REST/RESTAccountsProxy.swift82
-rw-r--r--ios/MullvadVPN/REST/RESTAuthenticationProxy.swift76
-rw-r--r--ios/MullvadVPN/REST/RESTAuthorization.swift16
-rw-r--r--ios/MullvadVPN/REST/RESTClient.swift524
-rw-r--r--ios/MullvadVPN/REST/RESTCoding.swift33
-rw-r--r--ios/MullvadVPN/REST/RESTDevicesProxy.swift96
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift27
-rw-r--r--ios/MullvadVPN/REST/RESTNetworkOperation.swift280
-rw-r--r--ios/MullvadVPN/REST/RESTProxy.swift91
-rw-r--r--ios/MullvadVPN/REST/RESTProxyFactory.swift51
-rw-r--r--ios/MullvadVPN/REST/RESTRequestFactory.swift118
-rw-r--r--ios/MullvadVPN/REST/RESTRequestHandler.swift67
-rw-r--r--ios/MullvadVPN/REST/RESTResponseDecoder.swift46
-rw-r--r--ios/MullvadVPN/REST/RESTResponseHandler.swift44
-rw-r--r--ios/MullvadVPN/REST/RESTTaskIdentifier.swift25
-rw-r--r--ios/MullvadVPN/REST/RESTURLSession.swift30
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift10
-rw-r--r--ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift19
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift14
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift6
-rw-r--r--ios/MullvadVPN/en.lproj/REST.strings (renamed from ios/MullvadVPN/en.lproj/RESTClient.strings)3
37 files changed, 1953 insertions, 856 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index a030a5546b..21cc7f746b 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -103,6 +103,12 @@
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
+ 58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */; };
+ 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */; };
+ 58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */; };
+ 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F78280B037400013055 /* RESTAccessTokenManager.swift */; };
+ 58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */; };
+ 58554F7D280D6FE000013055 /* RESTURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F7C280D6FE000013055 /* RESTURLSession.swift */; };
58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; };
58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; };
5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5856D13627450A8A00DFD627 /* UIImage+TintColor.swift */; };
@@ -180,6 +186,9 @@
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */; };
+ 588BCF24280FE43D009ADCEC /* RESTDevicesProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */; };
+ 588BCF26280FE79A009ADCEC /* RESTProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588BCF25280FE79A009ADCEC /* RESTProxy.swift */; };
+ 588BCF282816D664009ADCEC /* RESTResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588BCF272816D664009ADCEC /* RESTResponseHandler.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 */; };
58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; };
@@ -212,6 +221,8 @@
58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; };
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */; };
58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */; };
+ 58B5A895280AACC4009FDE99 /* RESTRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */; };
+ 58B5A899280AB0D7009FDE99 /* RESTAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B5A898280AB0D7009FDE99 /* RESTAuthorization.swift */; };
58B67B482602079E008EF58E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; };
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; };
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; };
@@ -224,7 +235,7 @@
58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; };
58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; };
- 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */; };
+ 58CB0EE024B86751001EF0D8 /* RESTAPIProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* RESTAPIProxy.swift */; };
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* ConnectViewController.swift */; };
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA01122424D11004F3011 /* SettingsViewController.swift */; };
@@ -261,7 +272,7 @@
58F558E32695D1D800F630D0 /* Preferences.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558E12695D1D800F630D0 /* Preferences.strings */; };
58F558E62695D1F200F630D0 /* ProblemReport.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558E42695D1F200F630D0 /* ProblemReport.strings */; };
58F558E92695D20F00F630D0 /* SelectLocation.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558E72695D20F00F630D0 /* SelectLocation.strings */; };
- 58F558EC2695D26A00F630D0 /* RESTClient.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558EA2695D26A00F630D0 /* RESTClient.strings */; };
+ 58F558EC2695D26A00F630D0 /* REST.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558EA2695D26A00F630D0 /* REST.strings */; };
58F558EF2695D50D00F630D0 /* ProblemReportReview.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558ED2695D50D00F630D0 /* ProblemReportReview.strings */; };
58F558F92696EB1C00F630D0 /* StoreKitErrors.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558F32696EB1C00F630D0 /* StoreKitErrors.strings */; };
58F558FA2696EB1C00F630D0 /* TunnelManager.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558F52696EB1C00F630D0 /* TunnelManager.strings */; };
@@ -278,6 +289,8 @@
58F840B22464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; };
58F840B32464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; };
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; };
+ 58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */; };
+ 58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */; };
58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
@@ -410,6 +423,12 @@
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDNSTextCell.swift; sourceTree = "<group>"; };
584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Markdown.swift"; sourceTree = "<group>"; };
5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddressRange+Codable.swift"; sourceTree = "<group>"; };
+ 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAuthenticationProxy.swift; sourceTree = "<group>"; };
+ 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTResponseDecoder.swift; sourceTree = "<group>"; };
+ 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTaskIdentifier.swift; sourceTree = "<group>"; };
+ 58554F78280B037400013055 /* RESTAccessTokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccessTokenManager.swift; sourceTree = "<group>"; };
+ 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccountsProxy.swift; sourceTree = "<group>"; };
+ 58554F7C280D6FE000013055 /* RESTURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTURLSession.swift; sourceTree = "<group>"; };
58561C98239A5D1500BD6B5E /* IPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPEndpoint.swift; sourceTree = "<group>"; };
5856D13627450A8A00DFD627 /* UIImage+TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+TintColor.swift"; sourceTree = "<group>"; };
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; };
@@ -463,6 +482,9 @@
588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTunnelSettingsOperation.swift; sourceTree = "<group>"; };
5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; };
5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; };
+ 588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
+ 588BCF25280FE79A009ADCEC /* RESTProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTProxy.swift; sourceTree = "<group>"; };
+ 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTResponseHandler.swift; sourceTree = "<group>"; };
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEProviderStopReason+Debug.swift"; sourceTree = "<group>"; };
58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectSplitButton.swift; sourceTree = "<group>"; };
5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; };
@@ -485,6 +507,8 @@
58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; };
58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectMainContentView.swift; sourceTree = "<group>"; };
+ 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestFactory.swift; sourceTree = "<group>"; };
+ 58B5A898280AB0D7009FDE99 /* RESTAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAuthorization.swift; sourceTree = "<group>"; };
58B93A1226C3F13600A55733 /* TunnelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelState.swift; sourceTree = "<group>"; };
58B993B02608A34500BA7811 /* LoginContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContentView.swift; sourceTree = "<group>"; };
58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; };
@@ -494,7 +518,7 @@
58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; };
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; };
58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyWithMetadata.swift; sourceTree = "<group>"; };
- 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTClient.swift; sourceTree = "<group>"; };
+ 58CB0EDF24B86751001EF0D8 /* RESTAPIProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAPIProxy.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>"; };
@@ -538,7 +562,7 @@
58F558E22695D1D800F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Preferences.strings; sourceTree = "<group>"; };
58F558E52695D1F200F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ProblemReport.strings; sourceTree = "<group>"; };
58F558E82695D20F00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SelectLocation.strings; sourceTree = "<group>"; };
- 58F558EB2695D26A00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/RESTClient.strings; sourceTree = "<group>"; };
+ 58F558EB2695D26A00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/REST.strings; sourceTree = "<group>"; };
58F558EE2695D50D00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ProblemReportReview.strings; sourceTree = "<group>"; };
58F558F42696EB1C00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/StoreKitErrors.strings; sourceTree = "<group>"; };
58F558F62696EB1C00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TunnelManager.strings; sourceTree = "<group>"; };
@@ -554,6 +578,8 @@
58F7D26427EB50A300E4D821 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = "<group>"; };
58F840B12464491D0044E708 /* ChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedError.swift; sourceTree = "<group>"; };
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = "<group>"; };
+ 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTProxyFactory.swift; sourceTree = "<group>"; };
+ 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestHandler.swift; sourceTree = "<group>"; };
58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAttributes.swift; sourceTree = "<group>"; };
58FAEDF6245088E100CB0F5B /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainMatchLimit.swift; sourceTree = "<group>"; };
@@ -695,7 +721,7 @@
587B7543266922BF00DEF7E9 /* Localizable.strings */,
58F558DE2695BD3E00F630D0 /* Login.strings */,
58F559052697002000F630D0 /* Main.strings */,
- 58F558EA2695D26A00F630D0 /* RESTClient.strings */,
+ 58F558EA2695D26A00F630D0 /* REST.strings */,
58F558E12695D1D800F630D0 /* Preferences.strings */,
58F558E42695D1F200F630D0 /* ProblemReport.strings */,
58F558ED2695D50D00F630D0 /* ProblemReportReview.strings */,
@@ -740,11 +766,24 @@
children = (
5820674F26E6514100655B05 /* HTTP.swift */,
5820674D26E6510200655B05 /* REST.swift */,
- 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */,
+ 58554F78280B037400013055 /* RESTAccessTokenManager.swift */,
+ 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */,
+ 58CB0EDF24B86751001EF0D8 /* RESTAPIProxy.swift */,
+ 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */,
+ 58B5A898280AB0D7009FDE99 /* RESTAuthorization.swift */,
585DA88926B027A300B8C587 /* RESTCoding.swift */,
+ 588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */,
585DA88626B0277200B8C587 /* RESTError.swift */,
58095C522760EEC700890776 /* RESTNetworkOperation.swift */,
+ 588BCF25280FE79A009ADCEC /* RESTProxy.swift */,
+ 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */,
+ 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */,
+ 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */,
+ 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */,
+ 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */,
58095C582762155700890776 /* RESTRetryStrategy.swift */,
+ 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */,
+ 58554F7C280D6FE000013055 /* RESTURLSession.swift */,
585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */,
584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */,
);
@@ -1214,7 +1253,7 @@
58F5590E2697002100F630D0 /* Main.strings in Resources */,
58F558FE2696F09100F630D0 /* KeyboardNavigation.strings in Resources */,
581FC4FA2695ACE100AA97BA /* Account.strings in Resources */,
- 58F558EC2695D26A00F630D0 /* RESTClient.strings in Resources */,
+ 58F558EC2695D26A00F630D0 /* REST.strings in Resources */,
582CFEEA269463B80072883A /* Settings.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1312,6 +1351,8 @@
5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
+ 588BCF282816D664009ADCEC /* RESTResponseHandler.swift in Sources */,
+ 58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */,
@@ -1335,10 +1376,11 @@
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */,
5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */,
- 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */,
+ 58CB0EE024B86751001EF0D8 /* RESTAPIProxy.swift in Sources */,
58095C532760EEC700890776 /* RESTNetworkOperation.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */,
+ 58554F7D280D6FE000013055 /* RESTURLSession.swift in Sources */,
5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */,
5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
@@ -1357,6 +1399,7 @@
5806767927048E8800C858CB /* Keychain.swift in Sources */,
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
+ 58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */,
58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */,
5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
@@ -1364,11 +1407,13 @@
5820674E26E6510200655B05 /* REST.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
+ 58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */,
5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */,
58095C4B2760B4F200890776 /* AddressCacheStoreError.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */,
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
+ 58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */,
581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */,
585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */,
584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */,
@@ -1393,6 +1438,7 @@
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
585DA89326B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
+ 588BCF26280FE79A009ADCEC /* RESTProxy.swift in Sources */,
5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */,
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
@@ -1423,12 +1469,14 @@
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */,
58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
+ 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */,
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */,
+ 58B5A895280AACC4009FDE99 /* RESTRequestFactory.swift in Sources */,
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */,
5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */,
58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */,
@@ -1444,14 +1492,18 @@
581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */,
+ 58B5A899280AB0D7009FDE99 /* RESTAuthorization.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */,
58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
+ 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */,
+ 58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */,
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58F840B22464491D0044E708 /* ChainedError.swift in Sources */,
58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
+ 588BCF24280FE43D009ADCEC /* RESTDevicesProxy.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */,
585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */,
@@ -1619,12 +1671,12 @@
name = SelectLocation.strings;
sourceTree = "<group>";
};
- 58F558EA2695D26A00F630D0 /* RESTClient.strings */ = {
+ 58F558EA2695D26A00F630D0 /* REST.strings */ = {
isa = PBXVariantGroup;
children = (
58F558EB2695D26A00F630D0 /* en */,
);
- name = RESTClient.strings;
+ name = REST.strings;
sourceTree = "<group>";
};
58F558ED2695D50D00F630D0 /* ProblemReportReview.strings */ = {
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 2ad08b0e49..79558024e7 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -60,6 +60,8 @@ class Account {
return operationQueue
}()
+ private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+
/// Returns true if user agreed to terms of service, otherwise false
var isAgreedToTermsOfService: Bool {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue)
@@ -100,7 +102,7 @@ class Account {
func loginWithNewAccount(completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
let operation = AsyncBlockOperation { operation in
- _ = REST.Client.shared.createAccount(retryStrategy: .noRetry) { result in
+ _ = self.apiProxy.createAccount(retryStrategy: .noRetry) { result in
switch result {
case .success(let response):
self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in
@@ -135,7 +137,7 @@ class Account {
/// application preferences.
func login(accountToken: String, completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
let operation = AsyncBlockOperation { operation in
- _ = REST.Client.shared.getAccountExpiry(token: accountToken, retryStrategy: .default) { result in
+ _ = self.apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in
switch result {
case .success(let response):
self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in
@@ -206,7 +208,7 @@ class Account {
return
}
- _ = REST.Client.shared.getAccountExpiry(token: token, retryStrategy: .default) { completion in
+ _ = self.apiProxy.getAccountExpiry(accountNumber: token, retryStrategy: .default) { completion in
switch completion {
case .success(let response):
if self.expiry != response.expires {
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift b/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
index 37bf2f2cd7..745e83dc9d 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
@@ -76,15 +76,84 @@ extension AddressCache {
/// The location of pre-bundled address cache file.
private let prebundledCacheFileURL: URL
- /// Queue used for synchronizing access to instance members.
- private let stateQueue = DispatchQueue(label: "AddressCacheStoreQueue")
+ /// Lock used for synchronizing access to instance members.
+ private let nslock = NSLock()
/// Designated initializer
init(cacheFileURL: URL, prebundledCacheFileURL: URL) {
self.cacheFileURL = cacheFileURL
self.prebundledCacheFileURL = prebundledCacheFileURL
- self.cachedAddresses = Self.defaultCachedAddresses
+ cachedAddresses = Self.defaultCachedAddresses
+ initializeStore()
+ }
+
+ func getCurrentEndpoint() -> AnyIPEndpoint {
+ nslock.lock()
+ defer { nslock.unlock() }
+ return cachedAddresses.endpoints.first!
+ }
+
+ func selectNextEndpoint(_ failedEndpoint: AnyIPEndpoint) -> AnyIPEndpoint {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ var currentEndpoint = cachedAddresses.endpoints.first!
+
+ if failedEndpoint == currentEndpoint {
+ cachedAddresses.endpoints.removeFirst()
+ cachedAddresses.endpoints.append(failedEndpoint)
+
+ currentEndpoint = cachedAddresses.endpoints.first!
+
+ logger.debug("Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)")
+
+ if case .failure(let error) = writeToDisk() {
+ logger.error(chainedError: error, message: "Failed to write address cache after selecting next endpoint.")
+ }
+ }
+
+ return currentEndpoint
+ }
+
+ func setEndpoints(_ endpoints: [AnyIPEndpoint]) -> Result<Void, AddressCache.StoreError> {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard !endpoints.isEmpty else {
+ return .failure(.emptyAddressList)
+ }
+
+ if Set(cachedAddresses.endpoints) == Set(endpoints) {
+ cachedAddresses.updatedAt = Date()
+ } else {
+ // Shuffle new endpoints
+ var newEndpoints = endpoints.shuffled()
+
+ // Move current endpoint to the top of the list
+ let currentEndpoint = cachedAddresses.endpoints.first!
+ if let index = newEndpoints.firstIndex(of: currentEndpoint) {
+ newEndpoints.remove(at: index)
+ newEndpoints.insert(currentEndpoint, at: 0)
+ }
+
+ cachedAddresses = CachedAddresses(
+ updatedAt: Date(),
+ endpoints: newEndpoints
+ )
+ }
+
+ return writeToDisk()
+ }
+
+ func getLastUpdateDate() -> Date {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return cachedAddresses.updatedAt
+ }
+
+ private func initializeStore() {
switch readFromCacheLocationWithFallback() {
case .success(let readResult):
if readResult.cachedAddresses.endpoints.isEmpty {
@@ -105,7 +174,7 @@ extension AddressCache {
logger.debug("Persist address list read from bundle.")
- if case .failure(let error) = self.writeToDisk() {
+ if case .failure(let error) = writeToDisk() {
logger.error(chainedError: error, message: "Failed to persist address cache after reading it from bundle.")
}
}
@@ -122,79 +191,6 @@ extension AddressCache {
}
}
- func getCurrentEndpoint(_ completionHandler: @escaping (AnyIPEndpoint) -> Void) {
- stateQueue.async {
- let currentEndpoint = self.cachedAddresses.endpoints.first!
-
- completionHandler(currentEndpoint)
- }
- }
-
- func selectNextEndpoint(_ failedEndpoint: AnyIPEndpoint, completionHandler: @escaping (AnyIPEndpoint) -> Void) {
- stateQueue.async {
- var currentEndpoint = self.cachedAddresses.endpoints.first!
-
- if failedEndpoint == currentEndpoint {
- self.cachedAddresses.endpoints.removeFirst()
- self.cachedAddresses.endpoints.append(failedEndpoint)
-
- currentEndpoint = self.cachedAddresses.endpoints.first!
-
- self.logger.debug("Failed to communicate using \(failedEndpoint). Next endpoint: \(currentEndpoint)")
-
- if case .failure(let error) = self.writeToDisk() {
- self.logger.error(chainedError: error, message: "Failed to write address cache after selecting next endpoint.")
- }
- }
-
- completionHandler(currentEndpoint)
- }
- }
-
- func setEndpoints(_ endpoints: [AnyIPEndpoint], completionHandler: @escaping (AddressCache.StoreError?) -> Void) {
- stateQueue.async {
- guard !endpoints.isEmpty else {
- completionHandler(.emptyAddressList)
- return
- }
-
- if Set(self.cachedAddresses.endpoints) == Set(endpoints) {
- self.cachedAddresses.updatedAt = Date()
- } else {
- // Shuffle new endpoints
- var newEndpoints = endpoints.shuffled()
-
- // Move current endpoint to the top of the list
- let currentEndpoint = self.cachedAddresses.endpoints.first!
- if let index = newEndpoints.firstIndex(of: currentEndpoint) {
- newEndpoints.remove(at: index)
- newEndpoints.insert(currentEndpoint, at: 0)
- }
-
- self.cachedAddresses = CachedAddresses(
- updatedAt: Date(),
- endpoints: newEndpoints
- )
- }
-
- let writeResult = self.writeToDisk()
-
- completionHandler(writeResult.error)
- }
- }
-
- func getLastUpdateDate(_ completionHandler: @escaping (Date) -> Void) {
- stateQueue.async {
- completionHandler(self.cachedAddresses.updatedAt)
- }
- }
-
- func getLastUpdateDateAndWait() -> Date {
- return stateQueue.sync {
- return self.cachedAddresses.updatedAt
- }
- }
-
private func readFromCacheLocationWithFallback() -> Result<ReadResult, AddressCache.StoreError> {
return readFromCacheLocation()
.map { addresses in
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
index 390fc31b4c..29d83f258e 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
@@ -21,8 +21,8 @@ extension AddressCache {
/// Logger.
private let logger = Logger(label: "AddressCache.Tracker")
- /// REST client
- private let restClient: REST.Client
+ /// REST API proxy.
+ private let apiProxy: REST.APIProxy
/// Store.
private let store: AddressCache.Store
@@ -47,8 +47,8 @@ extension AddressCache {
private let stateQueue = DispatchQueue(label: "AddressCache.Tracker.stateQueue")
/// Designated initializer
- init(restClient: REST.Client, store: AddressCache.Store) {
- self.restClient = restClient
+ init(apiProxy: REST.APIProxy, store: AddressCache.Store) {
+ self.apiProxy = apiProxy
self.store = store
}
@@ -86,7 +86,7 @@ extension AddressCache {
func updateEndpoints(completionHandler: ((_ completion: OperationCompletion<CacheUpdateResult, Error>) -> Void)? = nil) -> Cancellable {
let operation = UpdateAddressCacheOperation(
queue: stateQueue,
- restClient: restClient,
+ apiProxy: apiProxy,
store: store,
updateInterval: Self.updateInterval,
completionHandler: { [weak self] completion in
@@ -138,7 +138,7 @@ extension AddressCache {
if let lastFailureAttemptDate = lastFailureAttemptDate {
return Date(timeInterval: Self.retryInterval, since: lastFailureAttemptDate)
} else {
- let updatedAt = store.getLastUpdateDateAndWait()
+ let updatedAt = store.getLastUpdateDate()
return Date(timeInterval: Self.updateInterval, since: updatedAt)
}
diff --git a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
index 2b2d42ac33..8d246827a4 100644
--- a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
+++ b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
@@ -20,15 +20,15 @@ extension AddressCache {
class UpdateAddressCacheOperation: ResultOperation<CacheUpdateResult, Error> {
private let queue: DispatchQueue
- private let restClient: REST.Client
+ private let apiProxy: REST.APIProxy
private let store: AddressCache.Store
private let updateInterval: TimeInterval
private var requestTask: Cancellable?
- init(queue: DispatchQueue, restClient: REST.Client, store: AddressCache.Store, updateInterval: TimeInterval, completionHandler: CompletionHandler?) {
+ init(queue: DispatchQueue, apiProxy: REST.APIProxy, store: AddressCache.Store, updateInterval: TimeInterval, completionHandler: CompletionHandler?) {
self.queue = queue
- self.restClient = restClient
+ self.apiProxy = apiProxy
self.store = store
self.updateInterval = updateInterval
@@ -56,7 +56,7 @@ extension AddressCache {
return
}
- let lastUpdate = store.getLastUpdateDateAndWait()
+ let lastUpdate = store.getLastUpdateDate()
let nextUpdate = Date(timeInterval: updateInterval, since: lastUpdate)
guard nextUpdate <= Date() else {
@@ -64,7 +64,7 @@ extension AddressCache {
return
}
- requestTask = restClient.getAddressList(retryStrategy: .default) { completion in
+ requestTask = apiProxy.getAddressList(retryStrategy: .default) { completion in
self.queue.async {
self.handleResponse(completion)
}
@@ -74,12 +74,11 @@ extension AddressCache {
private func handleResponse(_ completion: OperationCompletion<[AnyIPEndpoint], REST.Error>) {
switch completion {
case .success(let newEndpoints):
- self.store.setEndpoints(newEndpoints) { error in
- if let error = error {
- self.finish(completion: .failure(error))
- } else {
- self.finish(completion: .success(.finished))
- }
+ switch store.setEndpoints(newEndpoints) {
+ case .success:
+ self.finish(completion: .success(.finished))
+ case .failure(let error):
+ self.finish(completion: .failure(error))
}
case .failure(let error):
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index e2ea21f452..2f7c945073 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -38,7 +38,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private lazy var addressCacheTracker: AddressCache.Tracker = {
return AddressCache.Tracker(
- restClient: REST.Client.shared,
+ apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
store: AddressCache.Store.shared
)
}()
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index 88d5a7c922..885ad3cbad 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -28,6 +28,8 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
return queue
}()
+ private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+
private let exclusivityController = ExclusivityController()
private let paymentQueue: SKPaymentQueue
@@ -115,7 +117,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
// Validate account token before adding new payment to the queue.
- cancellableTask = REST.Client.shared.getAccountExpiry(token: accountToken, retryStrategy: .default) { result in
+ cancellableTask = apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in
dispatchPrecondition(condition: .onQueue(.main))
switch result {
@@ -176,7 +178,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool, completionHandler: @escaping (OperationCompletion<REST.CreateApplePaymentResponse, Error>) -> Void) -> Cancellable {
let operation = SendAppStoreReceiptOperation(
- restClient: REST.Client.shared,
+ apiProxy: apiProxy,
accountToken: accountToken,
forceRefresh: forceRefresh,
receiptProperties: nil,
diff --git a/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
index 69ddf4459c..3bcfa7c4f3 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
@@ -10,77 +10,77 @@ import Foundation
import Logging
class SendAppStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error> {
- private let restClient: REST.Client
- private let accountToken: String
- private let forceRefresh: Bool
- private let receiptProperties: [String: Any]?
- private var fetchReceiptTask: Cancellable?
- private var submitReceiptTask: Cancellable?
+ private let apiProxy: REST.APIProxy
+ private let accountToken: String
+ private let forceRefresh: Bool
+ private let receiptProperties: [String: Any]?
+ private var fetchReceiptTask: Cancellable?
+ private var submitReceiptTask: Cancellable?
- private let logger = Logger(label: "AppStorePaymentManager.SendAppStoreReceiptOperation")
+ private let logger = Logger(label: "AppStorePaymentManager.SendAppStoreReceiptOperation")
- init(restClient: REST.Client, accountToken: String, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping CompletionHandler) {
- self.restClient = restClient
- self.accountToken = accountToken
- self.forceRefresh = forceRefresh
- self.receiptProperties = receiptProperties
+ init(apiProxy: REST.APIProxy, accountToken: String, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping CompletionHandler) {
+ self.apiProxy = apiProxy
+ self.accountToken = accountToken
+ self.forceRefresh = forceRefresh
+ self.receiptProperties = receiptProperties
- super.init(completionQueue: .main, completionHandler: completionHandler)
- }
+ super.init(completionQueue: .main, completionHandler: completionHandler)
+ }
- override func cancel() {
- super.cancel()
+ override func cancel() {
+ super.cancel()
- DispatchQueue.main.async {
- self.fetchReceiptTask?.cancel()
- self.fetchReceiptTask = nil
+ DispatchQueue.main.async {
+ self.fetchReceiptTask?.cancel()
+ self.fetchReceiptTask = nil
- self.submitReceiptTask?.cancel()
- self.submitReceiptTask = nil
- }
- }
+ self.submitReceiptTask?.cancel()
+ self.submitReceiptTask = nil
+ }
+ }
- override func main() {
- DispatchQueue.main.async {
- guard !self.isCancelled else {
- self.finish(completion: .cancelled)
- return
- }
+ override func main() {
+ DispatchQueue.main.async {
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
+ }
- self.fetchReceiptTask = AppStoreReceipt.fetch(forceRefresh: self.forceRefresh, receiptProperties: self.receiptProperties) { completion in
- switch completion {
- case .success(let receiptData):
- self.sendReceipt(receiptData)
+ self.fetchReceiptTask = AppStoreReceipt.fetch(forceRefresh: self.forceRefresh, receiptProperties: self.receiptProperties) { completion in
+ switch completion {
+ case .success(let receiptData):
+ self.sendReceipt(receiptData)
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt.")
- self.finish(completion: .failure(.readReceipt(error)))
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt.")
+ self.finish(completion: .failure(.readReceipt(error)))
- case .cancelled:
- self.finish(completion: .cancelled)
- }
- }
- }
- }
+ case .cancelled:
+ self.finish(completion: .cancelled)
+ }
+ }
+ }
+ }
- private func sendReceipt(_ receiptData: Data) {
- submitReceiptTask = restClient.createApplePayment(
- token: self.accountToken,
- receiptString: receiptData,
- retryStrategy: .noRetry) { result in
- switch result {
- case .success(let response):
- self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())")
- self.finish(completion: .success(response))
+ private func sendReceipt(_ receiptData: Data) {
+ submitReceiptTask = apiProxy.createApplePayment(
+ token: self.accountToken,
+ receiptString: receiptData,
+ retryStrategy: .noRetry) { result in
+ switch result {
+ case .success(let response):
+ self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())")
+ self.finish(completion: .success(response))
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to send the AppStore receipt.")
- self.finish(completion: .failure(.sendReceipt(error)))
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to send the AppStore receipt.")
+ self.finish(completion: .failure(.sendReceipt(error)))
- case .cancelled:
- self.logger.debug("Receipt submission cancelled.")
- self.finish(completion: .cancelled)
- }
- }
- }
+ case .cancelled:
+ self.logger.debug("Receipt submission cancelled.")
+ self.finish(completion: .cancelled)
+ }
+ }
+ }
}
diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift
index ad8102bd37..17bd6f04a1 100644
--- a/ios/MullvadVPN/ApplicationConfiguration.swift
+++ b/ios/MullvadVPN/ApplicationConfiguration.swift
@@ -44,9 +44,15 @@ extension ApplicationConfiguration {
/// FAQ & Guides URL.
static let faqAndGuidesURL = URL(string: "https://mullvad.net/help/tag/mullvad-app/")!
- /// Default API endpoint
+ /// Default API hostname.
+ static let defaultAPIHostname = "api.mullvad.net"
+
+ /// Default API endpoint.
static let defaultAPIEndpoint = AnyIPEndpoint(string: "193.138.218.78:443")!
+ /// Default network timeout for API requests.
+ static let defaultAPINetworkTimeout: TimeInterval = 10
+
/// Background fetch minimum interval
static let minimumBackgroundFetchInterval: TimeInterval = 3600
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index aab3fce95e..5e3d154f28 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -20,7 +20,7 @@ extension REST.Error: DisplayChainedError {
return String(
format: NSLocalizedString(
"NETWORK_ERROR",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Network error: %@",
comment: "Network error. Use %@ placeholder to place localized failure description."
),
@@ -33,7 +33,7 @@ extension REST.Error: DisplayChainedError {
return String(
format: NSLocalizedString(
"SERVER_ERROR",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Server error: %@",
comment: "Server error. Use %@ placeholder to place localized failure description."
),
@@ -43,21 +43,21 @@ extension REST.Error: DisplayChainedError {
case .encodePayload:
return NSLocalizedString(
"SERVER_REQUEST_ENCODING_ERROR",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Server request encoding error",
comment: "Failure to encode the server request."
)
case .decodeSuccessResponse:
return NSLocalizedString(
"SERVER_SUCCESS_RESPONSE_DECODING_ERROR",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Server success response decoding error",
comment: "Failure to decode the server success response."
)
case .decodeErrorResponse:
return NSLocalizedString(
"SERVER_FAILURE_RESPONSE_DECODING_ERROR",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Server error response decoding error",
comment: "Failure to decode the server failure response."
)
diff --git a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
index 66a6d2aa0b..d88e5a5ec7 100644
--- a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
+++ b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
@@ -20,13 +20,18 @@ class AsyncBlockOperation: AsyncOperation {
}
override func main() {
- executionBlock?(self)
+ stateLock.lock()
+ let block = executionBlock
executionBlock = nil
+ stateLock.unlock()
+
+ block?(self)
}
override func finish() {
stateLock.lock()
cancellationBlocks.removeAll()
+ executionBlock = nil
stateLock.unlock()
super.finish()
@@ -56,3 +61,4 @@ class AsyncBlockOperation: AsyncOperation {
}
}
}
+
diff --git a/ios/MullvadVPN/Operations/ResultOperation.swift b/ios/MullvadVPN/Operations/ResultOperation.swift
index 3d9233ce2e..fcaeec7091 100644
--- a/ios/MullvadVPN/Operations/ResultOperation.swift
+++ b/ios/MullvadVPN/Operations/ResultOperation.swift
@@ -13,10 +13,11 @@ class ResultOperation<Success, Failure: Error>: AsyncOperation {
typealias Completion = OperationCompletion<Success, Failure>
typealias CompletionHandler = (Completion) -> Void
- private let stateLock = NSLock()
+ fileprivate let stateLock = NSLock()
private var completionValue: Completion?
- private let completionQueue: DispatchQueue?
- private var completionHandler: CompletionHandler?
+ private var _completionQueue: DispatchQueue?
+ private var _completionHandler: CompletionHandler?
+ private var pendingFinish = false
var completion: Completion? {
stateLock.lock()
@@ -24,41 +25,80 @@ class ResultOperation<Success, Failure: Error>: AsyncOperation {
return completionValue
}
+ var completionQueue: DispatchQueue? {
+ get {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+
+ return _completionQueue
+ }
+ set {
+ stateLock.lock()
+ _completionQueue = newValue
+ stateLock.unlock()
+ }
+ }
+
+ var completionHandler: CompletionHandler? {
+ get {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+
+ return _completionHandler
+ }
+ set {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ if !pendingFinish {
+ _completionHandler = newValue
+ }
+ }
+ }
+
init(completionQueue: DispatchQueue?, completionHandler: CompletionHandler?) {
- self.completionQueue = completionQueue
- self.completionHandler = completionHandler
+ _completionQueue = completionQueue
+ _completionHandler = completionHandler
super.init()
}
@available(*, unavailable)
override func finish() {
- // Propagate cancellation if finish() is called directly from start().
- if isCancelled {
- finish(completion: .cancelled)
- } else {
- preconditionFailure("Use finish(completion:) to finish operation.")
- }
+ _finish()
}
func finish(completion: Completion) {
stateLock.lock()
+ if completionValue == nil {
+ completionValue = completion
+ }
+ stateLock.unlock()
+
+ _finish()
+ }
+ fileprivate func _finish() {
+ stateLock.lock()
// Bail if operation is already finishing.
- guard completionValue == nil else {
+ guard !pendingFinish else {
stateLock.unlock()
return
}
- // Store completion value.
- completionValue = completion
+ // Mark that operation is pending finish.
+ pendingFinish = true
// Copy completion handler.
- let completionHandler: CompletionHandler? = self.completionHandler
+ let completionHandler = _completionHandler
// Unset completion handler.
- self.completionHandler = nil
+ _completionHandler = nil
+
+ // Copy completion value.
+ let completion = completionValue ?? .cancelled
+ // Copy completion queue.
+ let completionQueue = _completionQueue
stateLock.unlock()
let block = {
@@ -76,3 +116,71 @@ class ResultOperation<Success, Failure: Error>: AsyncOperation {
}
}
}
+
+class ResultBlockOperation<Success, Failure: Error>: ResultOperation<Success, Failure> {
+ typealias ExecutionBlock = (ResultBlockOperation<Success, Failure>) -> Void
+
+ private var executionBlock: ExecutionBlock?
+ private var cancellationBlocks: [() -> Void] = []
+
+ convenience init(executionBlock: @escaping ExecutionBlock) {
+ self.init(
+ executionBlock: executionBlock,
+ completionQueue: nil,
+ completionHandler: nil
+ )
+ }
+
+ init(
+ executionBlock: @escaping ExecutionBlock,
+ completionQueue: DispatchQueue?,
+ completionHandler: CompletionHandler?
+ )
+ {
+ self.executionBlock = executionBlock
+ super.init(completionQueue: completionQueue, completionHandler: completionHandler)
+ }
+
+ override func main() {
+ stateLock.lock()
+ let block = executionBlock
+ executionBlock = nil
+ stateLock.unlock()
+
+ block?(self)
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ stateLock.lock()
+ let blocks = cancellationBlocks
+ cancellationBlocks.removeAll()
+ stateLock.unlock()
+
+ for block in blocks {
+ block()
+ }
+ }
+
+ override func _finish() {
+ stateLock.lock()
+ cancellationBlocks.removeAll()
+ executionBlock = nil
+ stateLock.unlock()
+
+ super._finish()
+ }
+
+ func addCancellationBlock(_ block: @escaping () -> Void) {
+ stateLock.lock()
+ if isCancelled {
+ stateLock.unlock()
+ block()
+ } else {
+ cancellationBlocks.append(block)
+ stateLock.unlock()
+ }
+ }
+}
+
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift
index b988d34cdb..b9311174e7 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/ProblemReportViewController.swift
@@ -10,6 +10,8 @@ import UIKit
class ProblemReportViewController: UIViewController, UITextFieldDelegate, ConditionalNavigation {
+ private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+
private var textViewKeyboardResponder: AutomaticKeyboardResponder?
private var scrollViewKeyboardResponder: AutomaticKeyboardResponder?
@@ -600,7 +602,7 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
willSendProblemReport()
- _ = REST.Client.shared.sendProblemReport(request, retryStrategy: .default) { completion in
+ _ = apiProxy.sendProblemReport(request, retryStrategy: .default) { completion in
self.didSendProblemReport(viewModel: viewModel, completion: completion)
}
}
diff --git a/ios/MullvadVPN/REST/RESTAPIProxy.swift b/ios/MullvadVPN/REST/RESTAPIProxy.swift
new file mode 100644
index 0000000000..15cc807c16
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTAPIProxy.swift
@@ -0,0 +1,498 @@
+//
+// RESTAPIProxy.swift
+// MullvadVPN
+//
+// Created by pronebird on 10/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+import class WireGuardKitTypes.PublicKey
+import struct WireGuardKitTypes.IPAddressRange
+
+extension REST {
+ class APIProxy: Proxy<ProxyConfiguration> {
+ init(configuration: ProxyConfiguration) {
+ super.init(
+ name: "APIProxy",
+ configuration: configuration,
+ requestFactory: RequestFactory.withDefaultAPICredentials(
+ pathPrefix: "/app/v1",
+ bodyEncoder: Coding.makeJSONEncoder()
+ ),
+ responseDecoder: ResponseDecoder(
+ decoder: Coding.makeJSONDecoder()
+ )
+ )
+ }
+
+ func createAccount(
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<AccountResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ let request = self.requestFactory.createURLRequest(
+ endpoint: endpoint,
+ method: .post,
+ path: "accounts"
+ )
+
+ return .success(request)
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: AccountResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "create-account",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func getAddressList(
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<[AnyIPEndpoint]>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ let request = self.requestFactory.createURLRequest(
+ endpoint: endpoint,
+ method: .get,
+ path: "api-addrs"
+ )
+
+ return .success(request)
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: [AnyIPEndpoint].self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-api-addrs",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func getRelays(
+ etag: String?,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<ServerRelaysCacheResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ path: "relays"
+ )
+
+ if let etag = etag {
+ requestBuilder.setETagHeader(etag: etag)
+ }
+
+ return .success(requestBuilder.getURLRequest())
+ }
+
+ let responseHandler = AnyResponseHandler { response, data -> Result<ServerRelaysCacheResponse, REST.Error> in
+ if HTTPStatus.isSuccess(response.statusCode) {
+ return self.responseDecoder.decodeSuccessResponse(ServerRelaysResponse.self, from: data)
+ .map { serverRelays in
+ let newEtag = response.value(forCaseInsensitiveHTTPHeaderField: HTTPHeader.etag)
+ return .newContent(newEtag, serverRelays)
+ }
+ } else if response.statusCode == HTTPStatus.notModified && etag != nil {
+ return .success(.notModified)
+ } else {
+ return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+
+ return addOperation(
+ name: "get-relays",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func getAccountExpiry(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<AccountResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory
+ .createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ path: "me"
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return .success(requestBuilder.getURLRequest())
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: AccountResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-account-expiry",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func getWireguardKey(
+ accountNumber: String,
+ publicKey: PublicKey,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ let urlEncodedPublicKey = publicKey.base64Key
+ .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
+ let path = "wireguard-keys/".appending(urlEncodedPublicKey)
+
+ var requestBuilder = self.requestFactory
+ .createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ path: path
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return .success(requestBuilder.getURLRequest())
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: WireguardAddressesResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-wireguard-key",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func pushWireguardKey(
+ accountNumber: String,
+ publicKey: PublicKey,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ path: "wireguard-keys"
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return Result {
+ let body = PushWireguardKeyRequest(
+ pubkey: publicKey.rawValue
+ )
+ try requestBuilder.setHTTPBody(value: body)
+ }
+ .mapError { error in
+ return .encodePayload(error)
+ }
+ .map { _ in
+ return requestBuilder.getURLRequest()
+ }
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: WireguardAddressesResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "push-wireguard-key",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func replaceWireguardKey(
+ accountNumber: String,
+ oldPublicKey: PublicKey,
+ newPublicKey: PublicKey,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ path: "replace-wireguard-key"
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return Result {
+ let body = ReplaceWireguardKeyRequest(
+ old: oldPublicKey.rawValue,
+ new: newPublicKey.rawValue
+ )
+ try requestBuilder.setHTTPBody(value: body)
+ }
+ .mapError { error in
+ return .encodePayload(error)
+ }
+ .map { _ in
+ return requestBuilder.getURLRequest()
+ }
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: WireguardAddressesResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "replace-wireguard-key",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func deleteWireguardKey(
+ accountNumber: String,
+ publicKey: PublicKey,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<Void>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ let urlEncodedPublicKey = publicKey.base64Key
+ .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
+
+ let path = "wireguard-keys/".appending(urlEncodedPublicKey)
+ var requestBuilder = self.requestFactory
+ .createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .delete,
+ path: path
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return .success(requestBuilder.getURLRequest())
+ }
+
+ let responseHandler = AnyResponseHandler { response, data -> Result<Void, REST.Error> in
+ if HTTPStatus.isSuccess(response.statusCode) {
+ return .success(())
+ } else {
+ return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+
+ return addOperation(
+ name: "delete-wireguard-key",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func createApplePayment(
+ accountNumber: String,
+ receiptString: Data,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<CreateApplePaymentResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory
+ .createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ path: "create-apple-payment"
+ )
+ requestBuilder.setAuthorization(.accountNumber(accountNumber))
+
+ return Result {
+ let body = CreateApplePaymentRequest(
+ receiptString: receiptString
+ )
+ try requestBuilder.setHTTPBody(value: body)
+ }
+ .mapError { error in
+ return .encodePayload(error)
+ }
+ .map { _ in
+ return requestBuilder.getURLRequest()
+ }
+ }
+
+ let responseHandler = AnyResponseHandler { response, data -> Result<CreateApplePaymentResponse, REST.Error> in
+ if HTTPStatus.isSuccess(response.statusCode) {
+ return self.responseDecoder.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
+ .map { (response) in
+ if response.timeAdded > 0 {
+ return .timeAdded(response.timeAdded, response.newExpiry)
+ } else {
+ return .noTimeAdded(response.newExpiry)
+ }
+ }
+ } else {
+ return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+
+ return addOperation(
+ name: "create-apple-payment",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+
+ func sendProblemReport(
+ _ body: ProblemReportRequest,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping CompletionHandler<Void>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ path: "problem-report"
+ )
+
+ return Result {
+ try requestBuilder.setHTTPBody(value: body)
+ }
+ .mapError { error in
+ return .encodePayload(error)
+ }
+ .map { _ in
+ return requestBuilder.getURLRequest()
+ }
+ }
+
+ let responseHandler = AnyResponseHandler { response, data -> Result<Void, REST.Error> in
+ if HTTPStatus.isSuccess(response.statusCode) {
+ return .success(())
+ } else {
+ return self.responseDecoder.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+
+ return addOperation(
+ name: "send-problem-report",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+ }
+ }
+
+ // MARK: - Response types
+
+ struct AccountResponse: Decodable {
+ let token: String
+ let expires: Date
+ }
+
+ enum ServerRelaysCacheResponse {
+ case notModified
+ case newContent(_ etag: String?, _ value: ServerRelaysResponse)
+ }
+
+ struct WireguardAddressesResponse: Decodable {
+ let id: String
+ let pubkey: Data
+ let ipv4Address: IPAddressRange
+ let ipv6Address: IPAddressRange
+ }
+
+ fileprivate struct PushWireguardKeyRequest: Encodable {
+ let pubkey: Data
+ }
+
+ fileprivate struct ReplaceWireguardKeyRequest: Encodable {
+ let old: Data
+ let new: Data
+ }
+
+ fileprivate struct CreateApplePaymentRequest: Encodable {
+ let receiptString: Data
+ }
+
+ enum CreateApplePaymentResponse {
+ case noTimeAdded(_ expiry: Date)
+ case timeAdded(_ timeAdded: Int, _ newExpiry: Date)
+
+ var newExpiry: Date {
+ switch self {
+ case .noTimeAdded(let expiry), .timeAdded(_, let expiry):
+ return expiry
+ }
+ }
+
+ var timeAdded: TimeInterval {
+ switch self {
+ case .noTimeAdded:
+ return 0
+ case .timeAdded(let timeAdded, _):
+ return TimeInterval(timeAdded)
+ }
+ }
+
+ /// 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: self.timeAdded)
+ }
+ }
+
+ fileprivate struct CreateApplePaymentRawResponse: Decodable {
+ let timeAdded: Int
+ let newExpiry: Date
+ }
+
+ struct ProblemReportRequest: Encodable {
+ let address: String
+ let message: String
+ let log: String
+ let metadata: [String: String]
+ }
+
+}
diff --git a/ios/MullvadVPN/REST/RESTAccessTokenManager.swift b/ios/MullvadVPN/REST/RESTAccessTokenManager.swift
new file mode 100644
index 0000000000..1dc849f59f
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTAccessTokenManager.swift
@@ -0,0 +1,74 @@
+//
+// RESTAccessTokenManager.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+extension REST {
+
+ final class AccessTokenManager {
+ private let logger = Logger(label: "REST.AccessTokenManager")
+ private let operationQueue = OperationQueue()
+ private let dispatchQueue = DispatchQueue(label: "REST.AccessTokenManager.dispatchQueue")
+ private let proxy: AuthenticationProxy
+ private var tokens = [String: AccessTokenData]()
+
+ init(authenticationProxy: AuthenticationProxy) {
+ operationQueue.name = "REST.AccessTokenManager.operationQueue"
+ operationQueue.maxConcurrentOperationCount = 1
+ operationQueue.underlyingQueue = dispatchQueue
+ proxy = authenticationProxy
+ }
+
+ func getAccessToken(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completionHandler: @escaping (OperationCompletion<REST.AccessTokenData, REST.Error>) -> Void
+ ) -> Cancellable
+ {
+ let operation = ResultBlockOperation<REST.AccessTokenData, REST.Error> { operation in
+ if let tokenData = self.tokens[accountNumber], tokenData.expiry > Date() {
+ operation.finish(completion: .success(tokenData))
+ return
+ }
+
+ let task = self.proxy.getAccessToken(
+ accountNumber: accountNumber,
+ retryStrategy: retryStrategy
+ ) { completion in
+ self.dispatchQueue.async {
+ switch completion {
+ case .success(let tokenData):
+ self.tokens[accountNumber] = tokenData
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to fetch access token.")
+
+ case .cancelled:
+ break
+ }
+
+ operation.finish(completion: completion)
+ }
+ }
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ operation.completionQueue = .main
+ operation.completionHandler = completionHandler
+
+ operationQueue.addOperation(operation)
+
+ return operation
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/REST/RESTAccountsProxy.swift b/ios/MullvadVPN/REST/RESTAccountsProxy.swift
new file mode 100644
index 0000000000..2bc05e088c
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTAccountsProxy.swift
@@ -0,0 +1,82 @@
+//
+// RESTAccountsProxy.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ class AccountsProxy: Proxy<AuthProxyConfiguration> {
+ init(configuration: AuthProxyConfiguration) {
+ super.init(
+ name: "AccountsProxy",
+ configuration: configuration,
+ requestFactory: RequestFactory.withDefaultAPICredentials(
+ pathPrefix: "/accounts/v1-beta1",
+ bodyEncoder: Coding.makeJSONEncoder()
+ ),
+ responseDecoder: ResponseDecoder(
+ decoder: Coding.makeJSONDecoder()
+ )
+ )
+ }
+
+ func getMyAccount(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<BetaAccountResponse>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ path: "/accounts/me"
+ )
+
+ requestBuilder.setAuthorization(authorization)
+
+ return .success(requestBuilder.getURLRequest())
+ },
+ requestAuthorization: { completion in
+ return self.configuration.accessTokenManager
+ .getAccessToken(
+ accountNumber: accountNumber,
+ retryStrategy: retryStrategy
+ ) { operationCompletion in
+ completion(operationCompletion.map { tokenData in
+ return .accessToken(tokenData.accessToken)
+ })
+ }
+ }
+ )
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: BetaAccountResponse.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-my-account",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+ }
+
+ struct BetaAccountResponse: Decodable {
+ let id: String
+ let number: String
+ let expiry: Date
+ let maxPorts: Int
+ let canAddPorts: Bool
+ let maxDevices: Int
+ let canAddDevices: Bool
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift
new file mode 100644
index 0000000000..c4ab8e9d04
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift
@@ -0,0 +1,76 @@
+//
+// RESTAuthenticationProxy.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ class AuthenticationProxy: Proxy<ProxyConfiguration> {
+ init(configuration: ProxyConfiguration) {
+ super.init(
+ name: "AuthenticationProxy",
+ configuration: configuration,
+ requestFactory: RequestFactory.withDefaultAPICredentials(
+ pathPrefix: "/auth/v1-beta1",
+ bodyEncoder: Coding.makeJSONEncoder()
+ ),
+ responseDecoder: ResponseDecoder(
+ decoder: Coding.makeJSONDecoder()
+ )
+ )
+ }
+
+ func getAccessToken(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<AccessTokenData>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler { endpoint in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .post,
+ path: "/token"
+ )
+
+ return Result {
+ let request = AccessTokenRequest(accountNumber: accountNumber)
+
+ try requestBuilder.setHTTPBody(value: request)
+ }
+ .mapError { error in
+ return .encodePayload(error)
+ }
+ .map { _ in
+ return requestBuilder.getURLRequest()
+ }
+ }
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: AccessTokenData.self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-access-token",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+ }
+
+ struct AccessTokenData: Decodable {
+ let accessToken: String
+ let expiry: Date
+ }
+
+ fileprivate struct AccessTokenRequest: Encodable {
+ let accountNumber: String
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTAuthorization.swift b/ios/MullvadVPN/REST/RESTAuthorization.swift
new file mode 100644
index 0000000000..b9a701b1ac
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTAuthorization.swift
@@ -0,0 +1,16 @@
+//
+// RESTAuthorization.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ enum Authorization {
+ case accountNumber(String)
+ case accessToken(String)
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTClient.swift b/ios/MullvadVPN/REST/RESTClient.swift
deleted file mode 100644
index 3d9ba2fb31..0000000000
--- a/ios/MullvadVPN/REST/RESTClient.swift
+++ /dev/null
@@ -1,524 +0,0 @@
-//
-// RESTClient.swift
-// MullvadVPN
-//
-// Created by pronebird on 10/07/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Network
-import class WireGuardKitTypes.PublicKey
-import struct WireGuardKitTypes.IPAddressRange
-
-extension REST {
-
- class Client {
- static let shared: Client = {
- return Client(addressCacheStore: AddressCache.Store.shared)
- }()
-
- /// URL session.
- private let session: URLSession
-
- /// URL session delegate.
- private let sessionDelegate: SSLPinningURLSessionDelegate
-
- /// REST API hostname.
- private let apiHostname = "api.mullvad.net"
-
- /// REST API base path.
- private let apiBasePath = "/app/v1"
-
- /// Network request timeout in seconds.
- private let networkTimeout: TimeInterval = 10
-
- /// Address cache store.
- private let addressCacheStore: AddressCache.Store
-
- /// Operation queue used for running network requests.
- private let operationQueue = OperationQueue()
-
- /// Network task counter.
- private var networkTaskCounter: UInt32 = 0
-
- /// Lock used for internal synchronization.
- private var nslock = NSLock()
-
- /// Returns array of trusted root certificates
- private static var trustedRootCertificates: [SecCertificate] {
- let rootCertificate = Bundle.main.path(forResource: "le_root_cert", ofType: "cer")!
-
- return [rootCertificate].map { (path) -> SecCertificate in
- let data = FileManager.default.contents(atPath: path)!
- return SecCertificateCreateWithData(nil, data as CFData)!
- }
- }
-
- init(addressCacheStore: AddressCache.Store) {
- sessionDelegate = SSLPinningURLSessionDelegate(sslHostname: apiHostname, trustedRootCertificates: Self.trustedRootCertificates)
- session = URLSession(configuration: .ephemeral, delegate: sessionDelegate, delegateQueue: nil)
- self.addressCacheStore = addressCacheStore
- }
-
- // MARK: - Public
-
- func createAccount(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<AccountResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "create-account", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- let request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "accounts")
-
- let dataTask = self.dataTask(request: request) { responseResult in
- let restResult = responseResult
- .mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<AccountResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(AccountResponse.self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func getAddressList(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<[AnyIPEndpoint], REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "get-api-addrs", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- let request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "api-addrs")
-
- let dataTask = self.dataTask(request: request) { responseResult in
- let restResult = responseResult.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<[AnyIPEndpoint], REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse([AnyIPEndpoint].self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func getRelays(etag: String?, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<ServerRelaysCacheResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "get-relays", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "relays")
- if let etag = etag {
- Self.setETagHeader(etag: etag, request: &request)
- }
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<ServerRelaysCacheResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(ServerRelaysResponse.self, from: data)
- .map { serverRelays in
- let newEtag = httpResponse.value(forCaseInsensitiveHTTPHeaderField: HTTPHeader.etag)
- return .newContent(newEtag, serverRelays)
- }
- } else if httpResponse.statusCode == HTTPStatus.notModified && etag != nil {
- return .success(.notModified)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func getAccountExpiry(token: String, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<AccountResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "get-account-expiry", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "me")
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<AccountResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(AccountResponse.self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func getWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "get-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- let urlEncodedPublicKey = publicKey.base64Key
- .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
-
- let path = "wireguard-keys/".appending(urlEncodedPublicKey)
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: path)
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<WireguardAddressesResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func pushWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "push-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "wireguard-keys")
- let body = PushWireguardKeyRequest(pubkey: publicKey.rawValue)
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- do {
- try Self.setHTTPBody(value: body, request: &request)
- } catch {
- return .failure(.encodePayload(error))
- }
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<WireguardAddressesResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func replaceWireguardKey(token: String, oldPublicKey: PublicKey, newPublicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "replace-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "replace-wireguard-key")
- let body = ReplaceWireguardKeyRequest(old: oldPublicKey.rawValue, new: newPublicKey.rawValue)
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- do {
- try Self.setHTTPBody(value: body, request: &request)
- } catch {
- return .failure(.encodePayload(error))
- }
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<WireguardAddressesResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func deleteWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<(), REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "delete-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- let urlEncodedPublicKey = publicKey.base64Key
- .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
-
- let path = "wireguard-keys/".appending(urlEncodedPublicKey)
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .delete, path: path)
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<(), REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return .success(())
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
-
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func createApplePayment(token: String, receiptString: Data, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<CreateApplePaymentResponse, REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "create-apple-payment", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "create-apple-payment")
- let body = CreateApplePaymentRequest(receiptString: receiptString)
-
- Self.setAuthenticationToken(token: token, request: &request)
-
- do {
- try Self.setHTTPBody(value: body, request: &request)
- } catch {
- return .failure(.encodePayload(error))
- }
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<CreateApplePaymentResponse, REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return REST.Client.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
- .map { (response) in
- if response.timeAdded > 0 {
- return .timeAdded(response.timeAdded, response.newExpiry)
- } else {
- return .noTimeAdded(response.newExpiry)
- }
- }
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- func sendProblemReport(_ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<(), REST.Error>) -> Void) -> Cancellable {
- return scheduleOperation(name: "send-problem-report", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
- var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "problem-report")
-
- do {
- try Self.setHTTPBody(value: body, request: &request)
- } catch {
- return .failure(.encodePayload(error))
- }
-
- let dataTask = self.dataTask(request: request) { restResponse in
- let restResult = restResponse.mapError(Self.mapNetworkError)
- .flatMap { httpResponse, data -> Result<(), REST.Error> in
- if HTTPStatus.isSuccess(httpResponse.statusCode) {
- return .success(())
- } else {
- return Self.decodeErrorResponseAndMapToServerError(from: data)
- }
- }
- finishOperation(restResult)
- }
-
- return .success(dataTask)
- }
- }
-
- // MARK: - Private
-
- private func nextTaskIdentifier() -> UInt32 {
- nslock.lock()
- let (partialValue, isOverflow) = networkTaskCounter.addingReportingOverflow(1)
- let nextValue = isOverflow ? 1 : partialValue
- networkTaskCounter = nextValue
- nslock.unlock()
-
- return nextValue
- }
-
- private func scheduleOperation<Response>(name: String, retryStrategy: REST.RetryStrategy, completionHandler: @escaping NetworkOperation<Response>.CompletionHandler, taskGenerator: @escaping NetworkOperation<Response>.Generator) -> Cancellable {
- let operation = NetworkOperation(
- taskIdentifier: nextTaskIdentifier(),
- name: name,
- networkTaskGenerator: taskGenerator,
- addressCacheStore: addressCacheStore,
- retryStrategy: retryStrategy,
- completionHandler: completionHandler
- )
-
- operationQueue.addOperation(operation)
-
- return operation
- }
-
- private func dataTask(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data), URLError>) -> Void) -> URLSessionDataTask {
- return self.session.dataTask(with: request) { data, response, error in
- if let error = error {
- let urlError = error as? URLError ?? URLError(.unknown)
-
- completion(.failure(urlError))
- } else {
- if let httpResponse = response as? HTTPURLResponse {
- let data = data ?? Data()
- let value = (httpResponse, data)
-
- completion(.success(value))
- } else {
- completion(.failure(URLError(.unknown)))
- }
- }
- }
- }
-
- private func createURLRequestWithEndpoint(endpoint: AnyIPEndpoint, method: HTTPMethod, path: String) -> URLRequest {
- var urlComponents = URLComponents()
- urlComponents.scheme = "https"
- urlComponents.path = apiBasePath
- urlComponents.host = "\(endpoint.ip)"
- urlComponents.port = Int(endpoint.port)
-
- let requestURL = urlComponents.url!.appendingPathComponent(path)
-
- var request = URLRequest(
- url: requestURL,
- cachePolicy: .useProtocolCachePolicy,
- timeoutInterval: networkTimeout
- )
- request.httpShouldHandleCookies = false
- request.addValue(apiHostname, forHTTPHeaderField: HTTPHeader.host)
- request.addValue("application/json", forHTTPHeaderField: HTTPHeader.contentType)
- request.httpMethod = method.rawValue
- return request
- }
-
- /// Parse JSON response into the given `Decodable` type.
- private static func decodeSuccessResponse<T: Decodable>(_ type: T.Type, from data: Data) -> Result<T, REST.Error> {
- return Result { try REST.Coding.makeJSONDecoder().decode(type, from: data) }
- .mapError { error in
- return .decodeSuccessResponse(error)
- }
- }
-
- /// Parse JSON response in case of error (Any HTTP code except 2xx).
- private static func decodeErrorResponse(from data: Data) -> Result<ServerErrorResponse, REST.Error> {
- return Result { () -> ServerErrorResponse in
- return try REST.Coding.makeJSONDecoder().decode(ServerErrorResponse.self, from: data)
- }.mapError { error in
- return .decodeErrorResponse(error)
- }
- }
-
- private static func decodeErrorResponseAndMapToServerError<T>(from data: Data) -> Result<T, REST.Error> {
- return Self.decodeErrorResponse(from: data)
- .flatMap { serverError in
- return .failure(.server(serverError))
- }
- }
-
- private static func mapNetworkError(_ error: URLError) -> REST.Error {
- return .network(error)
- }
-
- private static func setHTTPBody<T: Encodable>(value: T, request: inout URLRequest) throws {
- request.httpBody = try REST.Coding.makeJSONEncoder().encode(value)
- }
-
- private static func setETagHeader(etag: String, request: inout URLRequest) {
- var etag = etag
- // Enforce weak validator to account for some backend caching quirks.
- if etag.starts(with: "\"") {
- etag.insert(contentsOf: "W/", at: etag.startIndex)
- }
- request.setValue(etag, forHTTPHeaderField: HTTPHeader.ifNoneMatch)
- }
-
- private static func setAuthenticationToken(token: String, request: inout URLRequest) {
- request.addValue("Token \(token)", forHTTPHeaderField: HTTPHeader.authorization)
- }
- }
-
- // MARK: - Response types
-
- struct AccountResponse: Decodable {
- let token: String
- let expires: Date
- }
-
- enum ServerRelaysCacheResponse {
- case notModified
- case newContent(_ etag: String?, _ value: ServerRelaysResponse)
- }
-
- struct WireguardAddressesResponse: Decodable {
- let id: String
- let pubkey: Data
- let ipv4Address: IPAddressRange
- let ipv6Address: IPAddressRange
- }
-
- fileprivate struct PushWireguardKeyRequest: Encodable {
- let pubkey: Data
- }
-
- fileprivate struct ReplaceWireguardKeyRequest: Encodable {
- let old: Data
- let new: Data
- }
-
- fileprivate struct CreateApplePaymentRequest: Encodable {
- let receiptString: Data
- }
-
- enum CreateApplePaymentResponse {
- case noTimeAdded(_ expiry: Date)
- case timeAdded(_ timeAdded: Int, _ newExpiry: Date)
-
- var newExpiry: Date {
- switch self {
- case .noTimeAdded(let expiry), .timeAdded(_, let expiry):
- return expiry
- }
- }
-
- var timeAdded: TimeInterval {
- switch self {
- case .noTimeAdded:
- return 0
- case .timeAdded(let timeAdded, _):
- return TimeInterval(timeAdded)
- }
- }
-
- /// 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: self.timeAdded)
- }
- }
-
- fileprivate struct CreateApplePaymentRawResponse: Decodable {
- let timeAdded: Int
- let newExpiry: Date
- }
-
- struct ProblemReportRequest: Encodable {
- let address: String
- let message: String
- let log: String
- let metadata: [String: String]
- }
-
-}
diff --git a/ios/MullvadVPN/REST/RESTCoding.swift b/ios/MullvadVPN/REST/RESTCoding.swift
index e5515e439b..8a5356e3bc 100644
--- a/ios/MullvadVPN/REST/RESTCoding.swift
+++ b/ios/MullvadVPN/REST/RESTCoding.swift
@@ -17,8 +17,8 @@ extension REST.Coding {
static func makeJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
- encoder.dateEncodingStrategy = .iso8601
encoder.dataEncodingStrategy = .base64
+ encoder.dateEncodingStrategy = .iso8601
return encoder
}
@@ -26,8 +26,37 @@ extension REST.Coding {
static func makeJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
- decoder.dateDecodingStrategy = .iso8601
decoder.dataDecodingStrategy = .base64
+
+ let iso8601Formatter = ISO8601DateFormatter()
+
+ // Setup additional formatter to account for fractional seconds returned
+ // by some of the API calls.
+ lazy var iso8601WithSubSecondsFormatter: ISO8601DateFormatter = {
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions.insert(.withFractionalSeconds)
+ return formatter
+ }()
+
+ decoder.dateDecodingStrategy = .custom({ decoder in
+ let container = try decoder.singleValueContainer()
+ let value = try container.decode(String.self)
+
+ let date = iso8601Formatter.date(from: value) ??
+ iso8601WithSubSecondsFormatter.date(from: value)
+
+ switch date {
+ case .some(let parsedDate):
+ return parsedDate
+
+ case .none:
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "Expected date string to be RFC3339 or ISO8601-formatted."
+ )
+ }
+ })
+
return decoder
}
}
diff --git a/ios/MullvadVPN/REST/RESTDevicesProxy.swift b/ios/MullvadVPN/REST/RESTDevicesProxy.swift
new file mode 100644
index 0000000000..698c51681b
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTDevicesProxy.swift
@@ -0,0 +1,96 @@
+//
+// RESTDevicesProxy.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import class WireGuardKitTypes.PublicKey
+import struct WireGuardKitTypes.IPAddressRange
+
+extension REST {
+ class DevicesProxy: Proxy<AuthProxyConfiguration> {
+ init(configuration: AuthProxyConfiguration) {
+ super.init(
+ name: "DevicesProxy",
+ configuration: configuration,
+ requestFactory: RequestFactory.withDefaultAPICredentials(
+ pathPrefix: "/accounts/v1-beta1",
+ bodyEncoder: Coding.makeJSONEncoder()
+ ),
+ responseDecoder: ResponseDecoder(
+ decoder: Coding.makeJSONDecoder()
+ )
+ )
+ }
+
+ func getDevices(
+ accountNumber: String,
+ retryStrategy: REST.RetryStrategy,
+ completion: @escaping CompletionHandler<[Device]>
+ ) -> Cancellable
+ {
+ let requestHandler = AnyRequestHandler(
+ createURLRequest: { endpoint, authorization in
+ var requestBuilder = self.requestFactory.createURLRequestBuilder(
+ endpoint: endpoint,
+ method: .get,
+ path: "/devices"
+ )
+
+ requestBuilder.setAuthorization(authorization)
+
+ return .success(requestBuilder.getURLRequest())
+ },
+ requestAuthorization: { completion in
+ return self.configuration.accessTokenManager
+ .getAccessToken(
+ accountNumber: accountNumber,
+ retryStrategy: retryStrategy
+ ) { operationCompletion in
+ completion(operationCompletion.map { tokenData in
+ return .accessToken(tokenData.accessToken)
+ })
+ }
+ }
+ )
+
+ let responseHandler = REST.defaultResponseHandler(
+ decoding: [Device].self,
+ with: responseDecoder
+ )
+
+ return addOperation(
+ name: "get-devices",
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completion
+ )
+ }
+
+ }
+
+ struct Device: Decodable {
+ let id: String
+ let name: String
+ let pubkey: Data
+ let hijackDNS: Bool
+ let created: Date
+ let ipv4Address: IPAddressRange
+ let ipv6Address: IPAddressRange
+ let ports: [Port]
+
+ private enum CodingKeys: String, CodingKey {
+ case hijackDNS = "hijackDns"
+ case id, name, pubkey, created, ipv4Address, ipv6Address, ports
+ }
+ }
+
+ struct Port: Decodable {
+ let id: String
+ }
+
+}
diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift
index 6d13a6e6f9..6ee7e74d26 100644
--- a/ios/MullvadVPN/REST/RESTError.swift
+++ b/ios/MullvadVPN/REST/RESTError.swift
@@ -10,7 +10,7 @@ import Foundation
extension REST {
- /// An error type returned by `REST.Client`
+ /// An error type returned by REST API classes.
enum Error: ChainedError {
/// A failure to encode the payload
case encodePayload(Swift.Error)
@@ -50,6 +50,7 @@ extension REST {
case invalidAccount = "INVALID_ACCOUNT"
case keyLimitReached = "KEY_LIMIT_REACHED"
case pubKeyNotFound = "PUBKEY_NOT_FOUND"
+ case invalidAccessToken = "INVALID_ACCESS_TOKEN"
static func ~= (pattern: Self, value: REST.ServerErrorResponse) -> Bool {
return pattern.rawValue == value.code
@@ -65,6 +66,9 @@ extension REST {
static var pubKeyNotFound: Code {
return .pubKeyNotFound
}
+ static var invalidAccessToken: Code {
+ return .invalidAccessToken
+ }
let code: String
let error: String?
@@ -74,19 +78,32 @@ extension REST {
case Code.keyLimitReached.rawValue:
return NSLocalizedString(
"KEY_LIMIT_REACHED_ERROR_DESCRIPTION",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Too many WireGuard keys in use.",
comment: ""
)
case Code.invalidAccount.rawValue:
return NSLocalizedString(
"INVALID_ACCOUNT_ERROR_DESCRIPTION",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Invalid account.",
comment: ""
)
+
+ case Code.invalidAccessToken.rawValue:
+ return NSLocalizedString(
+ "INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION",
+ tableName: "REST",
+ value: "Invalid access token.",
+ comment: "")
default:
- return nil
+ let localizedString = NSLocalizedString(
+ "UNKNOWN_ERROR_DESCRIPTION",
+ tableName: "REST",
+ value: "Unknown error: %@",
+ comment: "Use %@ placeholder to place the error code into the localized string."
+ )
+ return String(format: localizedString, code)
}
}
@@ -95,7 +112,7 @@ extension REST {
case Code.keyLimitReached.rawValue:
return NSLocalizedString(
"KEY_LIMIT_REACHED_ERROR_RECOVERY_SUGGESTION",
- tableName: "RESTClient",
+ tableName: "REST",
value: "Please visit the website to revoke a key before login is possible.",
comment: ""
)
diff --git a/ios/MullvadVPN/REST/RESTNetworkOperation.swift b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
index cc20c526b0..97e7c0e9a6 100644
--- a/ios/MullvadVPN/REST/RESTNetworkOperation.swift
+++ b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
@@ -10,24 +10,19 @@ import Foundation
import Logging
extension REST {
-
- enum RetryAction {
- /// Retry request using next endpoint.
- case useNextEndpoint
-
- /// Retry request using current endpoint.
- case useCurrentEndpoint
-
- /// Fail immediately.
- case failImmediately
- }
-
class NetworkOperation<Success>: ResultOperation<Success, REST.Error> {
- typealias Generator = (AnyIPEndpoint, @escaping (Result<Success, REST.Error>) -> Void) -> Result<URLSessionTask, REST.Error>
+ private let requestHandler: AnyRequestHandler
+ private let responseHandler: AnyResponseHandler<Success>
- private let networkTaskGenerator: Generator
+ private let dispatchQueue: DispatchQueue
+ private let urlSession: URLSession
private let addressCacheStore: AddressCache.Store
- private var sessionTask: URLSessionTask?
+
+ private var networkTask: URLSessionTask?
+ private var authorizationTask: Cancellable?
+
+ private var requiresAuthorization = false
+ private var retryInvalidAccessTokenError = true
private let retryStrategy: RetryStrategy
private var retryTimer: DispatchSourceTimer?
@@ -36,155 +31,236 @@ extension REST {
private let logger = Logger(label: "REST.NetworkOperation")
private let loggerMetadata: Logger.Metadata
- init(taskIdentifier: UInt32, name: String, networkTaskGenerator: @escaping Generator, addressCacheStore: AddressCache.Store, retryStrategy: RetryStrategy, completionHandler: @escaping CompletionHandler) {
- self.networkTaskGenerator = networkTaskGenerator
- self.addressCacheStore = addressCacheStore
+ init(
+ name: String,
+ dispatchQueue: DispatchQueue,
+ configuration: ProxyConfiguration,
+ retryStrategy: RetryStrategy,
+ requestHandler: AnyRequestHandler,
+ responseHandler: AnyResponseHandler<Success>,
+ completionHandler: @escaping CompletionHandler
+ )
+ {
+ self.dispatchQueue = dispatchQueue
+ self.urlSession = configuration.session
+ self.addressCacheStore = configuration.addressCacheStore
self.retryStrategy = retryStrategy
+ self.requestHandler = requestHandler
+ self.responseHandler = responseHandler
- loggerMetadata = ["taskIdentifier": .stringConvertible(taskIdentifier), "name": .string(name)]
+ loggerMetadata = ["name": .string(name)]
super.init(completionQueue: .main, completionHandler: completionHandler)
}
override func cancel() {
- DispatchQueue.main.async {
- super.cancel()
+ super.cancel()
- // Cancel pending retry
+ dispatchQueue.async {
self.retryTimer?.cancel()
+ self.networkTask?.cancel()
+ self.authorizationTask?.cancel()
- // Cancel active network task
- self.sessionTask?.cancel()
+ self.retryTimer = nil
+ self.networkTask = nil
+ self.authorizationTask = nil
}
}
override func main() {
- DispatchQueue.main.async {
- // Finish immediately if operation was cancelled before execution
- guard !self.isCancelled else {
- self.finish(completion: .cancelled)
- return
- }
+ dispatchQueue.async {
+ self.startRequest()
+ }
+ }
+
+ private func startRequest() {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ guard !isCancelled else {
+ finish(completion: .cancelled)
+ return
+ }
+
+ let authorizationResult = requestHandler.requestAuthorization { completion in
+ self.dispatchQueue.async {
+ assert(self.requiresAuthorization, "Illegal use of completion handler.")
+
+ switch completion {
+ case .success(let authorization):
+ self.didReceiveAuthorization(authorization)
+
+ case .failure(let error):
+ self.didFailToRequestAuthorization(error)
- // Get current endpoint
- self.addressCacheStore.getCurrentEndpoint { endpoint in
- DispatchQueue.main.async {
- self.sendRequest(endpoint: endpoint) { [weak self] completion in
- self?.finish(completion: completion)
- }
+ case .cancelled:
+ self.finish(completion: .cancelled)
}
}
}
+
+ switch authorizationResult {
+ case .pending(let task):
+ requiresAuthorization = true
+ authorizationTask = task
+
+ case .noRequirement:
+ requiresAuthorization = false
+ didReceiveAuthorization(nil)
+ }
}
- private func sendRequest(endpoint: AnyIPEndpoint, completionHandler: @escaping CompletionHandler) {
- // Handle operation cancellation
+ private func didReceiveAuthorization(_ authorization: REST.Authorization?) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
guard !isCancelled else {
- completionHandler(.cancelled)
+ finish(completion: .cancelled)
return
}
- // Create network task and execute it
- let taskResult = networkTaskGenerator(endpoint) { [weak self] result in
- DispatchQueue.main.async {
- self?.handleResponse(endpoint: endpoint, result: result, completionHandler: completionHandler)
- }
- }
+ let endpoint = self.addressCacheStore.getCurrentEndpoint()
- switch taskResult {
- case .success(let dataTask):
- logger.debug("Executing request using \(endpoint)", metadata: loggerMetadata)
+ let result = requestHandler.createURLRequest(
+ endpoint: endpoint,
+ authorization: authorization
+ )
- sessionTask = dataTask
- dataTask.resume()
+ switch result {
+ case .success(let request):
+ didReceiveURLRequest(request, endpoint: endpoint)
case .failure(let error):
- logger.error(chainedError: error, message: "Failed to create data task", metadata: loggerMetadata)
-
- completionHandler(.failure(error))
+ didFailToCreateURLRequest(error)
}
}
- private func handleResponse(endpoint: AnyIPEndpoint, result: Result<Success, REST.Error>, completionHandler: @escaping CompletionHandler) {
- guard case .failure(let error) = result else {
- completionHandler(OperationCompletion(result: result))
- return
- }
+ private func didFailToRequestAuthorization(_ error: REST.Error) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ logger.error(
+ chainedError: error,
+ message: "Failed to request authorization.",
+ metadata: loggerMetadata
+ )
- logger.debug("Failed to perform request to \(endpoint)", metadata: self.loggerMetadata)
+ finish(completion: .failure(error))
+ }
+
+ private func didReceiveURLRequest(_ urlRequest: URLRequest, endpoint: AnyIPEndpoint) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ logger.debug(
+ "Executing request using \(endpoint).",
+ metadata: loggerMetadata
+ )
+
+ networkTask = urlSession.dataTask(with: urlRequest) { [weak self] data, response, error in
+ guard let self = self else { return }
- switch Self.evaluateError(error) {
- case .useNextEndpoint:
- // Pick next endpoint in the event of network error
- addressCacheStore.selectNextEndpoint(endpoint) { nextEndpoint in
- DispatchQueue.main.async {
- self.retryRequest(endpoint: nextEndpoint, previousResult: result, completionHandler: completionHandler)
+ self.dispatchQueue.async {
+ if let error = error {
+ let urlError = error as! URLError
+
+ self.didReceiveURLError(urlError, endpoint: endpoint)
+ } else {
+ let httpResponse = response as! HTTPURLResponse
+ let data = data ?? Data()
+
+ self.didReceiveURLResponse(httpResponse, data: data, endpoint: endpoint)
}
}
+ }
- case .useCurrentEndpoint:
- // Retry request using the same endpoint otherwise
- retryRequest(endpoint: endpoint, previousResult: result, completionHandler: completionHandler)
+ networkTask?.resume()
+ }
- case .failImmediately:
- // Fail immediately in case of other errors, like server errors
- completionHandler(OperationCompletion(result: result))
- }
+ private func didFailToCreateURLRequest(_ error: REST.Error) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ logger.error(
+ chainedError: error,
+ message: "Failed to create URLRequest.",
+ metadata: loggerMetadata
+ )
+
+ finish(completion: .failure(error))
}
- private func retryRequest(endpoint: AnyIPEndpoint, previousResult: Result<Success, REST.Error>, completionHandler: @escaping CompletionHandler) {
- // Handle operation cancellation
- guard !isCancelled else {
- completionHandler(.cancelled)
+ private func didReceiveURLError(_ urlError: URLError, endpoint: AnyIPEndpoint) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ switch urlError.code {
+ case .cancelled:
+ finish(completion: .cancelled)
return
+
+ case .notConnectedToInternet, .internationalRoamingOff, .callIsActive:
+ break
+
+ default:
+ _ = addressCacheStore.selectNextEndpoint(endpoint)
}
- // Increment retry count
- retryCount += 1
+ logger.error(
+ chainedError: AnyChainedError(urlError),
+ message: "Failed to perform request to \(endpoint).",
+ metadata: loggerMetadata
+ )
// Check if retry count is not exceeded.
guard retryCount < retryStrategy.maxRetryCount else {
- logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))", metadata: loggerMetadata)
+ if retryStrategy.maxRetryCount > 0 {
+ logger.debug(
+ "Ran out of retry attempts (\(retryStrategy.maxRetryCount))",
+ metadata: loggerMetadata
+ )
+ }
- completionHandler(OperationCompletion(result: previousResult))
+ finish(completion: OperationCompletion(result: .failure(.network(urlError))))
return
}
- // Retry immediatly if retry delay is set to .never
+ // Increment retry count.
+ retryCount += 1
+
+ // Retry immediatly if retry delay is set to never.
guard retryStrategy.retryDelay != .never else {
- sendRequest(endpoint: endpoint, completionHandler: completionHandler)
+ startRequest()
return
}
- // Create timer to delay retry
- retryTimer = DispatchSource.makeTimerSource(queue: .main)
+ // Create timer to delay retry.
+ let timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
- retryTimer?.setEventHandler { [weak self] in
- self?.sendRequest(endpoint: endpoint, completionHandler: completionHandler)
+ timer.setEventHandler { [weak self] in
+ self?.startRequest()
}
- retryTimer?.setCancelHandler {
- completionHandler(.cancelled)
+ timer.setCancelHandler { [weak self] in
+ self?.finish(completion: .cancelled)
}
- retryTimer?.schedule(wallDeadline: .now() + retryStrategy.retryDelay)
- retryTimer?.activate()
+ timer.schedule(wallDeadline: .now() + retryStrategy.retryDelay)
+ timer.activate()
+
+ retryTimer = timer
}
- private static func evaluateError(_ error: REST.Error) -> RetryAction {
- guard case .network(let networkError) = error else {
- return .failImmediately
- }
+ private func didReceiveURLResponse(_ response: HTTPURLResponse, data: Data, endpoint: AnyIPEndpoint) {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
- switch networkError.code {
- case .cancelled:
- return .failImmediately
+ let result = responseHandler.handleURLResponse(response, data: data)
- case .notConnectedToInternet, .internationalRoamingOff, .callIsActive:
- return .useCurrentEndpoint
-
- default:
- return .useNextEndpoint
+ if case .server(.invalidAccessToken) = result.error,
+ requiresAuthorization, retryInvalidAccessTokenError
+ {
+ logger.debug(
+ "Received invalid access token error. Retry once.",
+ metadata: loggerMetadata
+ )
+ retryInvalidAccessTokenError = false
+ startRequest()
+ } else {
+ finish(completion: OperationCompletion(result: result))
}
}
}
diff --git a/ios/MullvadVPN/REST/RESTProxy.swift b/ios/MullvadVPN/REST/RESTProxy.swift
new file mode 100644
index 0000000000..611f4d9c34
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTProxy.swift
@@ -0,0 +1,91 @@
+//
+// RESTProxy.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ class Proxy<ConfigurationType: ProxyConfiguration> {
+ typealias CompletionHandler<Success> = (OperationCompletion<Success, REST.Error>) -> Void
+
+ /// Synchronization queue used by network operations.
+ let dispatchQueue: DispatchQueue
+
+ /// Operation queue used for running network operations.
+ let operationQueue = OperationQueue()
+
+ /// Proxy configuration.
+ let configuration: ConfigurationType
+
+ /// URL request factory.
+ let requestFactory: REST.RequestFactory
+
+ /// URL response decoder.
+ let responseDecoder: REST.ResponseDecoder
+
+ init(
+ name: String,
+ configuration: ConfigurationType,
+ requestFactory: REST.RequestFactory,
+ responseDecoder: REST.ResponseDecoder
+ )
+ {
+ dispatchQueue = DispatchQueue(label: "REST.\(name).dispatchQueue")
+ operationQueue.name = "REST.\(name).operationQueue"
+
+ self.configuration = configuration
+ self.requestFactory = requestFactory
+ self.responseDecoder = responseDecoder
+ }
+
+ func addOperation<Success>(
+ name: String,
+ retryStrategy: REST.RetryStrategy,
+ requestHandler: REST.AnyRequestHandler,
+ responseHandler: REST.AnyResponseHandler<Success>,
+ completionHandler: @escaping NetworkOperation<Success>.CompletionHandler
+ ) -> Cancellable
+ {
+ let operation = NetworkOperation(
+ name: getTaskIdentifier(name: name),
+ dispatchQueue: dispatchQueue,
+ configuration: configuration,
+ retryStrategy: retryStrategy,
+ requestHandler: requestHandler,
+ responseHandler: responseHandler,
+ completionHandler: completionHandler
+ )
+
+ operationQueue.addOperation(operation)
+
+ return operation
+ }
+ }
+
+ class ProxyConfiguration {
+ let session: URLSession
+ let addressCacheStore: AddressCache.Store
+
+ init(session: URLSession, addressCacheStore: AddressCache.Store) {
+ self.session = session
+ self.addressCacheStore = addressCacheStore
+ }
+ }
+
+ class AuthProxyConfiguration: ProxyConfiguration {
+ let accessTokenManager: AccessTokenManager
+
+ init(proxyConfiguration: ProxyConfiguration, accessTokenManager: AccessTokenManager) {
+ self.accessTokenManager = accessTokenManager
+
+ super.init(
+ session: proxyConfiguration.session,
+ addressCacheStore: proxyConfiguration.addressCacheStore
+ )
+ }
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTProxyFactory.swift b/ios/MullvadVPN/REST/RESTProxyFactory.swift
new file mode 100644
index 0000000000..34eeee6776
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTProxyFactory.swift
@@ -0,0 +1,51 @@
+//
+// RESTProxyFactory.swift
+// MullvadVPN
+//
+// Created by pronebird on 19/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ class ProxyFactory {
+ let configuration: AuthProxyConfiguration
+
+ static let shared: ProxyFactory = {
+ let basicConfiguration = ProxyConfiguration(
+ session: REST.sharedURLSession,
+ addressCacheStore: AddressCache.Store.shared
+ )
+
+ let authenticationProxy = REST.AuthenticationProxy(
+ configuration: basicConfiguration
+ )
+ let accessTokenManager = AccessTokenManager(
+ authenticationProxy: authenticationProxy
+ )
+
+ let authConfiguration = AuthProxyConfiguration(
+ proxyConfiguration: basicConfiguration,
+ accessTokenManager: accessTokenManager
+ )
+ return ProxyFactory(configuration: authConfiguration)
+ }()
+
+ init(configuration: AuthProxyConfiguration) {
+ self.configuration = configuration
+ }
+
+ func createAPIProxy() -> REST.APIProxy {
+ return REST.APIProxy(configuration: configuration)
+ }
+
+ func createAccountsProxy() -> REST.AccountsProxy {
+ return REST.AccountsProxy(configuration: configuration)
+ }
+
+ func createDevicesProxy() -> REST.DevicesProxy {
+ return REST.DevicesProxy(configuration: configuration)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTRequestFactory.swift b/ios/MullvadVPN/REST/RESTRequestFactory.swift
new file mode 100644
index 0000000000..064af103fd
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTRequestFactory.swift
@@ -0,0 +1,118 @@
+//
+// RESTRequestFactory.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ class RequestFactory {
+ let hostname: String
+ let pathPrefix: String
+ let networkTimeout: TimeInterval
+ let bodyEncoder: JSONEncoder
+
+ class func withDefaultAPICredentials(pathPrefix: String, bodyEncoder: JSONEncoder) -> RequestFactory {
+ return RequestFactory(
+ hostname: ApplicationConfiguration.defaultAPIHostname,
+ pathPrefix: pathPrefix,
+ networkTimeout: ApplicationConfiguration.defaultAPINetworkTimeout,
+ bodyEncoder: bodyEncoder
+ )
+ }
+
+ init(
+ hostname: String,
+ pathPrefix: String,
+ networkTimeout: TimeInterval,
+ bodyEncoder: JSONEncoder
+ )
+ {
+ self.hostname = hostname
+ self.pathPrefix = pathPrefix
+ self.networkTimeout = networkTimeout
+ self.bodyEncoder = bodyEncoder
+ }
+
+ func createURLRequest(endpoint: AnyIPEndpoint, method: HTTPMethod, path: String) -> URLRequest {
+ var urlComponents = URLComponents()
+ urlComponents.scheme = "https"
+ urlComponents.path = pathPrefix
+ urlComponents.host = "\(endpoint.ip)"
+ urlComponents.port = Int(endpoint.port)
+
+ let requestURL = urlComponents.url!.appendingPathComponent(path)
+
+ var request = URLRequest(
+ url: requestURL,
+ cachePolicy: .useProtocolCachePolicy,
+ timeoutInterval: networkTimeout
+ )
+ request.httpShouldHandleCookies = false
+ request.addValue(hostname, forHTTPHeaderField: HTTPHeader.host)
+ request.addValue("application/json", forHTTPHeaderField: HTTPHeader.contentType)
+ request.httpMethod = method.rawValue
+ return request
+ }
+
+ func createURLRequestBuilder(
+ endpoint: AnyIPEndpoint,
+ method: HTTPMethod,
+ path: String
+ ) -> RequestBuilder {
+ let request = createURLRequest(
+ endpoint: endpoint,
+ method: method,
+ path: path
+ )
+
+ return RequestBuilder(
+ request: request,
+ bodyEncoder: bodyEncoder
+ )
+ }
+ }
+
+ struct RequestBuilder {
+ private var request: URLRequest
+ private let bodyEncoder: JSONEncoder
+
+ init(request: URLRequest, bodyEncoder: JSONEncoder) {
+ self.request = request
+ self.bodyEncoder = bodyEncoder
+ }
+
+ mutating func setHTTPBody<T: Encodable>(value: T) throws {
+ request.httpBody = try bodyEncoder.encode(value)
+ }
+
+ mutating func setETagHeader(etag: String) {
+ var etag = etag
+ // Enforce weak validator to account for some backend caching quirks.
+ if etag.starts(with: "\"") {
+ etag.insert(contentsOf: "W/", at: etag.startIndex)
+ }
+ request.setValue(etag, forHTTPHeaderField: HTTPHeader.ifNoneMatch)
+ }
+
+ mutating func setAuthorization(_ authorization: REST.Authorization) {
+ let value: String
+ switch authorization {
+ case .accountNumber(let accountNumber):
+ value = "Token \(accountNumber)"
+
+ case .accessToken(let accessToken):
+ value = "Bearer \(accessToken)"
+ }
+
+ request.addValue(value, forHTTPHeaderField: HTTPHeader.authorization)
+ }
+
+ func getURLRequest() -> URLRequest {
+ return request
+ }
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTRequestHandler.swift b/ios/MullvadVPN/REST/RESTRequestHandler.swift
new file mode 100644
index 0000000000..d0f7b54dc3
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTRequestHandler.swift
@@ -0,0 +1,67 @@
+//
+// RESTRequestHandler.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol RESTRequestHandler {
+ typealias AuthorizationCompletion = (OperationCompletion<REST.Authorization, REST.Error>) -> Void
+
+ func createURLRequest(endpoint: AnyIPEndpoint, authorization: REST.Authorization?) -> Result<URLRequest, REST.Error>
+ func requestAuthorization(completion: @escaping AuthorizationCompletion) -> REST.AuthorizationResult
+}
+
+extension REST {
+
+ enum AuthorizationResult {
+ /// There is no requirement for authorizing this request.
+ case noRequirement
+
+ /// Authorization request is initiated.
+ /// Associated value contains a handle that can be used to cancel
+ /// the request.
+ case pending(Cancellable)
+ }
+
+ final class AnyRequestHandler: RESTRequestHandler {
+ private let _createURLRequest: (AnyIPEndpoint, REST.Authorization?) -> Result<URLRequest, REST.Error>
+ private let _requestAuthorization: ((@escaping AuthorizationCompletion) -> AuthorizationResult)?
+
+ init(createURLRequest: @escaping (AnyIPEndpoint) -> Result<URLRequest, REST.Error>) {
+ _createURLRequest = { endpoint, authorization in
+ createURLRequest(endpoint)
+ }
+ _requestAuthorization = nil
+ }
+
+ init(
+ createURLRequest: @escaping (AnyIPEndpoint, REST.Authorization) -> Result<URLRequest, REST.Error>,
+ requestAuthorization: @escaping (@escaping AuthorizationCompletion) -> Cancellable
+ ) {
+ _createURLRequest = { endpoint, authorization in
+ return createURLRequest(endpoint, authorization!)
+ }
+ _requestAuthorization = { completion in
+ return .pending(requestAuthorization(completion))
+ }
+ }
+
+ func createURLRequest(
+ endpoint: AnyIPEndpoint,
+ authorization: REST.Authorization?
+ ) -> Result<URLRequest, REST.Error> {
+ return _createURLRequest(endpoint, authorization)
+ }
+
+ func requestAuthorization(
+ completion: @escaping (OperationCompletion<REST.Authorization, REST.Error>) -> Void
+ ) -> REST.AuthorizationResult {
+ return _requestAuthorization?(completion) ?? .noRequirement
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/REST/RESTResponseDecoder.swift b/ios/MullvadVPN/REST/RESTResponseDecoder.swift
new file mode 100644
index 0000000000..2e79bbc9e3
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTResponseDecoder.swift
@@ -0,0 +1,46 @@
+//
+// RESTResponseDecoder.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ struct ResponseDecoder {
+ let decoder: JSONDecoder
+
+ init(decoder: JSONDecoder) {
+ self.decoder = decoder
+ }
+
+ // Parse JSON response into the given `Decodable` type.
+ func decodeSuccessResponse<T: Decodable>(_ type: T.Type, from data: Data) -> Result<T, REST.Error> {
+ return Result { try decoder.decode(type, from: data) }
+ .mapError { error in
+ return .decodeSuccessResponse(error)
+ }
+ }
+
+ /// Parse server error response from JSON.
+ func decodeErrorResponse(from data: Data) -> Result<REST.ServerErrorResponse, REST.Error> {
+ return Result { () -> REST.ServerErrorResponse in
+ return try decoder.decode(REST.ServerErrorResponse.self, from: data)
+ }
+ .mapError { error in
+ return .decodeErrorResponse(error)
+ }
+ }
+
+ /// Parse server error response from JSON and map it to `RESTError.server` error kind.
+ func decodeErrorResponseAndMapToServerError<T>(from data: Data) -> Result<T, REST.Error> {
+ return decodeErrorResponse(from: data)
+ .flatMap { serverError in
+ return .failure(.server(serverError))
+ }
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/REST/RESTResponseHandler.swift b/ios/MullvadVPN/REST/RESTResponseHandler.swift
new file mode 100644
index 0000000000..65ae4b6e2d
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTResponseHandler.swift
@@ -0,0 +1,44 @@
+//
+// RESTResponseHandler.swift
+// MullvadVPN
+//
+// Created by pronebird on 25/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol RESTResponseHandler {
+ associatedtype Success
+
+ func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error>
+}
+
+extension REST {
+ final class AnyResponseHandler<Success>: RESTResponseHandler {
+ typealias HandlerBlock = (HTTPURLResponse, Data) -> Result<Success, REST.Error>
+
+ private let handlerBlock: HandlerBlock
+
+ init(_ block: @escaping HandlerBlock) {
+ handlerBlock = block
+ }
+
+ func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error> {
+ return handlerBlock(response, data)
+ }
+ }
+
+ /// Returns default response handler that parses JSON response into the
+ /// given `Decodable` type when it encounters HTTP `2xx` code, otherwise
+ /// attempts to decode the server error.
+ static func defaultResponseHandler<T: Decodable>(decoding type: T.Type, with decoder: REST.ResponseDecoder) -> AnyResponseHandler<T> {
+ return AnyResponseHandler { response, data in
+ if HTTPStatus.isSuccess(response.statusCode) {
+ return decoder.decodeSuccessResponse(type, from: data)
+ } else {
+ return decoder.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTTaskIdentifier.swift b/ios/MullvadVPN/REST/RESTTaskIdentifier.swift
new file mode 100644
index 0000000000..412475bcbd
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTTaskIdentifier.swift
@@ -0,0 +1,25 @@
+//
+// RESTTaskIdentifier.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ private static let nslock = NSLock()
+ private static var taskCount: UInt32 = 0
+
+ static func getTaskIdentifier(name: String) -> String {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ let (partialValue, isOverflow) = taskCount.addingReportingOverflow(1)
+ let nextValue = isOverflow ? 1 : partialValue
+ taskCount = nextValue
+
+ return "\(name).\(nextValue)"
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTURLSession.swift b/ios/MullvadVPN/REST/RESTURLSession.swift
new file mode 100644
index 0000000000..1db1de2841
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTURLSession.swift
@@ -0,0 +1,30 @@
+//
+// RESTURLSession.swift
+// MullvadVPN
+//
+// Created by pronebird on 18/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ static let sharedURLSession: URLSession = {
+ let certificatePath = Bundle.main.path(forResource: "le_root_cert", ofType: "cer")!
+ let data = FileManager.default.contents(atPath: certificatePath)!
+ let secCertificate = SecCertificateCreateWithData(nil, data as CFData)!
+
+ let sessionDelegate = SSLPinningURLSessionDelegate(
+ sslHostname: ApplicationConfiguration.defaultAPIHostname,
+ trustedRootCertificates: [secCertificate]
+ )
+
+ let session = URLSession(
+ configuration: .ephemeral,
+ delegate: sessionDelegate,
+ delegateQueue: nil
+ )
+
+ return session
+ }()
+}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index 9f7b9c0fc4..61614e92a9 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -101,7 +101,7 @@ extension RelayCache {
func updateRelays(completionHandler: @escaping (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void) -> Cancellable {
let operation = UpdateRelaysOperation(
dispatchQueue: stateQueue,
- restClient: REST.Client.shared,
+ apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
cacheFileURL: self.cacheFileURL,
relayUpdateInterval: Self.relayUpdateInterval,
updateHandler: { [weak self] newCachedRelays in
@@ -302,7 +302,7 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
typealias UpdateHandler = (RelayCache.CachedRelays) -> Void
private let dispatchQueue: DispatchQueue
- private let restClient: REST.Client
+ private let apiProxy: REST.APIProxy
private let cacheFileURL: URL
private let relayUpdateInterval: TimeInterval
@@ -312,13 +312,13 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
private var downloadCancellable: Cancellable?
init(dispatchQueue: DispatchQueue,
- restClient: REST.Client,
+ apiProxy: REST.APIProxy,
cacheFileURL: URL,
relayUpdateInterval: TimeInterval,
updateHandler: @escaping UpdateHandler,
completionHandler: @escaping CompletionHandler) {
self.dispatchQueue = dispatchQueue
- self.restClient = restClient
+ self.apiProxy = apiProxy
self.cacheFileURL = cacheFileURL
self.relayUpdateInterval = relayUpdateInterval
self.updateHandler = updateHandler
@@ -411,7 +411,7 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
}
private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) {
- downloadCancellable = REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] result in
+ downloadCancellable = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] result in
guard let self = self else { return }
self.dispatchQueue.async {
diff --git a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
index ceca68b708..70fb221cdb 100644
--- a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
@@ -13,25 +13,24 @@ class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunn
private let queue: DispatchQueue
private let state: TunnelManager.State
- private let restClient: REST.Client
+ private let apiProxy: REST.APIProxy
private var restRequest: Cancellable?
private let rotationInterval: TimeInterval?
- private var completionHandler: CompletionHandler?
private let logger = Logger(label: "TunnelManager.ReplaceKeyOperation")
class func operationForKeyRotation(
queue: DispatchQueue,
state: TunnelManager.State,
- restClient: REST.Client,
+ apiProxy: REST.APIProxy,
rotationInterval: TimeInterval,
completionHandler: @escaping CompletionHandler
) -> ReplaceKeyOperation {
return ReplaceKeyOperation(
queue: queue,
state: state,
- restClient: restClient,
+ apiProxy: apiProxy,
rotationInterval: rotationInterval,
completionHandler: completionHandler
)
@@ -40,13 +39,13 @@ class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunn
class func operationForKeyRegeneration(
queue: DispatchQueue,
state: TunnelManager.State,
- restClient: REST.Client,
+ apiProxy: REST.APIProxy,
completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void
) -> ReplaceKeyOperation {
return ReplaceKeyOperation(
queue: queue,
state: state,
- restClient: restClient,
+ apiProxy: apiProxy,
rotationInterval: nil
) { completion in
let mappedCompletion = completion.map { keyRotationResult -> () in
@@ -65,14 +64,14 @@ class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunn
private init(
queue: DispatchQueue,
state: TunnelManager.State,
- restClient: REST.Client,
+ apiProxy: REST.APIProxy,
rotationInterval: TimeInterval?,
completionHandler: @escaping CompletionHandler
) {
self.queue = queue
self.state = state
- self.restClient = restClient
+ self.apiProxy = apiProxy
self.rotationInterval = rotationInterval
super.init(completionQueue: queue, completionHandler: completionHandler)
@@ -153,8 +152,8 @@ class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunn
logger.debug("Replacing old key with new key on server...")
- restRequest = self.restClient.replaceWireguardKey(
- token: tunnelInfo.token,
+ restRequest = self.apiProxy.replaceWireguardKey(
+ accountNumber: tunnelInfo.token,
oldPublicKey: oldPublicKey,
newPublicKey: newPrivateKey.publicKey,
retryStrategy: .default
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 88f7fde065..6952a4130e 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -15,16 +15,24 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
private let queue: DispatchQueue
private let state: TunnelManager.State
- private let restClient: REST.Client
+ private let apiProxy: REST.APIProxy
private let accountToken: String?
private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler?
private let logger = Logger(label: "TunnelManager.SetAccountOperation")
- init(queue: DispatchQueue, state: TunnelManager.State, restClient: REST.Client, accountToken: String?, willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler, completionHandler: @escaping CompletionHandler) {
+ init(
+ queue: DispatchQueue,
+ state: TunnelManager.State,
+ apiProxy: REST.APIProxy,
+ accountToken: String?,
+ willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler,
+ completionHandler: @escaping CompletionHandler
+ )
+ {
self.queue = queue
self.state = state
- self.restClient = restClient
+ self.apiProxy = apiProxy
self.accountToken = accountToken
self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler
@@ -144,7 +152,7 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
for (index, publicKey) in publicKeys.enumerated() {
dispatchGroup.enter()
- _ = REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
+ _ = apiProxy.deleteWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
self.queue.async {
switch result {
case .success:
@@ -217,7 +225,7 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
}
private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) {
- _ = restClient.pushWireguardKey(token: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
+ _ = apiProxy.pushWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
self.queue.async {
switch result {
case .success(let associatedAddresses):
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 6f32182042..76f7c0945b 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -47,12 +47,12 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
static let shared: TunnelManager = {
- return TunnelManager(restClient: REST.Client.shared)
+ return TunnelManager(apiProxy: REST.ProxyFactory.shared.createAPIProxy())
}()
// MARK: - Internal variables
- private let restClient: REST.Client
+ private let apiProxy: REST.APIProxy
private let logger = Logger(label: "TunnelManager")
private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue")
@@ -80,8 +80,8 @@ final class TunnelManager: TunnelManagerStateDelegate {
return state.tunnelStatus.state
}
- private init(restClient: REST.Client) {
- self.restClient = restClient
+ private init(apiProxy: REST.APIProxy) {
+ self.apiProxy = apiProxy
self.state = TunnelManager.State(queue: stateQueue)
self.state.delegate = self
@@ -350,7 +350,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
- let operation = ReplaceKeyOperation.operationForKeyRegeneration(queue: stateQueue, state: state, restClient: restClient) { [weak self] completion in
+ let operation = ReplaceKeyOperation.operationForKeyRegeneration(queue: stateQueue, state: state, apiProxy: apiProxy) { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -389,7 +389,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
let operation = ReplaceKeyOperation.operationForKeyRotation(
queue: stateQueue,
state: state,
- restClient: restClient,
+ apiProxy: apiProxy,
rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval
) { [weak self] completion in
guard let self = self else { return }
@@ -573,7 +573,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
return SetAccountOperation(
queue: stateQueue,
state: state,
- restClient: restClient,
+ apiProxy: apiProxy,
accountToken: accountToken,
willDeleteVPNConfigurationHandler: { [weak self] in
guard let self = self else { return }
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index 1c839a0c54..d53739edae 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -47,6 +47,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
return .lightContent
}
+ private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+
override func viewDidLoad() {
super.viewDidLoad()
@@ -213,8 +215,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
verifyKeyCancellable?.cancel()
- verifyKeyCancellable = REST.Client.shared.getWireguardKey(
- token: tunnelInfo.token,
+ verifyKeyCancellable = apiProxy.getWireguardKey(
+ accountNumber: tunnelInfo.token,
publicKey: tunnelInfo.tunnelSettings.interface.publicKey,
retryStrategy: .default
) { [weak self] result in
diff --git a/ios/MullvadVPN/en.lproj/RESTClient.strings b/ios/MullvadVPN/en.lproj/REST.strings
index f5a83c67a3..5c5a809eda 100644
--- a/ios/MullvadVPN/en.lproj/RESTClient.strings
+++ b/ios/MullvadVPN/en.lproj/REST.strings
@@ -21,3 +21,6 @@
/* Failure to decode the server success response. */
"SERVER_SUCCESS_RESPONSE_DECODING_ERROR" = "Server success response decoding error";
+
+/* Use %@ placeholder to place the error code into the localized string. */
+"UNKNOWN_ERROR_DESCRIPTION" = "Unknown error: %@";