summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-09-15 13:03:10 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-09-16 10:44:16 +0200
commit1380f3f23c666e2cf0d9fd1e65d79246e9c90864 (patch)
treeacbd5c31ab8557069ecaf950df0f8e7189334c3a
parent8eb1502af70f6496dcf4641a3f388756c61f0d83 (diff)
downloadmullvadvpn-1380f3f23c666e2cf0d9fd1e65d79246e9c90864.tar.xz
mullvadvpn-1380f3f23c666e2cf0d9fd1e65d79246e9c90864.zip
REST: refactor
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj72
-rw-r--r--ios/MullvadVPN/Account.swift4
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift12
-rw-r--r--ios/MullvadVPN/LocationDataSource.swift2
-rw-r--r--ios/MullvadVPN/LoginViewController.swift4
-rw-r--r--ios/MullvadVPN/MullvadRest.swift830
-rw-r--r--ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift2
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift30
-rw-r--r--ios/MullvadVPN/REST/HTTP.swift73
-rw-r--r--ios/MullvadVPN/REST/REST.swift12
-rw-r--r--ios/MullvadVPN/REST/RESTClient.swift409
-rw-r--r--ios/MullvadVPN/REST/RESTCoding.swift33
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift112
-rw-r--r--ios/MullvadVPN/REST/SSLPinningURLSessionDelegate.swift (renamed from ios/MullvadVPN/SSLPinningURLSessionDelegate.swift)3
-rw-r--r--ios/MullvadVPN/REST/ServerRelaysResponse.swift45
-rw-r--r--ios/MullvadVPN/RelaySelector.swift10
-rw-r--r--ios/MullvadVPN/en.lproj/RESTClient.strings (renamed from ios/MullvadVPN/en.lproj/MullvadRest.strings)0
-rw-r--r--ios/MullvadVPNTests/RelaySelectorTests.swift18
18 files changed, 773 insertions, 898 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index b478a6570d..d3cf645deb 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -28,7 +28,13 @@
581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; };
5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */; };
5823FA5026CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; };
+ 5820674E26E6510200655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
+ 5820675026E6514100655B05 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674F26E6514100655B05 /* HTTP.swift */; };
+ 5820675626E6528A00655B05 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; };
+ 5820675726E652A600655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
+ 5820675926E652BE00655B05 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; };
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; };
+ 5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; };
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; };
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; };
@@ -61,8 +67,6 @@
584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; };
584789BF264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; };
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
- 584789E126529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
- 584789E626529DEF000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
584789EC2652A1A2000E45FB /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 584789EB2652A1A2000E45FB /* Logging */; };
584E96BC240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; };
584E96BD240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; };
@@ -82,6 +86,12 @@
585834F824D2BC1F00A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834F724D2BC1F00A8AF56 /* Logging */; };
585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; };
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; };
+ 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
+ 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
+ 585DA88726B0277200B8C587 /* RESTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88626B0277200B8C587 /* RESTError.swift */; };
+ 585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88926B027A300B8C587 /* RESTCoding.swift */; };
+ 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
+ 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
585DA8AF26B9492500B8C587 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; };
5860392726D91B8400554C79 /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; };
5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; };
@@ -114,7 +124,6 @@
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; };
5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; };
588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588534BD246193C00018B744 /* AutomaticKeyRotationManager.swift */; };
- 58871D1825D5359B002297FA /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; };
58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; };
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
@@ -175,8 +184,7 @@
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; };
58CAF4EA26025927007C5886 /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; };
58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; };
- 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; };
- 58CB0EE124B86751001EF0D8 /* MullvadRest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */; };
+ 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */; };
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
58CC40F024A602780019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* ConnectViewController.swift */; };
@@ -223,7 +231,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 /* MullvadRest.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558EA2695D26A00F630D0 /* MullvadRest.strings */; };
+ 58F558EC2695D26A00F630D0 /* RESTClient.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558EA2695D26A00F630D0 /* RESTClient.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 */; };
@@ -320,6 +328,8 @@
581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewDataSource.swift; sourceTree = "<group>"; };
581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; };
5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+BackgroundTask.swift"; sourceTree = "<group>"; };
+ 5820674D26E6510200655B05 /* REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
+ 5820674F26E6514100655B05 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; };
5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; };
5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; };
@@ -352,6 +362,9 @@
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; };
5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNavigationController.swift; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
+ 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRelaysResponse.swift; sourceTree = "<group>"; };
+ 585DA88626B0277200B8C587 /* RESTError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTError.swift; sourceTree = "<group>"; };
+ 585DA88926B027A300B8C587 /* RESTCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTCoding.swift; sourceTree = "<group>"; };
585DA8AE26B9492500B8C587 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
5860392826DCE7AB00554C79 /* PromiseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseCompletion.swift; sourceTree = "<group>"; };
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; };
@@ -413,7 +426,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 /* MullvadRest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadRest.swift; sourceTree = "<group>"; };
+ 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTClient.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>"; };
@@ -453,7 +466,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/MullvadRest.strings; sourceTree = "<group>"; };
+ 58F558EB2695D26A00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/RESTClient.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>"; };
@@ -569,7 +582,7 @@
587B7543266922BF00DEF7E9 /* Localizable.strings */,
58F558DE2695BD3E00F630D0 /* Login.strings */,
58F559052697002000F630D0 /* Main.strings */,
- 58F558EA2695D26A00F630D0 /* MullvadRest.strings */,
+ 58F558EA2695D26A00F630D0 /* RESTClient.strings */,
58F558E12695D1D800F630D0 /* Preferences.strings */,
58F558E42695D1F200F630D0 /* ProblemReport.strings */,
58F558ED2695D50D00F630D0 /* ProblemReportReview.strings */,
@@ -583,6 +596,20 @@
path = MullvadVPN;
sourceTree = "<group>";
};
+ 585DA87F26B0268500B8C587 /* REST */ = {
+ isa = PBXGroup;
+ children = (
+ 5820674F26E6514100655B05 /* HTTP.swift */,
+ 5820674D26E6510200655B05 /* REST.swift */,
+ 58CB0EDF24B86751001EF0D8 /* RESTClient.swift */,
+ 585DA88926B027A300B8C587 /* RESTCoding.swift */,
+ 585DA88626B0277200B8C587 /* RESTError.swift */,
+ 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */,
+ 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */,
+ );
+ path = REST;
+ sourceTree = "<group>";
+ };
586ADD4323FC13AD00CE9E87 /* GeoJSON */ = {
isa = PBXGroup;
children = (
@@ -695,7 +722,6 @@
58B993B02608A34500BA7811 /* LoginContentView.swift */,
58CE5E65224146200008646E /* LoginViewController.swift */,
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */,
- 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */,
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */,
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */,
@@ -718,6 +744,7 @@
58BFA5C522A7C97F00A6173D /* RelayCache.swift */,
58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */,
58781CD422AFBA39009B9D8E /* RelaySelector.swift */,
+ 585DA87F26B0268500B8C587 /* REST */,
587425C02299833500CA2045 /* RootContainerViewController.swift */,
5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */,
@@ -732,7 +759,6 @@
587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */,
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */,
58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */,
- 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */,
581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */,
58EF581025D69DB400AEBA94 /* StatusImageView.swift */,
5807E2BF2432038B00F5FF30 /* String+Split.swift */,
@@ -1033,7 +1059,7 @@
58F558FE2696F09100F630D0 /* KeyboardNavigation.strings in Resources */,
58F5590C2697002100F630D0 /* AppDelegate.strings in Resources */,
581FC4FA2695ACE100AA97BA /* Account.strings in Resources */,
- 58F558EC2695D26A00F630D0 /* MullvadRest.strings in Resources */,
+ 58F558EC2695D26A00F630D0 /* RESTClient.strings in Resources */,
582CFEEA269463B80072883A /* Settings.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1073,12 +1099,12 @@
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
- 584789E626529DEF000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
584E96BE240FD4DB00D3334F /* Location.swift in Sources */,
5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */,
5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */,
58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */,
58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */,
+ 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */,
58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */,
58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */,
5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */,
@@ -1095,12 +1121,13 @@
58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */,
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */,
58CAF4EA26025927007C5886 /* PacketTunnelIpc.swift in Sources */,
+ 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */,
+ 5820676226E75D8500655B05 /* REST.swift in Sources */,
5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */,
58E1337B26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */,
5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */,
58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */,
- 58871D1825D5359B002297FA /* MullvadRest.swift in Sources */,
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1125,6 +1152,7 @@
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */,
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */,
+ 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */,
@@ -1133,6 +1161,7 @@
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */,
588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */,
+ 5820675026E6514100655B05 /* HTTP.swift in Sources */,
5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */,
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */,
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
@@ -1140,7 +1169,7 @@
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */,
- 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */,
+ 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */,
5846226A26E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift in Sources */,
58E1337526D2BEC400CC316B /* Promise+Optional.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
@@ -1163,6 +1192,7 @@
5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
+ 5820674E26E6510200655B05 /* REST.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
@@ -1217,6 +1247,7 @@
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */,
587AD7CA2342283900E93A53 /* Account.swift in Sources */,
58F840AF2464382C0044E708 /* KeychainItemRevision.swift in Sources */,
+ 585DA88726B0277200B8C587 /* RESTError.swift in Sources */,
58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
@@ -1233,6 +1264,7 @@
58E1338126D2BF5C00CC316B /* Promise+Result.swift in Sources */,
58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
+ 585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1241,8 +1273,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 58CB0EE124B86751001EF0D8 /* MullvadRest.swift in Sources */,
- 584789E126529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */,
58B93A1826C54D7E00A55733 /* Locking.swift in Sources */,
58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */,
@@ -1253,6 +1283,7 @@
5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */,
580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */,
58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */,
+ 5820675726E652A600655B05 /* REST.swift in Sources */,
58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
58B93A2526C683B300A55733 /* Promise.swift in Sources */,
58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
@@ -1268,6 +1299,7 @@
58CC40F024A602780019D96E /* ObserverList.swift in Sources */,
58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */,
58E1338226D2BF5C00CC316B /* Promise+Result.swift in Sources */,
+ 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
581503A724D6F4AE00C9C50E /* Logging.swift in Sources */,
58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */,
58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */,
@@ -1284,6 +1316,7 @@
58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */,
58F840B32464491D0044E708 /* ChainedError.swift in Sources */,
58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */,
+ 5820675626E6528A00655B05 /* RESTError.swift in Sources */,
58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */,
58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */,
@@ -1291,6 +1324,7 @@
581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */,
58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */,
5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */,
+ 5820675926E652BE00655B05 /* RESTCoding.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1401,12 +1435,12 @@
name = SelectLocation.strings;
sourceTree = "<group>";
};
- 58F558EA2695D26A00F630D0 /* MullvadRest.strings */ = {
+ 58F558EA2695D26A00F630D0 /* RESTClient.strings */ = {
isa = PBXVariantGroup;
children = (
58F558EB2695D26A00F630D0 /* en */,
);
- name = MullvadRest.strings;
+ name = RESTClient.strings;
sourceTree = "<group>";
};
58F558ED2695D50D00F630D0 /* ProblemReportReview.strings */ = {
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 0115bcc7b3..5d3f603d1a 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -54,10 +54,10 @@ class Account {
enum Error: ChainedError {
/// A failure to create the new account token
- case createAccount(RestError)
+ case createAccount(REST.Error)
/// A failure to verify the account token
- case verifyAccount(RestError)
+ case verifyAccount(REST.Error)
/// A failure to configure a tunnel
case tunnelConfiguration(TunnelManager.Error)
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index aeb10a80ef..56f9103cb8 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -13,14 +13,14 @@ protocol DisplayChainedError {
var errorChainDescription: String? { get }
}
-extension RestError: DisplayChainedError {
+extension REST.Error: DisplayChainedError {
var errorChainDescription: String? {
switch self {
case .network(let urlError):
return String(
format: NSLocalizedString(
"NETWORK_ERROR",
- tableName: "MullvadRest",
+ tableName: "RESTClient",
value: "Network error: %@",
comment: "Network error. Use %@ placeholder to place localized failure description."
),
@@ -33,7 +33,7 @@ extension RestError: DisplayChainedError {
return String(
format: NSLocalizedString(
"SERVER_ERROR",
- tableName: "MullvadRest",
+ tableName: "RESTClient",
value: "Server error: %@",
comment: "Server error. Use %@ placeholder to place localized failure description."
),
@@ -43,21 +43,21 @@ extension RestError: DisplayChainedError {
case .encodePayload:
return NSLocalizedString(
"SERVER_REQUEST_ENCODING_ERROR",
- tableName: "MullvadRest",
+ tableName: "RESTClient",
value: "Server request encoding error",
comment: "Failure to encode the server request."
)
case .decodeSuccessResponse:
return NSLocalizedString(
"SERVER_SUCCESS_RESPONSE_DECODING_ERROR",
- tableName: "MullvadRest",
+ tableName: "RESTClient",
value: "Server success response decoding error",
comment: "Failure to decode the server success response."
)
case .decodeErrorResponse:
return NSLocalizedString(
"SERVER_FAILURE_RESPONSE_DECODING_ERROR",
- tableName: "MullvadRest",
+ tableName: "RESTClient",
value: "Server error response decoding error",
comment: "Failure to decode the server failure response."
)
diff --git a/ios/MullvadVPN/LocationDataSource.swift b/ios/MullvadVPN/LocationDataSource.swift
index 9d33f09b3e..6f7d922eb3 100644
--- a/ios/MullvadVPN/LocationDataSource.swift
+++ b/ios/MullvadVPN/LocationDataSource.swift
@@ -79,7 +79,7 @@ class LocationDataSource: NSObject, UITableViewDataSource {
}
- func setRelays(_ response: ServerRelaysResponse) {
+ func setRelays(_ response: REST.ServerRelaysResponse) {
let rootNode = Self.makeRootNode()
var nodeByLocation = [RelayLocation: Node]()
let dataSourceWasEmpty = locationList.isEmpty
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index ef5ce6a83c..56df3fcfb3 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -21,8 +21,8 @@ enum LoginState {
}
protocol LoginViewControllerDelegate: AnyObject {
- func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<AccountResponse, Account.Error>) -> Void)
- func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<AccountResponse, Account.Error>) -> Void)
+ func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void)
+ func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void)
func loginViewControllerDidLogin(_ controller: LoginViewController)
}
diff --git a/ios/MullvadVPN/MullvadRest.swift b/ios/MullvadVPN/MullvadRest.swift
deleted file mode 100644
index 0e7abdc239..0000000000
--- a/ios/MullvadVPN/MullvadRest.swift
+++ /dev/null
@@ -1,830 +0,0 @@
-//
-// MullvadRest.swift
-// MullvadVPN
-//
-// Created by pronebird on 10/07/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Network
-import Security
-import WireGuardKit
-
-/// REST API v1 base URL
-private let kRestBaseURL = URL(string: "https://api.mullvad.net/app/v1")!
-
-/// Network request timeout in seconds
-private let kNetworkTimeout: TimeInterval = 10
-
-/// HTTP method
-enum HttpMethod: String {
- case get = "GET"
- case post = "POST"
- case delete = "DELETE"
-}
-
-// HTTP status codes
-enum HttpStatus {
- static let ok = 200
- static let created = 201
- static let noContent = 204
- static let notModified = 304
-}
-
-/// HTTP headers
-enum HttpHeader {
- static let authorization = "Authorization"
- static let contentType = "Content-Type"
- static let etag = "ETag"
- static let ifNoneMatch = "If-None-Match"
-}
-
-/// A struct that represents a server response in case of error (any HTTP status code except 2xx).
-struct ServerErrorResponse: LocalizedError, Decodable, Equatable {
- /// A list of known server error codes
- enum Code: String, Equatable {
- case invalidAccount = "INVALID_ACCOUNT"
- case keyLimitReached = "KEY_LIMIT_REACHED"
- case pubKeyNotFound = "PUBKEY_NOT_FOUND"
-
- static func ~= (pattern: Self, value: ServerErrorResponse) -> Bool {
- return pattern.rawValue == value.code
- }
- }
-
- static var invalidAccount: Code {
- return .invalidAccount
- }
- static var keyLimitReached: Code {
- return .keyLimitReached
- }
- static var pubKeyNotFound: Code {
- return .pubKeyNotFound
- }
-
- let code: String
- let error: String?
-
- var errorDescription: String? {
- switch code {
- case Code.keyLimitReached.rawValue:
- return NSLocalizedString(
- "KEY_LIMIT_REACHED_ERROR_DESCRIPTION",
- tableName: "MullvadRest",
- value: "Too many WireGuard keys in use.",
- comment: ""
- )
- case Code.invalidAccount.rawValue:
- return NSLocalizedString(
- "INVALID_ACCOUNT_ERROR_DESCRIPTION",
- tableName: "MullvadRest",
- value: "Invalid account.",
- comment: ""
- )
- default:
- return nil
- }
- }
-
- var recoverySuggestion: String? {
- switch code {
- case Code.keyLimitReached.rawValue:
- return NSLocalizedString(
- "KEY_LIMIT_REACHED_ERROR_RECOVERY_SUGGESTION",
- tableName: "MullvadRest",
- value: "Please visit the website to revoke a key before login is possible.",
- comment: ""
- )
- default:
- return nil
- }
- }
-
- static func == (lhs: Self, rhs: Self) -> Bool {
- return lhs.code == rhs.code
- }
-}
-
-/// An error type returned by `MullvadRest`
-enum RestError: ChainedError {
- /// A failure to encode the payload
- case encodePayload(Error)
-
- /// A failure during networking
- case network(URLError)
-
- /// A failure reported by server
- case server(ServerErrorResponse)
-
- /// A failure to decode the error response from server
- case decodeErrorResponse(Error)
-
- /// A failure to decode the success response from server
- case decodeSuccessResponse(Error)
-
- var errorDescription: String? {
- switch self {
- case .encodePayload:
- return "Failure to encode the payload"
- case .network:
- return "Network error"
- case .server:
- return "Server error"
- case .decodeErrorResponse:
- return "Failure to decode error response from server"
- case .decodeSuccessResponse:
- return "Failure to decode success response from server"
- }
- }
-}
-
-/// Types conforming to this protocol can participate in forming the `URLRequest` created by
-/// `RestEndpoint`.
-protocol RestPayload {
- func inject(into request: inout URLRequest) throws
-}
-
-/// Any `Encodable` type can be injected as JSON payload
-extension RestPayload where Self: Encodable {
- func inject(into request: inout URLRequest) throws {
- request.httpBody = try MullvadRest.makeJSONEncoder().encode(self)
- }
-}
-
-// MARK: - Operations
-
-final class RestOperation<Input, Response>: AsyncOperation, InputOperation, OutputOperation
- where Input: RestPayload
-{
- typealias Output = Result<Response, RestError>
-
- private let endpoint: RestEndpoint<Input, Response>
- private let session: URLSession
- private var task: URLSessionTask?
-
- init(endpoint: RestEndpoint<Input, Response>, session: URLSession, input: Input? = nil) {
- self.endpoint = endpoint
- self.session = session
-
- super.init()
- self.input = input
- }
-
- override func main() {
- guard let payload = self.input else {
- finish()
- return
- }
-
- let result = endpoint.dataTask(session: session, payload: payload) { [weak self] (result) in
- self?.finish(with: result)
- }
-
- switch result {
- case .success(let task):
- self.task = task
- task.resume()
- case .failure(let error):
- finish(with: .failure(error))
- }
- }
-
- override func operationDidCancel() {
- task?.cancel()
- task = nil
- }
-}
-
-// MARK: - Response handlers
-
-/// Types conforming to this protocol can be used as response handlers to decode the raw server response to the data
-/// type expected by the caller.
-protocol ResponseHandler {
- associatedtype Response
-
- /// Decode the response.
- /// The implementation is expected to throw `BadResponseError` in case of failure to handle the HTTP response,
- /// or any other `Error` in case of failure to decode the data.
- func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError>
-}
-
-enum ResponseHandlerError: Error {
- /// A failure to handle the response due to unexpected status code
- case badResponse(_ statusCode: Int)
-
- /// A failure to decode data
- case decodeData(Error)
-}
-
-/// A placeholder error used to indicate that the server returned unexpected response.
-fileprivate struct BadResponseError: Error {
- let statusCode: Int
-}
-
-/// Type-erasing response handler.
-struct AnyResponseHandler<Response>: ResponseHandler {
- private let decodeResponseBlock: (HTTPURLResponse, Data) -> Result<Response, ResponseHandlerError>
-
- init<T: ResponseHandler>(_ wrappedHandler: T) where T.Response == Response {
- self.decodeResponseBlock = { (response, data) -> Result<Response, ResponseHandlerError> in
- return wrappedHandler.decodeResponse(response, data: data)
- }
- }
-
- init(block: @escaping (HTTPURLResponse, Data) -> Result<Response, ResponseHandlerError>) {
- self.decodeResponseBlock = block
- }
-
- func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
- return self.decodeResponseBlock(httpResponse, data)
- }
-}
-
-/// A REST response handler that decides when response contains the successful result based on the given status code and
-/// decodes the value in response.
-struct DecodingResponseHandler<Response: Decodable>: ResponseHandler {
- private let expectedStatus: Int
-
- init(expectedStatus: Int) {
- self.expectedStatus = expectedStatus
- }
-
- func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
- if httpResponse.statusCode == expectedStatus {
- return MullvadRest.decodeSuccessResponse(Response.self, from: data)
- } else {
- return .failure(.badResponse(httpResponse.statusCode))
- }
- }
-}
-
-/// A REST response handler that decides when response contains the successful result based on the given status code but
-/// never decodes the value in response as it anticipates it to be empty.
-struct EmptyResponseHandler: ResponseHandler {
- private let expectedStatus: Int
-
- init(expectedStatus: Int) {
- self.expectedStatus = expectedStatus
- }
-
- func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<(), ResponseHandlerError> {
- if httpResponse.statusCode == expectedStatus {
- return .success(())
- } else {
- return .failure(.badResponse(httpResponse.statusCode))
- }
- }
-}
-
-/// A REST response handler that takes into account ETag and 200 and 304 response codes to produce the output result.
-struct HttpCacheDecodingResponseHandler<WrappedType: Decodable>: ResponseHandler {
- typealias Response = HttpResourceCacheResponse<WrappedType>
-
- private let etag: String?
-
- init(etag: String?) {
- self.etag = etag
- }
-
- func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
- switch httpResponse.statusCode {
- case HttpStatus.ok:
- return MullvadRest.decodeSuccessResponse(WrappedType.self, from: data)
- .map { (relays) -> Response in
- let etag = httpResponse.value(forCaseInsensitiveHTTPHeaderField: HttpHeader.etag)
-
- return .newContent(etag, relays)
- }
-
- case HttpStatus.notModified where etag != nil:
- return .success(.notModified)
-
- case let statusCode:
- return .failure(.badResponse(statusCode))
- }
- }
-}
-
-// MARK: - Endpoints
-
-/// A struct that describes the REST endpoint, including the expected input and output
-struct RestEndpoint<Input, Response> where Input: RestPayload {
- let endpointURL: URL
- let httpMethod: HttpMethod
- let makeResponseHandler: (Input) -> AnyResponseHandler<Response>
-
- init<Handler: ResponseHandler>(endpointURL: URL, httpMethod: HttpMethod, responseHandlerFactory: @escaping (Input) -> Handler) where Handler.Response == Response {
- self.endpointURL = endpointURL
- self.httpMethod = httpMethod
- self.makeResponseHandler = { (input) -> AnyResponseHandler<Response> in
- return AnyResponseHandler(responseHandlerFactory(input))
- }
- }
-
- /// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the
- /// expected response type or error upon completion.
- func dataTask(session: URLSession, payload: Input, completionHandler: @escaping (Result<Response, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
- return makeURLRequest(payload: payload).map { (request) -> URLSessionDataTask in
- return session.dataTask(with: request) { (responseData, urlResponse, error) in
- let handler = self.makeResponseHandler(payload)
- let result = Self.handleURLResponse(urlResponse, data: responseData, error: error, responseHandler: handler)
- completionHandler(result)
- }
- }
- }
-
- /// Create `RestOperation` that automatically parses the response and sets the expected output
- /// type or error upon completion.
- func operation(session: URLSession, payload: Input?) -> RestOperation<Input, Response> {
- return RestOperation(endpoint: self, session: session, input: payload)
- }
-
- /// Create `URLRequest` that can be used to send an HTTP request
- private func makeURLRequest(payload: Input) -> Result<URLRequest, RestError> {
- var request = makeEndpointURLRequest()
- do {
- try payload.inject(into: &request)
-
- return .success(request)
- } catch {
- return .failure(.encodePayload(error))
- }
- }
-
- /// Create a boilerplate `URLRequest` before injecting the payload
- private func makeEndpointURLRequest() -> URLRequest {
- var request = URLRequest(
- url: endpointURL,
- cachePolicy: .useProtocolCachePolicy,
- timeoutInterval: kNetworkTimeout
- )
- request.httpShouldHandleCookies = false
- request.addValue("application/json", forHTTPHeaderField: HttpHeader.contentType)
- request.httpMethod = httpMethod.rawValue
- return request
- }
-
- /// A private HTTP response handler
- private static func handleURLResponse(_ urlResponse: URLResponse?, data: Data?, error: Error?, responseHandler: AnyResponseHandler<Response>) -> Result<Response, RestError> {
- if let error = error {
- let networkError = error as? URLError ?? URLError(.unknown)
-
- return .failure(.network(networkError))
- }
-
- guard let httpResponse = urlResponse as? HTTPURLResponse else {
- return .failure(.network(URLError(.unknown)))
- }
-
- let data = data ?? Data()
-
- return responseHandler.decodeResponse(httpResponse, data: data)
- .flatMapError { (error) -> Result<Response, RestError> in
- switch error {
- case .badResponse:
- // Try decoding the server error response in case when unexpected response is returned
- return MullvadRest.decodeErrorResponse(httpResponse: httpResponse, data: data)
- .flatMap { (serverErrorResponse) -> Result<Response, RestError> in
- return .failure(.server(serverErrorResponse))
- }
-
- case .decodeData(let decodingError):
- return .failure(.decodeSuccessResponse(decodingError))
- }
- }
- }
-}
-
-/// A convenience class for `RestEndpoint` that transparently provides it with the `URLSession`
-struct RestSessionEndpoint<Input, Response> where Input: RestPayload {
- let session: URLSession
- let endpoint: RestEndpoint<Input, Response>
-
- init(session: URLSession, endpoint: RestEndpoint<Input, Response>) {
- self.session = session
- self.endpoint = endpoint
- }
-
- /// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the
- /// expected response type or error upon completion.
- func dataTask(payload: Input, completionHandler: @escaping (Result<Response, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
- return endpoint.dataTask(session: session, payload: payload, completionHandler: completionHandler)
- }
-
- /// Create `RestOperation` that automatically parses the response and sets the expected output
- /// type or error upon completion.
- func operation(payload: Input?) -> RestOperation<Input, Response> {
- return endpoint.operation(session: session, payload: payload)
- }
-}
-
-// MARK: - REST interface
-
-class MullvadRest {
- let session: URLSession
-
- private let sessionDelegate: SSLPinningURLSessionDelegate
-
- /// Returns array of trusted root certificates
- private static var trustedRootCertificates: [SecCertificate] {
- let oldRootCertificate = Bundle.main.path(forResource: "old_le_root_cert", ofType: "cer")!
- let newRootCertificate = Bundle.main.path(forResource: "new_le_root_cert", ofType: "cer")!
-
- return [oldRootCertificate, newRootCertificate].map { (path) -> SecCertificate in
- let data = FileManager.default.contents(atPath: path)!
- return SecCertificateCreateWithData(nil, data as CFData)!
- }
- }
-
- init() {
- sessionDelegate = SSLPinningURLSessionDelegate(trustedRootCertificates: Self.trustedRootCertificates)
- session = URLSession(configuration: .ephemeral, delegate: sessionDelegate, delegateQueue: nil)
- }
-
- func createAccount() -> RestSessionEndpoint<EmptyPayload, AccountResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.createAccount())
- }
-
- func getRelays() -> RestSessionEndpoint<ETagPayload<EmptyPayload>, HttpResourceCacheResponse<ServerRelaysResponse>> {
- return RestSessionEndpoint(session: session, endpoint: Self.getRelays())
- }
-
- func getAccountExpiry() -> RestSessionEndpoint<TokenPayload<EmptyPayload>, AccountResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.getAccountExpiry())
- }
-
- func getWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, WireguardAddressesResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.getWireguardKey())
- }
-
- func pushWireguardKey() -> RestSessionEndpoint<TokenPayload<PushWireguardKeyRequest>, WireguardAddressesResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.pushWireguardKey())
- }
-
- func replaceWireguardKey() -> RestSessionEndpoint<TokenPayload<ReplaceWireguardKeyRequest>, WireguardAddressesResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.replaceWireguardKey())
- }
-
- func deleteWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, ()> {
- return RestSessionEndpoint(session: session, endpoint: Self.deleteWireguardKey())
- }
-
- func createApplePayment() -> RestSessionEndpoint<TokenPayload<CreateApplePaymentRequest>, CreateApplePaymentResponse> {
- return RestSessionEndpoint(session: session, endpoint: Self.createApplePayment())
- }
-
- func sendProblemReport() -> RestSessionEndpoint<ProblemReportRequest, ()> {
- return RestSessionEndpoint(session: session, endpoint: Self.sendProblemReport())
- }
-}
-
-extension MullvadRest {
- /// POST /v1/accounts
- static func createAccount() -> RestEndpoint<EmptyPayload, AccountResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("accounts"),
- httpMethod: .post,
- responseHandlerFactory: { (input) in
- return DecodingResponseHandler(expectedStatus: HttpStatus.created)
- }
- )
- }
-
- /// GET /v1/relays
- static func getRelays() -> RestEndpoint<ETagPayload<EmptyPayload>, HttpResourceCacheResponse<ServerRelaysResponse>> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("relays"),
- httpMethod: .get,
- responseHandlerFactory: { (input) in
- return HttpCacheDecodingResponseHandler(etag: input.etag)
- }
- )
- }
-
- /// GET /v1/me
- static func getAccountExpiry() -> RestEndpoint<TokenPayload<EmptyPayload>, AccountResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("me"),
- httpMethod: .get,
- responseHandlerFactory: { (input) in
- return DecodingResponseHandler(expectedStatus: HttpStatus.ok)
- }
- )
- }
- /// GET /v1/wireguard-keys/{pubkey}
- static func getWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, WireguardAddressesResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .get,
- responseHandlerFactory: { (input) in
- return DecodingResponseHandler(expectedStatus: HttpStatus.ok)
- }
- )
- }
-
- /// POST /v1/wireguard-keys
- static func pushWireguardKey() -> RestEndpoint<TokenPayload<PushWireguardKeyRequest>, WireguardAddressesResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .post,
- responseHandlerFactory: { (input) in
- return AnyResponseHandler { (httpResponse, data) -> Result<WireguardAddressesResponse, ResponseHandlerError> in
- switch httpResponse.statusCode {
- case HttpStatus.ok, HttpStatus.created:
- return MullvadRest.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
-
- default:
- return .failure(.badResponse(httpResponse.statusCode))
- }
- }
- }
- )
- }
-
- /// POST /v1/replace-wireguard-key
- static func replaceWireguardKey() -> RestEndpoint<TokenPayload<ReplaceWireguardKeyRequest>, WireguardAddressesResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("replace-wireguard-key"),
- httpMethod: .post,
- responseHandlerFactory: { (input) in
- return DecodingResponseHandler(expectedStatus: HttpStatus.created)
- }
- )
- }
-
- /// DELETE /v1/wireguard-keys/{pubkey}
- static func deleteWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, ()> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .delete,
- responseHandlerFactory: { (input) in
- return EmptyResponseHandler(expectedStatus: HttpStatus.noContent)
- }
- )
- }
-
- /// POST /v1/create-apple-payment
- static func createApplePayment() -> RestEndpoint<TokenPayload<CreateApplePaymentRequest>, CreateApplePaymentResponse> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("create-apple-payment"),
- httpMethod: .post,
- responseHandlerFactory: { (input) in
- return AnyResponseHandler { (httpResponse, data) -> Result<CreateApplePaymentResponse, ResponseHandlerError> in
- switch httpResponse.statusCode {
- case HttpStatus.ok:
- return MullvadRest.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
- .map { (response) in
- return .noTimeAdded(response.newExpiry)
- }
-
- case HttpStatus.created:
- return MullvadRest.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
- .map { (response) in
- return .timeAdded(response.timeAdded, response.newExpiry)
- }
-
- default:
- return .failure(.badResponse(httpResponse.statusCode))
- }
- }
- }
- )
- }
-
- static func sendProblemReport() -> RestEndpoint<ProblemReportRequest, ()> {
- return RestEndpoint(
- endpointURL: kRestBaseURL.appendingPathComponent("problem-report"),
- httpMethod: .post,
- responseHandlerFactory: { (input) in
- return EmptyResponseHandler(expectedStatus: HttpStatus.noContent)
- }
- )
- }
-
- /// Returns a JSON encoder used by REST API
- static func makeJSONEncoder() -> JSONEncoder {
- let encoder = JSONEncoder()
- encoder.keyEncodingStrategy = .convertToSnakeCase
- encoder.dateEncodingStrategy = .iso8601
- encoder.dataEncodingStrategy = .base64
- return encoder
- }
-
- /// Returns a JSON decoder used by REST API
- static func makeJSONDecoder() -> JSONDecoder {
- let decoder = JSONDecoder()
- decoder.keyDecodingStrategy = .convertFromSnakeCase
- decoder.dateDecodingStrategy = .iso8601
- decoder.dataDecodingStrategy = .base64
- return decoder
- }
-
- /// A private helper that parses the JSON response into the given `Decodable` type.
- fileprivate static func decodeSuccessResponse<T: Decodable>(_ type: T.Type, from data: Data) -> Result<T, ResponseHandlerError> {
- return Result { try MullvadRest.makeJSONDecoder().decode(type, from: data) }
- .mapError { (error) -> ResponseHandlerError in
- return .decodeData(error)
- }
- }
-
- /// A private helper that parses the JSON response in case of error (Any HTTP code except 2xx)
- fileprivate static func decodeErrorResponse(httpResponse: HTTPURLResponse, data: Data) -> Result<ServerErrorResponse, RestError> {
- return Result { () -> ServerErrorResponse in
- return try MullvadRest.makeJSONDecoder().decode(ServerErrorResponse.self, from: data)
- }.mapError({ (error) -> RestError in
- return .decodeErrorResponse(error)
- })
- }
-}
-
-
-// MARK: - Payload types
-
-/// A payload that adds the authentication token into HTTP Authorization header
-struct TokenPayload<Payload: RestPayload>: RestPayload {
- let token: String
- let payload: Payload
-
- init(token: String, payload: Payload) {
- self.token = token
- self.payload = payload
- }
-
- func inject(into request: inout URLRequest) throws {
- request.addValue("Token \(token)", forHTTPHeaderField: HttpHeader.authorization)
- try payload.inject(into: &request)
- }
-}
-
-/// A payload that adds the public key into the URL path
-struct PublicKeyPayload<Payload: RestPayload>: RestPayload {
- let pubKey: Data
- let payload: Payload
-
- init(pubKey: Data, payload: Payload) {
- self.pubKey = pubKey
- self.payload = payload
- }
-
- func inject(into request: inout URLRequest) throws {
- let pathComponent = pubKey.base64EncodedString()
- .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
-
- request.url = request.url?.appendingPathComponent(pathComponent)
- try payload.inject(into: &request)
- }
-}
-
-/// A payload that adds the ETag header to the request
-struct ETagPayload<Payload: RestPayload>: RestPayload {
- let etag: String?
- let enforceWeakValidator: Bool
- let payload: Payload
-
- init(etag: String?, enforceWeakValidator: Bool, payload: Payload) {
- self.etag = etag
- self.enforceWeakValidator = enforceWeakValidator
- self.payload = payload
- }
-
- func inject(into request: inout URLRequest) throws {
- if var etag = etag {
- // Enforce weak validator to account for some backend caching quirks.
- if enforceWeakValidator && etag.starts(with: "\"") {
- etag.insert(contentsOf: "W/", at: etag.startIndex)
- }
- request.setValue(etag, forHTTPHeaderField: HttpHeader.ifNoneMatch)
- }
- try payload.inject(into: &request)
- }
-}
-
-/// An empty payload placeholder type.
-/// Use it in places where the payload is not expected
-struct EmptyPayload: RestPayload {
- init() {}
- func inject(into request: inout URLRequest) throws {}
-}
-
-
-// MARK: - Response types
-
-struct AccountResponse: Decodable {
- let token: String
- let expires: Date
-}
-
-struct ServerLocation: Codable {
- let country: String
- let city: String
- let latitude: Double
- let longitude: Double
-}
-
-struct ServerRelay: Codable {
- let hostname: String
- let active: Bool
- let owned: Bool
- let location: String
- let provider: String
- let weight: Int32
- let ipv4AddrIn: IPv4Address
- let ipv6AddrIn: IPv6Address
- let publicKey: Data
- let includeInCountry: Bool
-}
-
-struct ServerWireguardTunnels: Codable {
- let ipv4Gateway: IPv4Address
- let ipv6Gateway: IPv6Address
- let portRanges: [ClosedRange<UInt16>]
- let relays: [ServerRelay]
-}
-
-private extension HTTPURLResponse {
- func value(forCaseInsensitiveHTTPHeaderField headerField: String) -> String? {
- if #available(iOS 13.0, *) {
- return self.value(forHTTPHeaderField: headerField)
- } else {
- for case let key as String in self.allHeaderFields.keys {
- if case .orderedSame = key.caseInsensitiveCompare(headerField) {
- return self.allHeaderFields[key] as? String
- }
- }
- return nil
- }
- }
-}
-
-enum HttpResourceCacheResponse<T: Decodable> {
- case notModified
- case newContent(_ etag: String?, _ value: T)
-}
-
-struct ServerRelaysResponse: Codable {
- let locations: [String: ServerLocation]
- let wireguard: ServerWireguardTunnels
-}
-
-struct PushWireguardKeyRequest: Encodable, RestPayload {
- let pubkey: Data
-}
-
-struct WireguardAddressesResponse: Decodable {
- let id: String
- let pubkey: Data
- let ipv4Address: IPAddressRange
- let ipv6Address: IPAddressRange
-}
-
-struct ReplaceWireguardKeyRequest: Encodable, RestPayload {
- let old: Data
- let new: Data
-}
-
-struct CreateApplePaymentRequest: Encodable, RestPayload {
- let receiptString: Data
-}
-
-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, RestPayload {
- let address: String
- let message: String
- let log: String
- let metadata: [String: String]
-}
diff --git a/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift b/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift
index be658c3ef6..47fb1028df 100644
--- a/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift
+++ b/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift
@@ -17,7 +17,7 @@ class ProblemReportSubmissionOverlayView: UIView {
enum State {
case sending
case sent(_ email: String)
- case failure(RestError)
+ case failure(REST.Error)
var title: String? {
switch self {
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift
index cd6ec4a490..433b5f8085 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/ProblemReportViewController.swift
@@ -13,7 +13,6 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
private var textViewKeyboardResponder: AutomaticKeyboardResponder?
private var scrollViewKeyboardResponder: AutomaticKeyboardResponder?
- private let mullvadRest = MullvadRest()
private lazy var consolidatedLog: ConsolidatedApplicationLog = {
let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier
@@ -565,7 +564,7 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
navigationItem.setHidesBackButton(true, animated: true)
}
- private func didSendProblemReport(viewModel: ViewModel, result: Result<(), RestError>) {
+ private func didSendProblemReport(viewModel: ViewModel, result: Result<(), REST.Error>) {
switch result {
case .success:
submissionOverlayView.state = .sent(viewModel.email)
@@ -585,31 +584,20 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
private func sendProblemReport() {
let viewModel = Self.persistentViewModel
- willSendProblemReport()
- sendProblemReportHelper(with: viewModel) { [weak self] (result) in
- self?.didSendProblemReport(viewModel: viewModel, result: result)
- }
- }
-
- private func sendProblemReportHelper(with viewModel: ViewModel, completion: @escaping (Result<(), RestError>) -> Void) {
let log = consolidatedLog.string
let metadata = consolidatedLog.metadata.reduce(into: [:]) { (output, entry) in
output[entry.key.rawValue] = entry.value
}
- let request = ProblemReportRequest(address: viewModel.email, message: viewModel.message, log: log, metadata: metadata)
- let result = mullvadRest.sendProblemReport().dataTask(payload: request) { (result) in
- DispatchQueue.main.async {
- completion(result)
- }
- }
+ let request = REST.ProblemReportRequest(address: viewModel.email, message: viewModel.message, log: log, metadata: metadata)
- switch result {
- case .success(let task):
- task.resume()
- case .failure(let error):
- completion(.failure(error))
- }
+ willSendProblemReport()
+
+ REST.Client.shared.sendProblemReport(request)
+ .receive(on: .main)
+ .observe { completion in
+ self.didSendProblemReport(viewModel: viewModel, result: completion.unwrappedValue!)
+ }
}
// MARK: - Input fields' notifications
diff --git a/ios/MullvadVPN/REST/HTTP.swift b/ios/MullvadVPN/REST/HTTP.swift
new file mode 100644
index 0000000000..cc63cd5b47
--- /dev/null
+++ b/ios/MullvadVPN/REST/HTTP.swift
@@ -0,0 +1,73 @@
+//
+// HTTP.swift
+// HTTP
+//
+// Created by pronebird on 06/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// HTTP method
+struct HTTPMethod: RawRepresentable {
+ static let get = HTTPMethod(rawValue: "GET")
+ static let post = HTTPMethod(rawValue: "POST")
+ static let delete = HTTPMethod(rawValue: "DELETE")
+
+ let rawValue: String
+ init(rawValue: String) {
+ self.rawValue = rawValue.uppercased()
+ }
+}
+
+// HTTP status codes
+struct HTTPStatus: RawRepresentable, Equatable {
+ static let ok = HTTPStatus(rawValue: 200)
+ static let created = HTTPStatus(rawValue: 201)
+ static let noContent = HTTPStatus(rawValue: 204)
+ static let notModified = HTTPStatus(rawValue: 304)
+
+ let rawValue: Int
+ init(rawValue value: Int) {
+ rawValue = value
+ }
+
+ static func == (lhs: Self, rhs: Self) -> Bool {
+ return lhs.rawValue == rhs.rawValue
+ }
+
+ static func == (lhs: Self, rhs: Int) -> Bool {
+ return lhs.rawValue == rhs
+ }
+
+ static func == (lhs: Int, rhs: Self) -> Bool {
+ return lhs == rhs.rawValue
+ }
+
+ static func ~= (lhs: Self, rhs: Int) -> Bool {
+ return lhs.rawValue == rhs
+ }
+}
+
+/// HTTP headers
+enum HTTPHeader {
+ static let authorization = "Authorization"
+ static let contentType = "Content-Type"
+ static let etag = "ETag"
+ static let ifNoneMatch = "If-None-Match"
+}
+
+extension HTTPURLResponse {
+ func value(forCaseInsensitiveHTTPHeaderField headerField: String) -> String? {
+ if #available(iOS 13.0, *) {
+ return self.value(forHTTPHeaderField: headerField)
+ } else {
+ for case let key as String in self.allHeaderFields.keys {
+ if case .orderedSame = key.caseInsensitiveCompare(headerField) {
+ return self.allHeaderFields[key] as? String
+ }
+ }
+ return nil
+ }
+ }
+}
diff --git a/ios/MullvadVPN/REST/REST.swift b/ios/MullvadVPN/REST/REST.swift
new file mode 100644
index 0000000000..7d8c3a8c98
--- /dev/null
+++ b/ios/MullvadVPN/REST/REST.swift
@@ -0,0 +1,12 @@
+//
+// REST.swift
+// REST
+//
+// Created by pronebird on 06/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// REST namespace
+enum REST {}
diff --git a/ios/MullvadVPN/REST/RESTClient.swift b/ios/MullvadVPN/REST/RESTClient.swift
new file mode 100644
index 0000000000..c0b0caa9dd
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTClient.swift
@@ -0,0 +1,409 @@
+//
+// RESTClient.swift
+// MullvadVPN
+//
+// Created by pronebird on 10/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+import WireGuardKit
+
+extension REST {
+
+ class Client {
+ static let shared = Client()
+
+ private let session: URLSession
+ private let sessionDelegate: SSLPinningURLSessionDelegate
+
+ /// REST API v1 base URL
+ private let baseURL = URL(string: "https://api.mullvad.net/app/v1")!
+
+ /// Network request timeout in seconds
+ private let networkTimeout: TimeInterval = 10
+
+ /// Returns array of trusted root certificates
+ private static var trustedRootCertificates: [SecCertificate] {
+ let oldRootCertificate = Bundle.main.path(forResource: "old_le_root_cert", ofType: "cer")!
+ let newRootCertificate = Bundle.main.path(forResource: "new_le_root_cert", ofType: "cer")!
+
+ return [oldRootCertificate, newRootCertificate].map { (path) -> SecCertificate in
+ let data = FileManager.default.contents(atPath: path)!
+ return SecCertificateCreateWithData(nil, data as CFData)!
+ }
+ }
+
+ private init() {
+ sessionDelegate = SSLPinningURLSessionDelegate(trustedRootCertificates: Self.trustedRootCertificates)
+ session = URLSession(configuration: .ephemeral, delegate: sessionDelegate, delegateQueue: nil)
+ }
+
+ // MARK: - Public
+
+ func createAccount() -> Result<AccountResponse, REST.Error>.Promise {
+ let request = createURLRequest(method: .post, path: "accounts")
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.created {
+ return Self.decodeSuccessResponse(AccountResponse.self, from: data)
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func getRelays(etag: String?) -> Result<ServerRelaysCacheResponse, REST.Error>.Promise {
+ var request = createURLRequest(method: .get, path: "relays")
+ if let etag = etag {
+ setETagHeader(etag: etag, request: &request)
+ }
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ switch httpResponse.statusCode {
+ case .ok:
+ return Self.decodeSuccessResponse(ServerRelaysResponse.self, from: data)
+ .map { serverRelays in
+ let newEtag = httpResponse.value(forCaseInsensitiveHTTPHeaderField: HTTPHeader.etag)
+ return .newContent(newEtag, serverRelays)
+ }
+
+ case .notModified where etag != nil:
+ return .success(.notModified)
+
+ default:
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func getAccountExpiry(token: String) -> Result<AccountResponse, REST.Error>.Promise {
+ var request = createURLRequest(method: .get, path: "me")
+
+ setAuthenticationToken(token: token, request: &request)
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.ok {
+ return Self.decodeSuccessResponse(AccountResponse.self, from: data)
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func getWireguardKey(token: String, publicKey: PublicKey) -> Result<WireguardAddressesResponse, REST.Error>.Promise {
+ let urlEncodedPublicKey = publicKey.base64Key
+ .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
+
+ let path = "wireguard-keys/".appending(urlEncodedPublicKey)
+ var request = createURLRequest(method: .get, path: path)
+
+ setAuthenticationToken(token: token, request: &request)
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.ok {
+ return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func pushWireguardKey(token: String, publicKey: PublicKey) -> Result<WireguardAddressesResponse, REST.Error>.Promise {
+ var request = createURLRequest(method: .post, path: "wireguard-keys")
+ let body = PushWireguardKeyRequest(pubkey: publicKey.rawValue)
+
+ setAuthenticationToken(token: token, request: &request)
+
+ do {
+ try setHTTPBody(value: body, request: &request)
+ } catch {
+ return .failure(.encodePayload(error))
+ }
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ switch httpResponse.statusCode {
+ case .created, .ok:
+ return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
+ default:
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func replaceWireguardKey(token: String, oldPublicKey: PublicKey, newPublicKey: PublicKey) -> Result<WireguardAddressesResponse, REST.Error>.Promise {
+ var request = createURLRequest(method: .post, path: "replace-wireguard-key")
+ let body = ReplaceWireguardKeyRequest(old: oldPublicKey.rawValue, new: newPublicKey.rawValue)
+
+ setAuthenticationToken(token: token, request: &request)
+
+ do {
+ try setHTTPBody(value: body, request: &request)
+ } catch {
+ return .failure(.encodePayload(error))
+ }
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.created {
+ return Self.decodeSuccessResponse(WireguardAddressesResponse.self, from: data)
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func deleteWireguardKey(token: String, publicKey: PublicKey) -> Result<(), REST.Error>.Promise {
+ let urlEncodedPublicKey = publicKey.base64Key
+ .addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
+
+ let path = "wireguard-keys/".appending(urlEncodedPublicKey)
+ var request = createURLRequest(method: .delete, path: path)
+
+ setAuthenticationToken(token: token, request: &request)
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.noContent {
+ return .success(())
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func createApplePayment(token: String, receiptString: Data) -> Result<CreateApplePaymentResponse, REST.Error>.Promise {
+ var request = createURLRequest(method: .post, path: "create-apple-payment")
+ let body = CreateApplePaymentRequest(receiptString: receiptString)
+
+ setAuthenticationToken(token: token, request: &request)
+
+ do {
+ try setHTTPBody(value: body, request: &request)
+ } catch {
+ return .failure(.encodePayload(error))
+ }
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ switch httpResponse.statusCode {
+ case HTTPStatus.ok:
+ return REST.Client.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
+ .map { (response) in
+ return .noTimeAdded(response.newExpiry)
+ }
+
+ case HTTPStatus.created:
+ return REST.Client.decodeSuccessResponse(CreateApplePaymentRawResponse.self, from: data)
+ .map { (response) in
+ return .timeAdded(response.timeAdded, response.newExpiry)
+ }
+
+ default:
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ func sendProblemReport(_ body: ProblemReportRequest) -> Result<(), REST.Error>.Promise {
+ var request = createURLRequest(method: .post, path: "problem-report")
+
+ do {
+ try setHTTPBody(value: body, request: &request)
+ } catch {
+ return .failure(.encodePayload(error))
+ }
+
+ return dataTaskPromise(request: request)
+ .mapError(self.mapNetworkError)
+ .flatMap { httpResponse, data in
+ if httpResponse.statusCode == HTTPStatus.noContent {
+ return .success(())
+ } else {
+ return Self.decodeErrorResponseAndMapToServerError(from: data)
+ }
+ }
+ }
+
+ // MARK: - Private
+
+ /// A private helper that parses the 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)
+ }
+ }
+
+ /// A private helper that parses the 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 func mapNetworkError(_ error: URLError) -> REST.Error {
+ return .network(error)
+ }
+
+ 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 dataTaskPromise(request: URLRequest) -> Result<(HTTPURLResponse, Data), URLError>.Promise {
+ return Result<(HTTPURLResponse, Data), URLError>.Promise { resolver in
+ let task = self.dataTask(request: request) { result in
+ resolver.resolve(value: result)
+ }
+
+ resolver.setCancelHandler {
+ task.cancel()
+ }
+
+ task.resume()
+ }
+ }
+
+ private func setHTTPBody<T: Encodable>(value: T, request: inout URLRequest) throws {
+ request.httpBody = try REST.Coding.makeJSONEncoder().encode(value)
+ }
+
+ private 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 func setAuthenticationToken(token: String, request: inout URLRequest) {
+ request.addValue("Token \(token)", forHTTPHeaderField: HTTPHeader.authorization)
+ }
+
+ private func createURLRequest(method: HTTPMethod, path: String) -> URLRequest {
+ var request = URLRequest(
+ url: baseURL.appendingPathComponent(path),
+ cachePolicy: .useProtocolCachePolicy,
+ timeoutInterval: networkTimeout
+ )
+ request.httpShouldHandleCookies = false
+ request.addValue("application/json", forHTTPHeaderField: HTTPHeader.contentType)
+ request.httpMethod = method.rawValue
+ return request
+ }
+ }
+
+ // 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
new file mode 100644
index 0000000000..e5515e439b
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTCoding.swift
@@ -0,0 +1,33 @@
+//
+// RESTCoding.swift
+// RESTCoding
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+ enum Coding {}
+}
+
+extension REST.Coding {
+ /// Returns a JSON encoder used by REST API.
+ static func makeJSONEncoder() -> JSONEncoder {
+ let encoder = JSONEncoder()
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ encoder.dateEncodingStrategy = .iso8601
+ encoder.dataEncodingStrategy = .base64
+ return encoder
+ }
+
+ /// Returns a JSON decoder used by REST API.
+ static func makeJSONDecoder() -> JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ decoder.dateDecodingStrategy = .iso8601
+ decoder.dataDecodingStrategy = .base64
+ return decoder
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift
new file mode 100644
index 0000000000..110eafbfcd
--- /dev/null
+++ b/ios/MullvadVPN/REST/RESTError.swift
@@ -0,0 +1,112 @@
+//
+// RESTError.swift
+// RESTError
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension REST {
+
+ /// An error type returned by `REST.Client`
+ enum Error: ChainedError {
+ /// A failure to encode the payload
+ case encodePayload(Swift.Error)
+
+ /// A failure during networking
+ case network(URLError)
+
+ /// A failure reported by server
+ case server(REST.ServerErrorResponse)
+
+ /// A failure to decode the error response from server
+ case decodeErrorResponse(Swift.Error)
+
+ /// A failure to decode the success response from server
+ case decodeSuccessResponse(Swift.Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .encodePayload:
+ return "Failure to encode the payload"
+ case .network:
+ return "Network error"
+ case .server:
+ return "Server error"
+ case .decodeErrorResponse:
+ return "Failure to decode error response from server"
+ case .decodeSuccessResponse:
+ return "Failure to decode success response from server"
+ }
+ }
+ }
+
+ /// A struct that represents a server response in case of error (any HTTP status code except 2xx).
+ struct ServerErrorResponse: LocalizedError, Decodable, Equatable {
+ /// A list of known server error codes
+ enum Code: String, Equatable {
+ case invalidAccount = "INVALID_ACCOUNT"
+ case keyLimitReached = "KEY_LIMIT_REACHED"
+ case pubKeyNotFound = "PUBKEY_NOT_FOUND"
+
+ static func ~= (pattern: Self, value: REST.ServerErrorResponse) -> Bool {
+ return pattern.rawValue == value.code
+ }
+ }
+
+ static var invalidAccount: Code {
+ return .invalidAccount
+ }
+ static var keyLimitReached: Code {
+ return .keyLimitReached
+ }
+ static var pubKeyNotFound: Code {
+ return .pubKeyNotFound
+ }
+
+ let code: String
+ let error: String?
+
+ var errorDescription: String? {
+ switch code {
+ case Code.keyLimitReached.rawValue:
+ return NSLocalizedString(
+ "KEY_LIMIT_REACHED_ERROR_DESCRIPTION",
+ tableName: "RESTClient",
+ value: "Too many WireGuard keys in use.",
+ comment: ""
+ )
+ case Code.invalidAccount.rawValue:
+ return NSLocalizedString(
+ "INVALID_ACCOUNT_ERROR_DESCRIPTION",
+ tableName: "RESTClient",
+ value: "Invalid account.",
+ comment: ""
+ )
+ default:
+ return nil
+ }
+ }
+
+ var recoverySuggestion: String? {
+ switch code {
+ case Code.keyLimitReached.rawValue:
+ return NSLocalizedString(
+ "KEY_LIMIT_REACHED_ERROR_RECOVERY_SUGGESTION",
+ tableName: "RESTClient",
+ value: "Please visit the website to revoke a key before login is possible.",
+ comment: ""
+ )
+ default:
+ return nil
+ }
+ }
+
+ static func == (lhs: Self, rhs: Self) -> Bool {
+ return lhs.code == rhs.code
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/SSLPinningURLSessionDelegate.swift b/ios/MullvadVPN/REST/SSLPinningURLSessionDelegate.swift
index 73df25f55d..d62e34d7cf 100644
--- a/ios/MullvadVPN/SSLPinningURLSessionDelegate.swift
+++ b/ios/MullvadVPN/REST/SSLPinningURLSessionDelegate.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import Security
import Logging
class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate {
@@ -68,6 +69,4 @@ class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate {
return "\(message) (code: \(code))"
}
-
-
}
diff --git a/ios/MullvadVPN/REST/ServerRelaysResponse.swift b/ios/MullvadVPN/REST/ServerRelaysResponse.swift
new file mode 100644
index 0000000000..1604e15ee2
--- /dev/null
+++ b/ios/MullvadVPN/REST/ServerRelaysResponse.swift
@@ -0,0 +1,45 @@
+//
+// ServerRelaysResponse.swift
+// ServerRelaysResponse
+//
+// Created by pronebird on 27/07/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct Network.IPv4Address
+import struct Network.IPv6Address
+
+extension REST {
+ struct ServerLocation: Codable {
+ let country: String
+ let city: String
+ let latitude: Double
+ let longitude: Double
+ }
+
+ struct ServerRelay: Codable {
+ let hostname: String
+ let active: Bool
+ let owned: Bool
+ let location: String
+ let provider: String
+ let weight: Int32
+ let ipv4AddrIn: IPv4Address
+ let ipv6AddrIn: IPv6Address
+ let publicKey: Data
+ let includeInCountry: Bool
+ }
+
+ struct ServerWireguardTunnels: Codable {
+ let ipv4Gateway: IPv4Address
+ let ipv6Gateway: IPv6Address
+ let portRanges: [ClosedRange<UInt16>]
+ let relays: [ServerRelay]
+ }
+
+ struct ServerRelaysResponse: Codable {
+ let locations: [String: ServerLocation]
+ let wireguard: ServerWireguardTunnels
+ }
+}
diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift
index da1074452c..c69e8a8501 100644
--- a/ios/MullvadVPN/RelaySelector.swift
+++ b/ios/MullvadVPN/RelaySelector.swift
@@ -11,12 +11,12 @@ import Network
struct RelaySelectorResult {
var endpoint: MullvadEndpoint
- var relay: ServerRelay
+ var relay: REST.ServerRelay
var location: Location
}
private struct RelayWithLocation {
- var relay: ServerRelay
+ var relay: REST.ServerRelay
var location: Location
}
@@ -33,9 +33,9 @@ extension RelaySelectorResult {
struct RelaySelector {
- private let relays: ServerRelaysResponse
+ private let relays: REST.ServerRelaysResponse
- init(relays: ServerRelaysResponse) {
+ init(relays: REST.ServerRelaysResponse) {
self.relays = relays
}
@@ -97,7 +97,7 @@ struct RelaySelector {
}
}
- private static func parseRelaysResponse(_ response: ServerRelaysResponse) -> [RelayWithLocation] {
+ private static func parseRelaysResponse(_ response: REST.ServerRelaysResponse) -> [RelayWithLocation] {
return response.wireguard.relays.compactMap { (serverRelay) -> RelayWithLocation? in
guard let serverLocation = response.locations[serverRelay.location] else { return nil }
diff --git a/ios/MullvadVPN/en.lproj/MullvadRest.strings b/ios/MullvadVPN/en.lproj/RESTClient.strings
index f5a83c67a3..f5a83c67a3 100644
--- a/ios/MullvadVPN/en.lproj/MullvadRest.strings
+++ b/ios/MullvadVPN/en.lproj/RESTClient.strings
diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift
index fb17f7eaf4..f37e405aee 100644
--- a/ios/MullvadVPNTests/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/RelaySelectorTests.swift
@@ -40,33 +40,33 @@ class RelaySelectorTests: XCTestCase {
}
-private let sampleRelays = ServerRelaysResponse(
+private let sampleRelays = REST.ServerRelaysResponse(
locations: [
- "es-mad": ServerLocation(
+ "es-mad": REST.ServerLocation(
country: "Spain",
city: "Madrid",
latitude: 40.408566,
longitude: -3.69222
),
- "se-got": ServerLocation(
+ "se-got": REST.ServerLocation(
country: "Sweden",
city: "Gothenburg",
latitude: 57.70887,
longitude: 11.97456
),
- "se-sto": ServerLocation(
+ "se-sto": REST.ServerLocation(
country: "Sweden",
city: "Stockholm",
latitude: 59.3289,
longitude: 18.0649
)
],
- wireguard: ServerWireguardTunnels(
+ wireguard: REST.ServerWireguardTunnels(
ipv4Gateway: .loopback,
ipv6Gateway: .loopback,
portRanges: [53...53],
relays: [
- ServerRelay(
+ REST.ServerRelay(
hostname: "es1-wireguard",
active: true,
owned: true,
@@ -78,7 +78,7 @@ private let sampleRelays = ServerRelaysResponse(
publicKey: Data(),
includeInCountry: true
),
- ServerRelay(
+ REST.ServerRelay(
hostname: "se10-wireguard",
active: true,
owned: true,
@@ -90,7 +90,7 @@ private let sampleRelays = ServerRelaysResponse(
publicKey: Data(),
includeInCountry: true
),
- ServerRelay(
+ REST.ServerRelay(
hostname: "se2-wireguard",
active: true,
owned: true,
@@ -102,7 +102,7 @@ private let sampleRelays = ServerRelaysResponse(
publicKey: Data(),
includeInCountry: true
),
- ServerRelay(
+ REST.ServerRelay(
hostname: "se6-wireguard",
active: true,
owned: true,