diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-09-15 13:03:10 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-09-16 10:44:16 +0200 |
| commit | 1380f3f23c666e2cf0d9fd1e65d79246e9c90864 (patch) | |
| tree | acbd5c31ab8557069ecaf950df0f8e7189334c3a | |
| parent | 8eb1502af70f6496dcf4641a3f388756c61f0d83 (diff) | |
| download | mullvadvpn-1380f3f23c666e2cf0d9fd1e65d79246e9c90864.tar.xz mullvadvpn-1380f3f23c666e2cf0d9fd1e65d79246e9c90864.zip | |
REST: refactor
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, |
