diff --git a/LibSession-Util b/LibSession-Util index 6dab3b992..4e79b252d 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 6dab3b99208b9be410952174e72cb38bb0dedb27 +Subproject commit 4e79b252d480c24e1cb543c5a65d4d4f5a7b5fdd diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 3022f32de..fe6ab9ded 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -394,7 +394,6 @@ C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; }; C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; }; C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; }; - C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; }; C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -792,7 +791,6 @@ FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */; }; - FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */; }; FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */; }; FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; @@ -853,12 +851,6 @@ FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; - FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; }; - FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; }; - FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */; }; - FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */; }; - FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */; }; - FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; @@ -885,7 +877,7 @@ FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; }; FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */; }; FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */; }; - FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487629405906007DCAE5 /* HTTPError.swift */; }; + FDF8487A29405906007DCAE5 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487629405906007DCAE5 /* NetworkError.swift */; }; FDF8487B29405906007DCAE5 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487729405906007DCAE5 /* HTTPHeader.swift */; }; FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487829405906007DCAE5 /* HTTPMethod.swift */; }; FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; @@ -931,13 +923,10 @@ FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */; }; FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */; }; FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */; }; - FDF848E329405D6E007DCAE5 /* OnionRequestAPIVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */; }; FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */; }; FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */; }; FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */; }; - FDF848E729405D6E007DCAE5 /* OnionRequestAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */; }; FDF848EB29405E4F007DCAE5 /* OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */; }; - FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */; }; FDF848ED29405E4F007DCAE5 /* Notification+OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */; }; FDF848EF294067E4007DCAE5 /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */; }; FDF848F129406A30007DCAE5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F029406A30007DCAE5 /* Format.swift */; }; @@ -1594,7 +1583,6 @@ C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C3C2A5B7255385EC00C340D1 /* Snode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snode.swift; sourceTree = ""; }; C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - C3C2A5BC255385EE00C340D1 /* HTTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; @@ -1980,7 +1968,6 @@ FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = ""; }; - FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIRequest.swift; sourceTree = ""; }; FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = ""; }; FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; @@ -2040,12 +2027,6 @@ FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; - FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = ""; }; - FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = ""; }; - FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed1-2023-2y.der"; sourceTree = ""; }; - FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed2-2023-2y.der"; sourceTree = ""; }; - FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed3-2023-2y.crt"; sourceTree = ""; }; - FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; @@ -2078,7 +2059,7 @@ FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = ""; }; FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPQueryParam.swift; sourceTree = ""; }; - FDF8487629405906007DCAE5 /* HTTPError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = ""; }; + FDF8487629405906007DCAE5 /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; FDF8487729405906007DCAE5 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = ""; }; FDF8487829405906007DCAE5 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; @@ -2121,13 +2102,10 @@ FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = ""; }; FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = ""; }; FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = ""; }; - FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIVersion.swift; sourceTree = ""; }; FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = ""; }; FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIDestination.swift; sourceTree = ""; }; - FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIError.swift; sourceTree = ""; }; FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = ""; }; - FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = ""; }; FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+OnionRequestAPI.swift"; sourceTree = ""; }; FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = ""; }; FDF848F029406A30007DCAE5 /* Format.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Format.swift; path = "SessionUIKit/Style Guide/Format.swift"; sourceTree = SOURCE_ROOT; }; @@ -2562,19 +2540,6 @@ path = "Content Views"; sourceTree = ""; }; - B81D260326158DF5004D1FE1 /* Certificates */ = { - isa = PBXGroup; - children = ( - FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */, - FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */, - FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */, - FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */, - FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */, - FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */, - ); - path = Certificates; - sourceTree = ""; - }; B821493625D4D6A7009C0F2A /* Views & Modals */ = { isa = PBXGroup; children = ( @@ -2664,7 +2629,6 @@ isa = PBXGroup; children = ( C33FDB68255A580F00E217F9 /* ContentProxy.swift */, - FDF8487629405906007DCAE5 /* HTTPError.swift */, FDF8487729405906007DCAE5 /* HTTPHeader.swift */, FDF8487829405906007DCAE5 /* HTTPMethod.swift */, FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */, @@ -2678,7 +2642,7 @@ FDF8488229405A12007DCAE5 /* BatchResponse.swift */, FDC438B227BB15B400C60D73 /* ResponseInfo.swift */, C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, - C3C2A5BC255385EE00C340D1 /* HTTP.swift */, + FDF8487629405906007DCAE5 /* NetworkError.swift */, FD23CE1A2A651E6D0000B97C /* NetworkType.swift */, FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */, ); @@ -3526,7 +3490,6 @@ C3AAFFF125AE99710089E6DD /* AppDelegate.swift */, 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */, 7BFD1A952747689000FB91B9 /* TurnServers */, - B81D260326158DF5004D1FE1 /* Certificates */, B8FF8E6025C10D8B004D1F22 /* Countries */, 34330A581E7875FB00DF2FB9 /* Fonts */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */, @@ -4338,7 +4301,6 @@ FDC4382D27B383A600C60D73 /* Models */ = { isa = PBXGroup; children = ( - FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */, FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */, FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, @@ -4501,8 +4463,6 @@ FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */, FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, - FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */, - FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */, FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */, FD7F74832BB283DF006DDFD8 /* SwarmDrainBehaviour.swift */, FD7F74812BB283CE006DDFD8 /* UpdatableTimestamp.swift */, @@ -4519,7 +4479,6 @@ FD7F74852BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift */, FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */, FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */, - FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */, FD7F747B2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift */, ); path = Networking; @@ -5162,7 +5121,6 @@ 34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */, 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */, 34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */, - FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */, 34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */, 45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */, 34C3C78D20409F320000134C /* Opening.m4r in Resources */, @@ -5180,7 +5138,6 @@ 45B74A812044AAB600CD42F8 /* chord-quiet.aifc in Resources */, 45B74A832044AAB600CD42F8 /* circles.aifc in Resources */, 45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */, - FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */, C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */, 4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */, B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */, @@ -5193,11 +5150,8 @@ 45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */, 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */, 45B74A792044AAB600CD42F8 /* input.aifc in Resources */, - FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */, C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */, 45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */, - FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */, - FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */, 45B74A7A2044AAB600CD42F8 /* keys.aifc in Resources */, 45B74A762044AAB600CD42F8 /* keys-quiet.aifc in Resources */, 45B74A862044AAB600CD42F8 /* note.aifc in Resources */, @@ -5207,7 +5161,6 @@ 45B74A822044AAB600CD42F8 /* pulse.aifc in Resources */, C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */, B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */, - FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */, 45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */, 45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */, 45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */, @@ -5829,7 +5782,6 @@ FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */, FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */, FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, - FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */, FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, @@ -5860,7 +5812,6 @@ FDF848C429405C5A007DCAE5 /* RevokeSubkeyResponse.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, - FDF848E329405D6E007DCAE5 /* OnionRequestAPIVersion.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, @@ -5880,7 +5831,6 @@ FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */, FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */, - FDF848E729405D6E007DCAE5 /* OnionRequestAPIError.swift in Sources */, FDF848BE29405C5A007DCAE5 /* GetServiceNodesRequest.swift in Sources */, FDF848EB29405E4F007DCAE5 /* OnionRequestAPI.swift in Sources */, FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, @@ -5993,7 +5943,6 @@ FD29598D2A43BC0B00888A17 /* Version.swift in Sources */, FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */, FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */, - C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */, B8856D23256F116B001CE70E /* Weak.swift in Sources */, FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */, @@ -6005,7 +5954,7 @@ FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, - FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */, + FDF8487A29405906007DCAE5 /* NetworkError.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, B87EF18126377A1D00124B3C /* Features.swift in Sources */, FD09797727FAB7A600936362 /* Data+Image.swift in Sources */, @@ -6202,7 +6151,6 @@ FD245C682850666300B966DD /* Message+Destination.swift in Sources */, FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */, - FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */, C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 593559c8f..9d4144476 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1352,7 +1352,7 @@ extension ConversationVC: guard cellViewModel.threadVariant == .community else { return } Storage.shared - .readPublisher { db -> (HTTP.PreparedRequest, OpenGroupAPI.PendingChange) in + .readPublisher { db -> (Network.PreparedRequest, OpenGroupAPI.PendingChange) in guard let openGroup: OpenGroup = try? OpenGroup .fetchOne(db, id: cellViewModel.threadId), @@ -1363,7 +1363,7 @@ extension ConversationVC: .fetchOne(db) else { throw StorageError.objectNotFound } - let preparedRequest: HTTP.PreparedRequest = try OpenGroupAPI + let preparedRequest: Network.PreparedRequest = try OpenGroupAPI .preparedReactionDeleteAll( db, emoji: emoji, @@ -1453,7 +1453,7 @@ extension ConversationVC: typealias OpenGroupInfo = ( pendingReaction: Reaction?, pendingChange: OpenGroupAPI.PendingChange, - preparedRequest: HTTP.PreparedRequest + preparedRequest: Network.PreparedRequest ) /// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup @@ -1540,7 +1540,7 @@ extension ConversationVC: OpenGroupManager.doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer) else { throw MessageSenderError.invalidMessage } - let preparedRequest: HTTP.PreparedRequest = try { + let preparedRequest: Network.PreparedRequest = try { guard !remove else { return try OpenGroupAPI .preparedReactionDelete( @@ -2232,7 +2232,7 @@ extension ConversationVC: from: self, request: SnodeAPI .deleteMessages( - publicKey: targetPublicKey, + swarmPublicKey: targetPublicKey, serverHashes: [serverHash] ) .map { _ in () } @@ -2308,7 +2308,7 @@ extension ConversationVC: cancelStyle: .alert_text, onConfirm: { [weak self] _ in Storage.shared - .readPublisher { db -> HTTP.PreparedRequest in + .readPublisher { db -> Network.PreparedRequest in guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { throw StorageError.objectNotFound } diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index ff5ad3f98..a2fd3dc2f 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -280,7 +280,7 @@ enum GiphyAPI { let urlString = "/v1/gifs/trending?api_key=\(kGiphyApiKey)&limit=\(kGiphyPageSize)" // stringlint:disable guard let url: URL = URL(string: "\(kGiphyBaseURL)\(urlString)") else { - return Fail(error: HTTPError.invalidURL) + return Fail(error: NetworkError.invalidURL) .eraseToAnyPublisher() } @@ -290,7 +290,7 @@ enum GiphyAPI { Logger.error("search request failed: \(urlError)") // URLError codes are negative values - return HTTPError.generic + return NetworkError.unknown } .map { data, _ in Logger.debug("search request succeeded") @@ -320,15 +320,15 @@ enum GiphyAPI { ].joined() ) else { - return Fail(error: HTTPError.invalidURL) + return Fail(error: NetworkError.invalidURL) .eraseToAnyPublisher() } var request: URLRequest = URLRequest(url: url) guard ContentProxy.configureProxiedRequest(request: &request) else { - owsFailDebug("Could not configure query: \(query).") - return Fail(error: HTTPError.generic) + SNLog("Could not configure query: \(query).") + return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } @@ -338,13 +338,13 @@ enum GiphyAPI { Logger.error("search request failed: \(urlError)") // URLError codes are negative values - return HTTPError.generic + return NetworkError.unknown } .tryMap { data, _ -> [GiphyImageInfo] in Logger.debug("search request succeeded") guard let imageInfos = self.parseGiphyImages(responseData: data) else { - throw HTTPError.invalidResponse + throw NetworkError.invalidResponse } return imageInfos diff --git a/Session/Meta/Certificates/seed1-2023-2y.crt b/Session/Meta/Certificates/seed1-2023-2y.crt deleted file mode 100644 index 658e0eb41..000000000 --- a/Session/Meta/Certificates/seed1-2023-2y.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEDTCCAvWgAwIBAgIUPwyEuBgX6kfxt+G2tQ4GNTZErMMwDQYJKoZIhvcNAQEL -BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN -ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x -HTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTYyMloX -DTI1MDQxMTEyNTYyMlowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh -MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo -IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwxkbApgfFA1upIFj47y+7k+qrM0l -MLDvtX3U95icVgb7HGhxKzkzbCOscKZnVsq1N90drYVh7to0H69b2t6y7l+9q6Zd -Ytzi9U0NoL/OabmR6F+w/XpokRM7CMz9zeg84VLnyu2yRdR26keG4/AZRXk+j8Dy -6xp09+hTF7kfdfzL3HdYyUsyx+/CqoyzU01yn4aVgJ9aufYu38QKnnjfROiVahJf -Xm1MvHLmDCe+WbDFgsp2Y0NjNbpASUgrOEPNnIJeY3Lw4kzwNVGsbSBHgvLgSfaD -p5L6k89TUUKA0onlGFAN/MDXL4DNfjSpmfzHyhM8XwKJ9COSXsvvpX5hHQIDAQAB -o4GKMIGHMB0GA1UdDgQWBBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAfBgNVHSMEGDAW -gBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY -MBaCFHNlZWQxLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G -CSqGSIb3DQEBCwUAA4IBAQBW8q3DzJWVXZew9pJ1MqjqsMuNt2OlnptwIZUme/Lh -krhqBj5o87218542ao1Hkgph4IuuwEQPwJvUoUbh7dT/k+4D6Ua3oUxhmdeyFUv+ -mjQKZ1mfcfrwW+6rCWJRa2mAVYfOhdfBQZgLP7NqYdskVQF5LWXSs1IF3XLTyROy -gCeapTexTvKlr/TMW4spE4ewaQ4AfB2c24iVLcpAWT+12GaJ0AYO+gY2o7LQqywN -qIxt2mbvXyf2wuhr489tmGz53mKa3Xu7JC1uU6g9zqJ4FGMYsI8pa0Ec2ODRBb8s -8W54r5LN472aTYn+UGgV8wadzPFd0FZtQABkDTuWSZY7 ------END CERTIFICATE----- diff --git a/Session/Meta/Certificates/seed1-2023-2y.der b/Session/Meta/Certificates/seed1-2023-2y.der deleted file mode 100644 index d3064e94d..000000000 Binary files a/Session/Meta/Certificates/seed1-2023-2y.der and /dev/null differ diff --git a/Session/Meta/Certificates/seed2-2023-2y.crt b/Session/Meta/Certificates/seed2-2023-2y.crt deleted file mode 100644 index fea4fd4f5..000000000 --- a/Session/Meta/Certificates/seed2-2023-2y.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEDTCCAvWgAwIBAgIUaPiMYcZh7cZZfacCni2NwT5DKh4wDQYJKoZIhvcNAQEL -BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN -ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x -HTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTY0NVoX -DTI1MDQxMTEyNTY0NVowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh -MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo -IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh2UcfW0I+1QWRa3cj7RnMGelYkGK -7l4V6q7je1IkudXBNretkvVF1NCpfZ8dz72JmdGPJ5/uIEW15HDD2L63OmSDVPhA -2JCb/NqmXfeO91lyxgb0sDnN1UH0wzuS75aBjaQ0nXQV3ffmqKnNNv0HK+LTMFD+ -Dv2yGDtZTWH6H3VzPLCvHHYXVdyuQHwchAcNQar5k4dbdEIcYIV+ANccPg7iQ81a -ITZ9bCeACdMqbB9gILq21KWdkxCu1fwSXs/B6n+U4UpJyv87fprvAyU3HqQhqlU7 -dHnzA1dPn8D4a/3CMYZogVm8USNjv4HmWIwKbYDX+VahvuZwEi6+pwEurQIDAQAB -o4GKMIGHMB0GA1UdDgQWBBRxVM4+gFFipZFAg+Fs4x580js+2TAfBgNVHSMEGDAW -gBRxVM4+gFFipZFAg+Fs4x580js+2TAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY -MBaCFHNlZWQyLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G -CSqGSIb3DQEBCwUAA4IBAQBIFj6hsOgNVr2kZufimTxoT1TE8uvycIWyt04q6/nP -8h33u/sHuNPdnr2UewqRyDRFefxrGlqBUQAQJVyzJGIlju/HTZaBnVB0H2smCRtK -ZRHAJ/cwcnAp+STjqgPqt1ZZ6JcfFwJZID4pPmrW8WaQNAtQPi2Ly2JLQ+Ym5wus -aGxGjbDRQSWGmUpg5TE+XdDsHeJtCl6HAEjvtXfq1uzKedRzmqYfIa8Rd7b2tmuy -dN27swR4DRJOK4rAxHnI8jt7GKVtPXnYfRuk2+0dVZ4CD6qHw+CO5mcdCabnflgT -XS8BYlOvkAyVbtmZNAacoUZvPRx3o186BMJoK2coQyFN ------END CERTIFICATE----- diff --git a/Session/Meta/Certificates/seed2-2023-2y.der b/Session/Meta/Certificates/seed2-2023-2y.der deleted file mode 100644 index acc374d57..000000000 Binary files a/Session/Meta/Certificates/seed2-2023-2y.der and /dev/null differ diff --git a/Session/Meta/Certificates/seed3-2023-2y.crt b/Session/Meta/Certificates/seed3-2023-2y.crt deleted file mode 100644 index 1a45bf9fd..000000000 --- a/Session/Meta/Certificates/seed3-2023-2y.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEDTCCAvWgAwIBAgIUEZkKsCM3Leodz+JB0ADefbWoRbswDQYJKoZIhvcNAQEL -BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN -ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x -HTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMB4XDTIzMDUxNzAyNDAwOFoX -DTI1MDQxMjAyNDAwOFowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh -MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo -IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4Yz/kIXn5t+VMATXsortcyK3DFF -hjNICxAt8qdLwyCCJDnedBdfeQb7zrn2A3btzfKrBD0x3JrbVHabUrtI+wFqfDLS -id2WOIIM/8RP2V/e4zanpKsk9yB/euKga+M+fybfTn1WTqQU5nEuU6eZyyEEZBk6 -1rzWJstxWhcfN4rfl+ciSWLcmFLC2LuNZqwm6To77oLPj+DGrUHyRKFZ4Tw9ilcU -TpMKFaMmNzrHEzS5lPJIRa+2LD5vDYR/sv+lPiKMXTb64OTOJjTfucdsyZqWrI0R -mV2pBcrYBoDbxO+7pnr8GrJIcFqTLDI6MbjH6eseZqRHJSYKrNCyGlDeSQIDAQAB -o4GKMIGHMB0GA1UdDgQWBBRUYnrMlCbDZo6YXpnivhBui51XhDAfBgNVHSMEGDAW -gBRUYnrMlCbDZo6YXpnivhBui51XhDAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY -MBaCFHNlZWQzLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G -CSqGSIb3DQEBCwUAA4IBAQBFYRlRODyQTIhNQC+pTapKtHdS9GJqKvyJX6NVFF6w -+oBzZGNYsDTmzaelraAuUz+uS7d0vngu5cV+3jG0DgksELT6hbpuHcad1rxAhuDv -wv/f02qJyB1F2luXma2n+NHgRFhvIYulWjV/DSSmwea2XD4DH+ZKcYeEXyT71b2T -VZfGnxLPVMz99iA6sQxsNfccFMvDxKofha7teRkUJ+SVzyutrneYySqrjGie6+Nb -oOw4CnpiqiUKIf47B6ZKlsJ8MAS8zAo6O9UqfmNdVoXFrZDjaQGPAjSH1oxL7iP5 -pED6BUMytm8spiTEVBYIer/gcXaA4zWSKZ/Fd24OK0GL ------END CERTIFICATE----- diff --git a/Session/Meta/Certificates/seed3-2023-2y.der b/Session/Meta/Certificates/seed3-2023-2y.der deleted file mode 100644 index b197d674d..000000000 Binary files a/Session/Meta/Certificates/seed3-2023-2y.der and /dev/null differ diff --git a/Session/Meta/Session-Info.plist b/Session/Meta/Session-Info.plist index 3e6e2b8e0..e8471f861 100644 --- a/Session/Meta/Session-Info.plist +++ b/Session/Meta/Session-Info.plist @@ -57,6 +57,8 @@ NSAppTransportSecurity + NSAllowsLocalNetworking + NSExceptionDomains seed1.getsession.org @@ -85,7 +87,7 @@ NSCameraUsageDescription Session needs camera access to take pictures and scan QR codes. NSFaceIDUsageDescription - Session's Screen Lock feature uses Face ID. + Session's Screen Lock feature uses Face ID. NSHumanReadableCopyright com.loki-project.loki-messenger NSMicrophoneUsageDescription diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 62b14881d..59f17b380 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -32,19 +32,17 @@ enum Onboarding { ) -> AnyPublisher { let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - return SnodeAPI.getSwarm(for: userPublicKey) - .tryFlatMapWithRandomSnode { snode -> AnyPublisher<[Message], Error> in - CurrentUserPoller - .poll( - namespaces: [.configUserProfile], - from: snode, - for: userPublicKey, - // Note: These values mean the received messages will be - // processed immediately rather than async as part of a Job - calledFromBackgroundPoller: true, - isBackgroundPollValid: { true } - ) - } + return CurrentUserPoller() + .poll( + namespaces: [.configUserProfile], + for: userPublicKey, + // Note: These values mean the received messages will be + // processed immediately rather than async as part of a Job + calledFromBackgroundPoller: true, + isBackgroundPollValid: { true }, + drainBehaviour: .alwaysRandom, + using: dependencies + ) .map { _ -> String? in guard requestId == profileNameRetrievalIdentifier.wrappedValue else { return nil } diff --git a/Session/Onboarding/PNModeVC.swift b/Session/Onboarding/PNModeVC.swift index 33cdf58b3..5dcff3ead 100644 --- a/Session/Onboarding/PNModeVC.swift +++ b/Session/Onboarding/PNModeVC.swift @@ -170,7 +170,7 @@ final class PNModeVC: BaseVC, OptionViewDelegate { ModalActivityIndicatorViewController.present(fromViewController: self) { [weak self, flow = self.flow] viewController in Onboarding.profileNamePublisher .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { HTTPError.timeout }) + .timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { NetworkError.timeout }) .catch { _ -> AnyPublisher in SNLog("Onboarding failed to retrieve existing profile information") return Just(nil) diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index e25430dde..2406223c4 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -172,7 +172,7 @@ final class NukeDataModal: Modal { Publishers .MergeMany( Storage.shared - .read { db -> [(String, HTTP.PreparedRequest)] in + .read { db -> [(String, Network.PreparedRequest)] in return try OpenGroup .filter(OpenGroup.Columns.isActive == true) .select(.server) diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 339e2bf21..782650752 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -75,19 +75,16 @@ public final class BackgroundPoller { ) -> AnyPublisher { let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - return SnodeAPI.getSwarm(for: userPublicKey) - .tryFlatMapWithRandomSnode { snode -> AnyPublisher<[Message], Error> in - CurrentUserPoller.poll( - namespaces: CurrentUserPoller.namespaces, - from: snode, - for: userPublicKey, - calledFromBackgroundPoller: true, - isBackgroundPollValid: { BackgroundPoller.isValid }, - using: dependencies - ) - } - .map { _ in () } - .eraseToAnyPublisher() + return CurrentUserPoller().poll( + namespaces: CurrentUserPoller.namespaces, + for: userPublicKey, + calledFromBackgroundPoller: true, + isBackgroundPollValid: { BackgroundPoller.isValid }, + drainBehaviour: .alwaysRandom, + using: dependencies + ) + .map { _ in () } + .eraseToAnyPublisher() } private static func pollForClosedGroupMessages( @@ -108,21 +105,15 @@ public final class BackgroundPoller { } .defaulting(to: []) .map { groupPublicKey in - SnodeAPI.getSwarm(for: groupPublicKey) - .tryFlatMap { swarm -> AnyPublisher<[Message], Error> in - guard let snode: Snode = swarm.randomElement() else { - throw OnionRequestAPIError.insufficientSnodes - } - - return ClosedGroupPoller.poll( - namespaces: ClosedGroupPoller.namespaces, - from: snode, - for: groupPublicKey, - calledFromBackgroundPoller: true, - isBackgroundPollValid: { BackgroundPoller.isValid }, - using: dependencies - ) - } + return ClosedGroupPoller() + .poll( + namespaces: ClosedGroupPoller.namespaces, + for: groupPublicKey, + calledFromBackgroundPoller: true, + isBackgroundPollValid: { BackgroundPoller.isValid }, + drainBehaviour: .alwaysRandom, + using: dependencies + ) .map { _ in () } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 2aa0c4bd2..b1003f6c7 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -1080,7 +1080,7 @@ extension Attachment { let attachmentId: String = self.id return Storage.shared - .writePublisher { db -> (HTTP.PreparedRequest?, String?, Data?, Data?) in + .writePublisher { db -> (Network.PreparedRequest?, String?, Data?, Data?) in // If the attachment is a downloaded attachment, check if it came from // the server and if so just succeed immediately (no use re-uploading // an attachment that is already present on the server) - or if we want @@ -1118,7 +1118,7 @@ extension Attachment { // Check the file size SNLog("File size: \(data.count) bytes.") - if data.count > FileServerAPI.maxFileSize { throw HTTPError.maxFileSizeExceeded } + if data.count > FileServerAPI.maxFileSize { throw NetworkError.maxFileSizeExceeded } // Update the attachment to the 'uploading' state _ = try? Attachment @@ -1126,7 +1126,7 @@ extension Attachment { .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading)) // We need database access for OpenGroup uploads so generate prepared data - let preparedSendData: HTTP.PreparedRequest? = try { + let preparedSendData: Network.PreparedRequest? = try { switch destination { case .openGroup(let openGroup): return try OpenGroupAPI diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index c87740015..3609e3bd0 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -351,7 +351,7 @@ public extension LinkPreview { return session .dataTaskPublisher(for: request) - .mapError { _ -> Error in HTTPError.generic } // URLError codes are negative values + .mapError { _ -> Error in NetworkError.unknown } // URLError codes are negative values .tryMap { data, response -> (Data, URLResponse) in guard let urlResponse: HTTPURLResponse = response as? HTTPURLResponse else { throw LinkPreviewError.assertionFailure diff --git a/SessionMessagingKit/File Server/FileServerAPI.swift b/SessionMessagingKit/File Server/FileServerAPI.swift index 5cdc38944..cb80aa242 100644 --- a/SessionMessagingKit/File Server/FileServerAPI.swift +++ b/SessionMessagingKit/File Server/FileServerAPI.swift @@ -90,7 +90,7 @@ public enum FileServerAPI { x25519PublicKey: serverPublicKey ), responseType: VersionResponse.self, - timeout: HTTP.defaultTimeout, + timeout: Network.defaultTimeout, using: dependencies ) .send(using: dependencies) @@ -108,8 +108,8 @@ public enum FileServerAPI { retryCount: Int = 0, timeout: TimeInterval, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { - return HTTP.PreparedRequest( + ) throws -> Network.PreparedRequest { + return Network.PreparedRequest( request: request, urlRequest: try request.generateUrlRequest(using: dependencies), responseType: responseType, diff --git a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift index 7015433d4..f09a608e1 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift @@ -95,7 +95,7 @@ public enum AttachmentDownloadJob: JobExecutor { else { throw AttachmentDownloadError.invalidUrl } return Storage.shared - .readPublisher { db -> HTTP.PreparedRequest? in + .readPublisher { db -> Network.PreparedRequest? in try OpenGroup.fetchOne(db, id: threadId) .map { openGroup in try OpenGroupAPI @@ -109,7 +109,7 @@ public enum AttachmentDownloadJob: JobExecutor { } } .flatMap { maybePreparedRequest -> AnyPublisher in - guard let preparedRequest: HTTP.PreparedRequest = maybePreparedRequest else { + guard let preparedRequest: Network.PreparedRequest = maybePreparedRequest else { return FileServerAPI .download( fileId: fileId, @@ -189,13 +189,14 @@ public enum AttachmentDownloadJob: JobExecutor { /// If we get a 404 then we got a successful response from the server but the attachment doesn't /// exist, in this case update the attachment to an "invalid" state so the user doesn't get stuck in /// a retry download loop - case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 404: + case NetworkError.notFound: targetState = .invalid permanentFailure = true - case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400 || statusCode == 401: - /// If we got a 400 or a 401 then we want to fail the download in a way that has to be manually retried as it's - /// likely something else is going on that caused the failure + /// If we got a 400 or a 401 then we want to fail the download in a way that has to be manually retried as it's + /// likely something else is going on that caused the failure + case NetworkError.badRequest, NetworkError.unauthorised, + SnodeAPIError.signatureVerificationFailed: targetState = .failedDownload permanentFailure = true diff --git a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift index 5a5ee81fa..115f9aa43 100644 --- a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift @@ -92,7 +92,7 @@ public enum ConfigurationSyncJob: JobExecutor { ) } } - .flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher in + .flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher in SnodeAPI .sendConfigMessages( changes.compactMap { change in @@ -109,7 +109,7 @@ public enum ConfigurationSyncJob: JobExecutor { } .subscribe(on: queue) .receive(on: queue) - .map { (response: HTTP.BatchResponse) -> [ConfigDump] in + .map { (response: Network.BatchResponse) -> [ConfigDump] in /// The number of responses returned might not match the number of changes sent but they will be returned /// in the same order, this means we can just `zip` the two arrays as it will take the smaller of the two and /// correctly align the response to the change @@ -118,7 +118,7 @@ public enum ConfigurationSyncJob: JobExecutor { /// If the request wasn't successful then just ignore it (the next time we sync this config we will try /// to send the changes again) guard - let typedResponse: HTTP.BatchSubResponse = (subResponse as? HTTP.BatchSubResponse), + let typedResponse: Network.BatchSubResponse = (subResponse as? Network.BatchSubResponse), 200...299 ~= typedResponse.code, !typedResponse.failedToParseBody, let sendMessageResponse: SendMessagesResponse = typedResponse.body @@ -244,7 +244,7 @@ public extension ConfigurationSyncJob { Job(variant: .configurationSync), queue: .global(qos: .userInitiated), success: { _, _, _ in resolver(Result.success(())) }, - failure: { _, error, _, _ in resolver(Result.failure(error ?? HTTPError.generic)) }, + failure: { _, error, _, _ in resolver(Result.failure(error ?? NetworkError.unknown)) }, deferred: { _, _ in }, using: dependencies ) diff --git a/SessionMessagingKit/Jobs/Types/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/Types/ExpirationUpdateJob.swift index 16f44edab..26b4359d1 100644 --- a/SessionMessagingKit/Jobs/Types/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/Types/ExpirationUpdateJob.swift @@ -31,7 +31,7 @@ public enum ExpirationUpdateJob: JobExecutor { let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) SnodeAPI .updateExpiry( - publicKey: userPublicKey, + swarmPublicKey: userPublicKey, serverHashes: details.serverHashes, updatedExpiryMs: details.expirationTimestampMs, shortenOnly: true, diff --git a/SessionMessagingKit/Jobs/Types/GetExpirationJob.swift b/SessionMessagingKit/Jobs/Types/GetExpirationJob.swift index 75d5773f8..9090dce26 100644 --- a/SessionMessagingKit/Jobs/Types/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/Types/GetExpirationJob.swift @@ -46,11 +46,11 @@ public enum GetExpirationJob: JobExecutor { SnodeAPI .getSwarm(for: userPublicKey, using: dependencies) .tryFlatMap { swarm -> AnyPublisher<(ResponseInfoType, GetExpiriesResponse), Error> in - guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic } + guard let snode = swarm.randomElement() else { throw SnodeAPIError.ranOutOfRandomSnodes } return SnodeAPI.getExpiries( from: snode, - associatedWith: userPublicKey, + swarmPublicKey: userPublicKey, of: expirationInfo.map { $0.key }, using: dependencies ) diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index 66da9eca4..45ce08a6f 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -184,7 +184,7 @@ public enum MessageSendJob: JobExecutor { .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } .subscribe(on: queue, using: dependencies) .receive(on: queue, using: dependencies) - .timeout(.milliseconds(Int(HTTP.defaultTimeout * 2 * 1000)), scheduler: queue, customError: { + .timeout(.milliseconds(Int(Network.defaultTimeout * 2 * 1000)), scheduler: queue, customError: { MessageSenderError.sendJobTimeout }) .sinkUntilComplete( @@ -205,7 +205,7 @@ public enum MessageSendJob: JobExecutor { case let senderError as MessageSenderError where !senderError.isRetryable: failure(job, error, true, dependencies) - case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 429: // Rate limited + case SnodeAPIError.rateLimited: failure(job, error, true, dependencies) case SnodeAPIError.clockOutOfSync: diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 4e3b69091..fac22a4dc 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -67,10 +67,10 @@ extension OpenGroupAPI.Message { // If we have data and a signature (ie. the message isn't a deletion) then validate the signature if let base64EncodedData: String = maybeBase64EncodedData, let base64EncodedSignature: String = maybeBase64EncodedSignature { guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } guard let dependencies: Dependencies = decoder.userInfo[Dependencies.userInfoKey] as? Dependencies else { - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } // Verify the signature based on the SessionId.Prefix type @@ -84,7 +84,7 @@ extension OpenGroupAPI.Message { ) else { SNLog("Ignoring message with invalid signature.") - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } case .standard, .unblinded: @@ -94,12 +94,12 @@ extension OpenGroupAPI.Message { ) else { SNLog("Ignoring message with invalid signature.") - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } case .none, .group: SNLog("Ignoring message with invalid sender.") - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 17f5c6949..d665dde75 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -33,7 +33,7 @@ public enum OpenGroupAPI { hasPerformedInitialPoll: Bool, timeSinceLastPoll: TimeInterval, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest> { + ) throws -> Network.PreparedRequest> { let lastInboxMessageId: Int64 = (try? OpenGroup .select(.inboxLatestMessageId) .filter(OpenGroup.Columns.server == server) @@ -149,7 +149,7 @@ public enum OpenGroupAPI { server: String, requests: [any ErasedPreparedRequest], using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest> { + ) throws -> Network.PreparedRequest> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -157,9 +157,9 @@ public enum OpenGroupAPI { method: .post, server: server, endpoint: .batch, - body: HTTP.BatchRequest(requests: requests) + body: Network.BatchRequest(requests: requests) ), - responseType: HTTP.BatchResponseMap.self, + responseType: Network.BatchResponseMap.self, using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) @@ -180,7 +180,7 @@ public enum OpenGroupAPI { server: String, requests: [any ErasedPreparedRequest], using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest> { + ) throws -> Network.PreparedRequest> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -188,9 +188,9 @@ public enum OpenGroupAPI { method: .post, server: server, endpoint: .sequence, - body: HTTP.BatchRequest(requests: requests) + body: Network.BatchRequest(requests: requests) ), - responseType: HTTP.BatchResponseMap.self, + responseType: Network.BatchResponseMap.self, using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) @@ -210,7 +210,7 @@ public enum OpenGroupAPI { server: String, forceBlinded: Bool = false, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -234,7 +234,7 @@ public enum OpenGroupAPI { _ db: Database, server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[Room]> { + ) throws -> Network.PreparedRequest<[Room]> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -254,7 +254,7 @@ public enum OpenGroupAPI { for roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -278,7 +278,7 @@ public enum OpenGroupAPI { for roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -304,7 +304,7 @@ public enum OpenGroupAPI { for roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .preparedSequence( db, @@ -318,8 +318,8 @@ public enum OpenGroupAPI { using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) - .map { (info: ResponseInfoType, response: HTTP.BatchResponseMap) -> CapabilitiesAndRoomResponse in - let maybeCapabilities: HTTP.BatchSubResponse? = (response[.capabilities] as? HTTP.BatchSubResponse) + .map { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRoomResponse: Any? = response.data .first(where: { key, _ in switch key { @@ -328,14 +328,14 @@ public enum OpenGroupAPI { } }) .map { _, value in value } - let maybeRoom: HTTP.BatchSubResponse? = (maybeRoomResponse as? HTTP.BatchSubResponse) + let maybeRoom: Network.BatchSubResponse? = (maybeRoomResponse as? Network.BatchSubResponse) guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, let capabilities: Capabilities = maybeCapabilities?.body, let roomInfo: ResponseInfoType = maybeRoom, let room: Room = maybeRoom?.body - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } return ( capabilities: (info: capabilitiesInfo, data: capabilities), @@ -355,7 +355,7 @@ public enum OpenGroupAPI { _ db: Database, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .preparedSequence( db, @@ -369,23 +369,23 @@ public enum OpenGroupAPI { using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) - .map { (info: ResponseInfoType, response: HTTP.BatchResponseMap) -> CapabilitiesAndRoomsResponse in - let maybeCapabilities: HTTP.BatchSubResponse? = (response[.capabilities] as? HTTP.BatchSubResponse) - let maybeRooms: HTTP.BatchSubResponse<[Room]>? = response.data + .map { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) + let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data .first(where: { key, _ in switch key { case .rooms: return true default: return false } }) - .map { _, value in value as? HTTP.BatchSubResponse<[Room]> } + .map { _, value in value as? Network.BatchSubResponse<[Room]> } guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, let capabilities: Capabilities = maybeCapabilities?.body, let roomsInfo: ResponseInfoType = maybeRooms, let rooms: [Room] = maybeRooms?.body - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } return ( capabilities: (info: capabilitiesInfo, data: capabilities), @@ -406,7 +406,7 @@ public enum OpenGroupAPI { whisperMods: Bool, fileIds: [String]?, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { let signResult: (publicKey: String, signature: [UInt8]) = try sign( db, messageBytes: plaintext.bytes, @@ -443,7 +443,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -468,7 +468,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { let signResult: (publicKey: String, signature: [UInt8]) = try sign( db, messageBytes: plaintext.bytes, @@ -503,7 +503,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -528,7 +528,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[Failable]> { + ) throws -> Network.PreparedRequest<[Failable]> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -558,7 +558,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[Failable]> { + ) throws -> Network.PreparedRequest<[Failable]> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -588,7 +588,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[Failable]> { + ) throws -> Network.PreparedRequest<[Failable]> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -625,7 +625,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -650,7 +650,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { @@ -682,7 +682,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { @@ -712,7 +712,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { @@ -743,7 +743,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { @@ -782,7 +782,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -806,7 +806,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -829,7 +829,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -859,7 +859,7 @@ public enum OpenGroupAPI { to roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -892,7 +892,7 @@ public enum OpenGroupAPI { from roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -916,7 +916,7 @@ public enum OpenGroupAPI { _ db: Database, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[DirectMessage]?> { + ) throws -> Network.PreparedRequest<[DirectMessage]?> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -938,7 +938,7 @@ public enum OpenGroupAPI { id: Int64, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[DirectMessage]?> { + ) throws -> Network.PreparedRequest<[DirectMessage]?> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -957,7 +957,7 @@ public enum OpenGroupAPI { _ db: Database, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -981,7 +981,7 @@ public enum OpenGroupAPI { toInboxFor blindedSessionId: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -1006,7 +1006,7 @@ public enum OpenGroupAPI { _ db: Database, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[DirectMessage]?> { + ) throws -> Network.PreparedRequest<[DirectMessage]?> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -1028,7 +1028,7 @@ public enum OpenGroupAPI { id: Int64, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest<[DirectMessage]?> { + ) throws -> Network.PreparedRequest<[DirectMessage]?> { return try OpenGroupAPI .prepareRequest( request: Request( @@ -1082,7 +1082,7 @@ public enum OpenGroupAPI { from roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -1132,7 +1132,7 @@ public enum OpenGroupAPI { from roomTokens: [String]?, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { return try OpenGroupAPI .prepareRequest( request: Request( @@ -1211,9 +1211,9 @@ public enum OpenGroupAPI { for roomTokens: [String]?, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { + ) throws -> Network.PreparedRequest { guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { - throw HTTPError.generic + throw NetworkError.invalidPreparedRequest } return try OpenGroupAPI @@ -1245,7 +1245,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest> { + ) throws -> Network.PreparedRequest> { return try OpenGroupAPI .preparedSequence( db, @@ -1348,12 +1348,12 @@ public enum OpenGroupAPI { /// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities) private static func signRequest( _ db: Database, - preparedRequest: HTTP.PreparedRequest, + preparedRequest: Network.PreparedRequest, using dependencies: Dependencies ) throws -> URLRequest { guard let url: URL = preparedRequest.request.url, - let target: HTTP.OpenGroupAPITarget = preparedRequest.target as? HTTP.OpenGroupAPITarget + let target: Network.OpenGroupAPITarget = preparedRequest.target as? Network.OpenGroupAPITarget else { throw OpenGroupAPIError.signingFailed } var updatedRequest: URLRequest = preparedRequest.request @@ -1420,10 +1420,10 @@ public enum OpenGroupAPI { private static func prepareRequest( request: Request, responseType: R.Type, - timeout: TimeInterval = HTTP.defaultTimeout, + timeout: TimeInterval = Network.defaultTimeout, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { - return HTTP.PreparedRequest( + ) throws -> Network.PreparedRequest { + return Network.PreparedRequest( request: request, urlRequest: try request.generateUrlRequest(using: dependencies), responseType: responseType, diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 07f6b5ea4..e74f1a826 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -990,7 +990,7 @@ public final class OpenGroupManager { // Try to retrieve the default rooms 8 times let publisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.storage - .readPublisher { db -> HTTP.PreparedRequest in + .readPublisher { db -> Network.PreparedRequest in try OpenGroupAPI.preparedCapabilitiesAndRooms( db, on: OpenGroupAPI.defaultServer, @@ -1126,7 +1126,7 @@ public final class OpenGroupManager { DispatchQueue.global(qos: .background).async(using: dependencies) { // Hold on to the publisher until it has completed at least once dependencies.storage - .readPublisher { db -> (Data?, HTTP.PreparedRequest?) in + .readPublisher { db -> (Data?, Network.PreparedRequest?) in if canUseExistingImage { let maybeExistingData: Data? = try? OpenGroup .select(.imageData) @@ -1164,7 +1164,7 @@ public final class OpenGroupManager { .eraseToAnyPublisher() default: - return Fail(error: HTTPError.generic) + return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } } diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 2c81341d2..edfc0ef72 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -6,16 +6,18 @@ import SessionUtilitiesKit // MARK: - OpenGroupAPITarget -internal extension HTTP { - struct OpenGroupAPITarget: ServerRequestTarget { +internal extension Network { + struct OpenGroupAPITarget: ServerRequestTarget { + typealias Endpoint = E + public let server: String - let path: String + public let endpoint: Endpoint let queryParameters: [HTTPQueryParam: String] public let serverPublicKey: String public let forceBlinded: Bool public var url: URL? { URL(string: "\(server)\(urlPathAndParamsString)") } - public var urlPathAndParamsString: String { pathFor(path: path, queryParams: queryParameters) } + public var urlPathAndParamsString: String { pathFor(path: endpoint.path, queryParams: queryParameters) } public var x25519PublicKey: String { serverPublicKey } } } @@ -44,9 +46,9 @@ public extension Request { self = Request( method: method, endpoint: endpoint, - target: HTTP.OpenGroupAPITarget( + target: Network.OpenGroupAPITarget( server: server, - path: endpoint.path, + endpoint: endpoint, queryParameters: queryParameters, serverPublicKey: publicKey, forceBlinded: forceBlinded diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 41de36a4a..534123c0f 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -62,7 +62,7 @@ extension OpenGroupAPI { case userModerator(String) public static var name: String { "OpenGroupAPI.Endpoint" } - public static var batchRequestVariant: HTTP.BatchRequest.Child.Variant = .sogs + public static var batchRequestVariant: Network.BatchRequest.Child.Variant = .sogs public static var excludedSubRequestHeaders: [HTTPHeader] = [ .sogsPubKey, .sogsTimestamp, .sogsNonce, .sogsSignature ] diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index ea80c22ff..8f7d7065c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -45,7 +45,7 @@ extension MessageReceiver { if author == message.sender, let serverHash: String = interaction.serverHash { SnodeAPI .deleteMessages( - publicKey: author, + swarmPublicKey: author, serverHashes: [serverHash] ) .subscribe(on: DispatchQueue.global(qos: .background)) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 7a35206db..1c9764d4c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -118,7 +118,7 @@ extension MessageReceiver { case .group: SNLog("Ignoring message with invalid sender.") - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } }() diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 93add56fe..b378df596 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -69,7 +69,7 @@ public enum MessageReceiver { case .group: // TODO: Need to decide how we will handle updated group messages SNLog("Ignoring message with invalid sender.") - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } case .closedGroupMessage: diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift deleted file mode 100644 index 671b0b7a5..000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/PushNotificationAPIRequest.swift +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -public struct PushNotificationAPIRequest: Encodable { - private enum CodingKeys: String, CodingKey { - case method - case body = "params" - } - - internal let endpoint: PushNotificationAPI.Endpoint - internal let body: T - - // MARK: - Initialization - - public init( - endpoint: PushNotificationAPI.Endpoint, - body: T - ) { - self.endpoint = endpoint - self.body = body - } - - // MARK: - Codable - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(endpoint.rawValue, forKey: .method) - try container.encode(body, forKey: .body) - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index f490d3723..1aa6e6152 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -1,4 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import Combine @@ -498,10 +500,10 @@ public enum PushNotificationAPI { request: Request, responseType: R.Type, retryCount: Int = 0, - timeout: TimeInterval = HTTP.defaultTimeout, + timeout: TimeInterval = Network.defaultTimeout, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { - return HTTP.PreparedRequest( + ) throws -> Network.PreparedRequest { + return Network.PreparedRequest( request: request, urlRequest: try request.generateUrlRequest(using: dependencies), responseType: responseType, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index 8d34820e4..d208fc9cb 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -17,10 +17,9 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { self = Request( method: method, endpoint: endpoint, - target: HTTP.ServerTarget( + target: Network.ServerTarget( server: endpoint.server, endpoint: endpoint, - path: endpoint.path, queryParameters: queryParameters, x25519PublicKey: endpoint.serverPublicKey ), diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index c4c77c7fa..4875b31c7 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -12,7 +12,7 @@ public final class ClosedGroupPoller: Poller { // MARK: - Settings override var namespaces: [SnodeAPI.Namespace] { ClosedGroupPoller.namespaces } - override var maxNodePollCount: UInt { 0 } + override var pollDrainBehaviour: SwarmDrainBehaviour { .alwaysRandom } private static let minPollInterval: Double = 3 private static let maxPollInterval: Double = 30 diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index bf69dd878..7439aa486 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -16,11 +16,11 @@ public final class CurrentUserPoller: Poller { override var namespaces: [SnodeAPI.Namespace] { CurrentUserPoller.namespaces } - /// After polling a given snode this many times we always switch to a new one. + /// After polling a given snode 6 times we always switch to a new one. /// /// The reason for doing this is that sometimes a snode will be giving us successful responses while /// it isn't actually getting messages from other snodes. - override var maxNodePollCount: UInt { 6 } + override var pollDrainBehaviour: SwarmDrainBehaviour { .limitedReuse(count: 6) } private let pollInterval: TimeInterval = 1.5 private let retryInterval: TimeInterval = 0.25 diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 157f77edd..1ab5834fc 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -126,7 +126,7 @@ extension OpenGroupAPI { ) return dependencies.storage - .readPublisher { db -> (Int64, HTTP.PreparedRequest>) in + .readPublisher { db -> (Int64, Network.PreparedRequest>) in let failureCount: Int64 = (try? OpenGroup .filter(OpenGroup.Columns.server == server) .select(max(OpenGroup.Columns.pollFailureCount)) @@ -310,10 +310,8 @@ extension OpenGroupAPI { /// happening multiple times in a row guard !isPostCapabilitiesRetry, - let error: OnionRequestAPIError = error as? OnionRequestAPIError, - case .httpRequestFailedAtDestination(let statusCode, let data, _) = error, - statusCode == 400, - let dataString: String = String(data: data, encoding: .utf8), + let error: NetworkError = error as? NetworkError, + case .badRequest(let dataString, _) = error, dataString.contains("Invalid authentication: this server requires the use of blinded ids") else { return Just(false) @@ -372,7 +370,7 @@ extension OpenGroupAPI { private func handlePollResponse( info: ResponseInfoType, - response: HTTP.BatchResponseMap, + response: Network.BatchResponseMap, failureCount: Int64, using dependencies: Dependencies ) { @@ -381,7 +379,7 @@ extension OpenGroupAPI { .filter { endpoint, data in switch endpoint { case .capabilities: - guard (data as? HTTP.BatchSubResponse)?.body != nil else { + guard (data as? Network.BatchSubResponse)?.body != nil else { SNLog("Open group polling failed due to invalid capability data.") return false } @@ -389,8 +387,8 @@ extension OpenGroupAPI { return true case .roomPollInfo(let roomToken, _): - guard (data as? HTTP.BatchSubResponse)?.body != nil else { - switch (data as? HTTP.BatchSubResponse)?.code { + guard (data as? Network.BatchSubResponse)?.body != nil else { + switch (data as? Network.BatchSubResponse)?.code { case 404: SNLog("Open group polling failed to retrieve info for unknown room '\(roomToken)'.") default: SNLog("Open group polling failed due to invalid room info data.") } @@ -401,10 +399,10 @@ extension OpenGroupAPI { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: HTTP.BatchSubResponse<[Failable]> = data as? HTTP.BatchSubResponse<[Failable]>, + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, let responseBody: [Failable] = responseData.body else { - switch (data as? HTTP.BatchSubResponse<[Failable]>)?.code { + switch (data as? Network.BatchSubResponse<[Failable]>)?.code { case 404: SNLog("Open group polling failed to retrieve messages for unknown room '\(roomToken)'.") default: SNLog("Open group polling failed due to invalid messages data.") } @@ -423,7 +421,7 @@ extension OpenGroupAPI { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: HTTP.BatchSubResponse<[DirectMessage]?> = data as? HTTP.BatchSubResponse<[DirectMessage]?>, + let responseData: Network.BatchSubResponse<[DirectMessage]?> = data as? Network.BatchSubResponse<[DirectMessage]?>, !responseData.failedToParseBody else { SNLog("Open group polling failed due to invalid inbox/outbox data.") @@ -481,7 +479,7 @@ extension OpenGroupAPI { switch endpoint { case .capabilities: guard - let responseData: HTTP.BatchSubResponse = data as? HTTP.BatchSubResponse, + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, let responseBody: Capabilities = responseData.body else { return false } @@ -489,7 +487,7 @@ extension OpenGroupAPI { case .roomPollInfo(let roomToken, _): guard - let responseData: HTTP.BatchSubResponse = data as? HTTP.BatchSubResponse, + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, let responseBody: RoomPollInfo = responseData.body else { return false } guard let existingOpenGroup: OpenGroup = currentInfo?.groups.first(where: { $0.roomToken == roomToken }) else { @@ -526,7 +524,7 @@ extension OpenGroupAPI { switch endpoint { case .capabilities: guard - let responseData: HTTP.BatchSubResponse = data as? HTTP.BatchSubResponse, + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, let responseBody: Capabilities = responseData.body else { return } @@ -538,7 +536,7 @@ extension OpenGroupAPI { case .roomPollInfo(let roomToken, _): guard - let responseData: HTTP.BatchSubResponse = data as? HTTP.BatchSubResponse, + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, let responseBody: RoomPollInfo = responseData.body else { return } @@ -553,7 +551,7 @@ extension OpenGroupAPI { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: HTTP.BatchSubResponse<[Failable]> = data as? HTTP.BatchSubResponse<[Failable]>, + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, let responseBody: [Failable] = responseData.body else { return } @@ -567,7 +565,7 @@ extension OpenGroupAPI { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: HTTP.BatchSubResponse<[DirectMessage]?> = data as? HTTP.BatchSubResponse<[DirectMessage]?>, + let responseData: Network.BatchSubResponse<[DirectMessage]?> = data as? Network.BatchSubResponse<[DirectMessage]?>, !responseData.failedToParseBody else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index e28249f78..d0169eb11 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -12,6 +12,7 @@ public class Poller { internal var isPolling: Atomic<[String: Bool]> = Atomic([:]) internal var pollCount: Atomic<[String: Int]> = Atomic([:]) internal var failureCount: Atomic<[String: Int]> = Atomic([:]) + internal var drainBehaviour: Atomic<[String: Atomic]> = Atomic([:]) internal var targetSnode: Atomic = Atomic(nil) private var usedSnodes: Atomic> = Atomic([]) @@ -23,8 +24,8 @@ public class Poller { preconditionFailure("abstract class - override in subclass") } - /// The number of times the poller can poll a single snode before swapping to a new snode - internal var maxNodePollCount: UInt { + /// The behaviour for how the poller should drain it's swarm when polling + internal var pollDrainBehaviour: SwarmDrainBehaviour { preconditionFailure("abstract class - override in subclass") } @@ -42,6 +43,8 @@ public class Poller { public func stopPolling(for publicKey: String) { isPolling.mutate { $0[publicKey] = false } + failureCount.mutate { $0[publicKey] = nil } + drainBehaviour.mutate { $0[publicKey] = nil } cancellables.mutate { $0[publicKey]?.cancel() } } @@ -67,6 +70,8 @@ public class Poller { internal func startIfNeeded(for publicKey: String, using dependencies: Dependencies) { // Run on the 'pollerQueue' to ensure any 'Atomic' access doesn't block the main thread // on startup + let drainBehaviour: Atomic = Atomic(pollDrainBehaviour) + Threading.pollerQueue.async { [weak self] in guard self?.isPolling.wrappedValue[publicKey] != true else { return } @@ -74,88 +79,30 @@ public class Poller { // and the timer is not created, if we mark the group as is polling // after setUpPolling. So the poller may not work, thus misses messages self?.isPolling.mutate { $0[publicKey] = true } - self?.pollRecursively(for: publicKey, using: dependencies) - } - } - - internal func getSnodeForPolling( - for publicKey: String, - using dependencies: Dependencies - ) -> AnyPublisher { - // If we don't want to poll a snode multiple times then just grab a random one from the swarm - guard maxNodePollCount > 0 else { - return SnodeAPI.getSwarm(for: publicKey, using: dependencies) - .tryMap { swarm -> Snode in - try swarm.randomElement() ?? { throw OnionRequestAPIError.insufficientSnodes }() - } - .eraseToAnyPublisher() + self?.drainBehaviour.mutate { $0[publicKey] = drainBehaviour } + self?.pollRecursively(for: publicKey, drainBehaviour: drainBehaviour, using: dependencies) } - - // If we already have a target snode then use that - if let targetSnode: Snode = self.targetSnode.wrappedValue { - return Just(targetSnode) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - // Select the next unused snode from the swarm (if we've used them all then clear the used list and - // start cycling through them again) - return SnodeAPI.getSwarm(for: publicKey, using: dependencies) - .tryMap { [usedSnodes = self.usedSnodes, targetSnode = self.targetSnode] swarm -> Snode in - let unusedSnodes: Set = swarm.subtracting(usedSnodes.wrappedValue) - - // If we've used all of the SNodes then clear out the used list - if unusedSnodes.isEmpty { - usedSnodes.mutate { $0.removeAll() } - } - - // Select the next SNode - let nextSnode: Snode = try swarm.randomElement() ?? { throw OnionRequestAPIError.insufficientSnodes }() - targetSnode.mutate { $0 = nextSnode } - usedSnodes.mutate { $0.insert(nextSnode) } - - return nextSnode - } - .eraseToAnyPublisher() - } - - internal func incrementPollCount(publicKey: String) { - guard maxNodePollCount > 0 else { return } - - let pollCount: Int = (self.pollCount.wrappedValue[publicKey] ?? 0) - self.pollCount.mutate { $0[publicKey] = (pollCount + 1) } - - // Check if we've polled the serice node too many times - guard pollCount > maxNodePollCount else { return } - - // If we have polled this service node more than the maximum allowed then clear out - // the 'targetServiceNode' value - self.targetSnode.mutate { $0 = nil } } private func pollRecursively( - for publicKey: String, + for swarmPublicKey: String, + drainBehaviour: Atomic, using dependencies: Dependencies ) { - guard isPolling.wrappedValue[publicKey] == true else { return } + guard isPolling.wrappedValue[swarmPublicKey] == true else { return } let namespaces: [SnodeAPI.Namespace] = self.namespaces let lastPollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let lastPollInterval: TimeInterval = nextPollDelay(for: publicKey, using: dependencies) - let getSnodePublisher: AnyPublisher = getSnodeForPolling(for: publicKey, using: dependencies) + let lastPollInterval: TimeInterval = nextPollDelay(for: swarmPublicKey, using: dependencies) // Store the publisher intp the cancellables dictionary cancellables.mutate { [weak self] cancellables in - cancellables[publicKey] = getSnodePublisher - .flatMap { snode -> AnyPublisher<[Message], Error> in - Poller.poll( - namespaces: namespaces, - from: snode, - for: publicKey, - poller: self, - using: dependencies - ) - } + cancellables[swarmPublicKey] = self?.poll( + namespaces: namespaces, + for: swarmPublicKey, + drainBehaviour: drainBehaviour, + using: dependencies + ) .subscribe(on: Threading.pollerQueue, using: dependencies) .receive(on: Threading.pollerQueue, using: dependencies) .sink( @@ -163,20 +110,17 @@ public class Poller { switch result { case .failure(let error): // Determine if the error should stop us from polling anymore - guard self?.handlePollError(error, for: publicKey, using: dependencies) == true else { + guard self?.handlePollError(error, for: swarmPublicKey, using: dependencies) == true else { return } case .finished: break } - // Increment the poll count - self?.incrementPollCount(publicKey: publicKey) - // Calculate the remaining poll delay let currentTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let nextPollInterval: TimeInterval = ( - self?.nextPollDelay(for: publicKey, using: dependencies) ?? + self?.nextPollDelay(for: swarmPublicKey, using: dependencies) ?? lastPollInterval ) let remainingInterval: TimeInterval = max(0, nextPollInterval - (currentTime - lastPollStart)) @@ -184,12 +128,12 @@ public class Poller { // Schedule the next poll guard remainingInterval > 0 else { return Threading.pollerQueue.async(using: dependencies) { - self?.pollRecursively(for: publicKey, using: dependencies) + self?.pollRecursively(for: swarmPublicKey, drainBehaviour: drainBehaviour, using: dependencies) } } Threading.pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default, using: dependencies) { - self?.pollRecursively(for: publicKey, using: dependencies) + self?.pollRecursively(for: swarmPublicKey, drainBehaviour: drainBehaviour, using: dependencies) } }, receiveValue: { _ in } @@ -202,44 +146,42 @@ public class Poller { /// /// **Note:** The returned messages will have already been processed by the `Poller`, they are only returned /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) - public static func poll( + public func poll( namespaces: [SnodeAPI.Namespace], - from snode: Snode, - for publicKey: String, + for swarmPublicKey: String, calledFromBackgroundPoller: Bool = false, isBackgroundPollValid: @escaping (() -> Bool) = { true }, - poller: Poller? = nil, - using dependencies: Dependencies = Dependencies() + drainBehaviour: Atomic, + using dependencies: Dependencies ) -> AnyPublisher<[Message], Error> { // If the polling has been cancelled then don't continue guard (calledFromBackgroundPoller && isBackgroundPollValid()) || - poller?.isPolling.wrappedValue[publicKey] == true + isPolling.wrappedValue[swarmPublicKey] == true else { return Just([]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } - let pollerName: String = ( - poller?.pollerName(for: publicKey) ?? - "poller with public key \(publicKey)" - ) - let configHashes: [String] = LibSession.configHashes(for: publicKey) + let pollerName: String = pollerName(for: swarmPublicKey) + let configHashes: [String] = LibSession.configHashes(for: swarmPublicKey) // Fetch the messages - return SnodeAPI - .poll( - namespaces: namespaces, - refreshingConfigHashes: configHashes, - from: snode, - associatedWith: publicKey, - using: dependencies - ) - .flatMap { namespacedResults -> AnyPublisher<[Message], Error> in + return SnodeAPI.getSwarm(for: swarmPublicKey, using: dependencies) + .tryFlatMapWithRandomSnode(drainBehaviour: drainBehaviour, using: dependencies) { snode -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> in + SnodeAPI.poll( + namespaces: namespaces, + refreshingConfigHashes: configHashes, + from: snode, + swarmPublicKey: swarmPublicKey, + using: dependencies + ) + } + .flatMap { [weak self] namespacedResults -> AnyPublisher<[Message], Error> in guard (calledFromBackgroundPoller && isBackgroundPollValid()) || - poller?.isPolling.wrappedValue[publicKey] == true + self?.isPolling.wrappedValue[swarmPublicKey] == true else { return Just([]) .setFailureType(to: Error.self) diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index dfa3a412d..524dd080e 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -473,7 +473,7 @@ public struct ProfileManager { case .failure(let error): SNLog("Updating service with profile failed.") - let isMaxFileSizeExceeded: Bool = ((error as? HTTPError) == .maxFileSizeExceeded) + let isMaxFileSizeExceeded: Bool = ((error as? NetworkError) == .maxFileSizeExceeded) failure?(isMaxFileSizeExceeded ? .avatarUploadMaxFileSizeExceeded : .avatarUploadFailed diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index a5fbf423b..2ad991364 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -101,7 +101,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } // MARK: ------ errors if the data is not a base64 encoded string @@ -124,7 +124,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } // MARK: ------ errors if the signature is not a base64 encoded string @@ -147,7 +147,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } // MARK: ------ errors if the dependencies are not provided to the JSONDecoder @@ -157,7 +157,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } // MARK: ------ errors if the session_id value is not valid @@ -180,7 +180,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } // MARK: ------ that is blinded @@ -249,7 +249,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } } @@ -296,7 +296,7 @@ class SOGSMessageSpec: QuickSpec { expect { try decoder.decode(OpenGroupAPI.Message.self, from: messageData) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index e2b2b1274..6d4df1109 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -502,7 +502,7 @@ class OpenGroupAPISpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) expect(response).to(beNil()) } @@ -528,7 +528,7 @@ class OpenGroupAPISpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) expect(response).to(beNil()) } } @@ -605,7 +605,7 @@ class OpenGroupAPISpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) expect(response).to(beNil()) } @@ -630,7 +630,7 @@ class OpenGroupAPISpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) expect(response).to(beNil()) } } @@ -1593,7 +1593,7 @@ class OpenGroupAPISpec: QuickSpec { } } - expect(preparationError).to(matchError(HTTPError.generic)) + expect(preparationError).to(matchError(NetworkError.generic)) expect(preparedRequest).to(beNil()) } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index ebe50c661..5b697dfbe 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -932,7 +932,7 @@ class OpenGroupManagerSpec: QuickSpec { .mapError { result -> Error in error.setting(to: result) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) } } } @@ -1808,7 +1808,7 @@ class OpenGroupManagerSpec: QuickSpec { it("does nothing if it fails to retrieve the room image") { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: HTTPError.generic).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: NetworkError.generic).eraseToAnyPublisher() ]) testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( @@ -3138,7 +3138,7 @@ class OpenGroupManagerSpec: QuickSpec { .mapError { result -> Error in error.setting(to: result) } .sinkAndStore(in: &disposables) - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) expect(mockNetwork) // First attempt + 8 retries .to(call(.exactly(times: 9)) { $0.send(.onionRequest(any(), to: any(), endpoint: any(), with: any()), using: dependencies) @@ -3158,7 +3158,7 @@ class OpenGroupManagerSpec: QuickSpec { .sinkAndStore(in: &disposables) expect(error) - .to(matchError(HTTPError.parsingFailed)) + .to(matchError(NetworkError.parsingFailed)) expect(mockOGMCache) .to(call(matchingParameters: true) { $0.defaultRoomsPublisher = nil diff --git a/SessionSnodeKit/Database/Models/Snode.swift b/SessionSnodeKit/Database/Models/Snode.swift index 8d9879305..ebe960323 100644 --- a/SessionSnodeKit/Database/Models/Snode.swift +++ b/SessionSnodeKit/Database/Models/Snode.swift @@ -57,7 +57,7 @@ extension Snode { } catch { SNLog("Failed to parse snode: \(error.localizedDescription).") - throw HTTPError.invalidJSON + throw NetworkError.parsingFailed } } } diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift index 5b1096e67..a4a83d994 100644 --- a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift +++ b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift @@ -3,7 +3,7 @@ import Foundation extension SnodeAPI { - public class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody { + public final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case beforeMs = "before" case namespace @@ -77,5 +77,18 @@ extension SnodeAPI { return signatureBytes } + + // MARK: - UpdatableTimestamp + + public func with(timestampMs: UInt64) -> DeleteAllBeforeRequest { + return DeleteAllBeforeRequest( + beforeMs: self.beforeMs, + namespace: self.namespace, + pubkey: self.pubkey, + timestampMs: timestampMs, + ed25519PublicKey: self.ed25519PublicKey, + ed25519SecretKey: self.ed25519SecretKey + ) + } } } diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift index efaf678b1..0d395ebb7 100644 --- a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift +++ b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift @@ -3,7 +3,7 @@ import Foundation extension SnodeAPI { - public class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody { + public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case namespace } @@ -71,5 +71,17 @@ extension SnodeAPI { return signatureBytes } + + // MARK: - UpdatableTimestamp + + public func with(timestampMs: UInt64) -> DeleteAllMessagesRequest { + return DeleteAllMessagesRequest( + namespace: self.namespace, + pubkey: self.pubkey, + timestampMs: timestampMs, + ed25519PublicKey: self.ed25519PublicKey, + ed25519SecretKey: self.ed25519SecretKey + ) + } } } diff --git a/SessionSnodeKit/Models/GetSwarmResponse.swift b/SessionSnodeKit/Models/GetSwarmResponse.swift index 1cc725b1e..c64c07000 100644 --- a/SessionSnodeKit/Models/GetSwarmResponse.swift +++ b/SessionSnodeKit/Models/GetSwarmResponse.swift @@ -87,7 +87,7 @@ extension GetSwarmResponse._Snode { } catch { SNLog("Failed to parse snode: \(error.localizedDescription).") - throw HTTPError.invalidJSON + throw NetworkError.parsingFailed } } } diff --git a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift b/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift index 7e349a719..b72fa9ec1 100644 --- a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift +++ b/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift @@ -12,8 +12,8 @@ public class SnodeAuthenticatedRequestBody: Encodable { case signatureBase64 = "signature" } - private let pubkey: String - private let ed25519PublicKey: [UInt8] + internal let pubkey: String + internal let ed25519PublicKey: [UInt8] internal let ed25519SecretKey: [UInt8] private let subkey: String? internal let timestampMs: UInt64? diff --git a/SessionSnodeKit/Models/SnodeBatchRequest.swift b/SessionSnodeKit/Models/SnodeBatchRequest.swift index 0d5a56466..e6a05c0c1 100644 --- a/SessionSnodeKit/Models/SnodeBatchRequest.swift +++ b/SessionSnodeKit/Models/SnodeBatchRequest.swift @@ -19,7 +19,7 @@ internal extension SnodeAPI { public init(request: SnodeRequest, responseType: R.Type) { self.child = Child(request: request) - self.responseType = HTTP.BatchSubResponse.self + self.responseType = Network.BatchSubResponse.self } public init(request: SnodeRequest) { diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionSnodeKit/Models/SnodeRequest.swift index 480209bfb..ab23b427b 100644 --- a/SessionSnodeKit/Models/SnodeRequest.swift +++ b/SessionSnodeKit/Models/SnodeRequest.swift @@ -9,7 +9,7 @@ public struct SnodeRequest: Encodable { case body = "params" } - public/*internal*/ let endpoint: SnodeAPI.Endpoint + internal let endpoint: SnodeAPI.Endpoint internal let body: T // MARK: - Initialization @@ -31,3 +31,20 @@ public struct SnodeRequest: Encodable { try container.encode(body, forKey: .body) } } + +// MARK: - BatchRequestChildRetrievable + +extension SnodeRequest: BatchRequestChildRetrievable where T: BatchRequestChildRetrievable { + public var requests: [Network.BatchRequest.Child] { body.requests } +} + +// MARK: - UpdatableTimestamp + +extension SnodeRequest: UpdatableTimestamp where T: UpdatableTimestamp { + public func with(timestampMs: UInt64) -> SnodeRequest { + return SnodeRequest( + endpoint: self.endpoint, + body: self.body.with(timestampMs: timestampMs) + ) + } +} diff --git a/SessionSnodeKit/Networking/OnionRequestAPI+Encryption.swift b/SessionSnodeKit/Networking/OnionRequestAPI+Encryption.swift deleted file mode 100644 index c80054165..000000000 --- a/SessionSnodeKit/Networking/OnionRequestAPI+Encryption.swift +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine -import CryptoKit -import SessionUtilitiesKit - -internal extension OnionRequestAPI { - static func encode(ciphertext: Data, json: JSON) -> AnyPublisher { - // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | - guard - JSONSerialization.isValidJSONObject(json), - let jsonAsData = try? JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ]) - else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - let ciphertextSize = Int32(ciphertext.count).littleEndian - let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout.size) } - - return Just(ciphertextSizeAsData + ciphertext + jsonAsData) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. - static func encrypt( - _ payload: Data, - for destination: OnionRequestAPIDestination - ) -> AnyPublisher { - switch destination { - case .snode(let snode): - // Need to wrap the payload for snode requests - return encode(ciphertext: payload, json: [ "headers" : "" ]) - .tryMap { data -> AES.GCM.EncryptionResult in - try AES.GCM.encrypt(data, for: snode.x25519PublicKey) - } - .eraseToAnyPublisher() - - case .server(_, _, let serverX25519PublicKey, _, _): - do { - return Just(try AES.GCM.encrypt(payload, for: serverX25519PublicKey)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - catch { - return Fail(error: error) - .eraseToAnyPublisher() - } - } - } - - /// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. - static func encryptHop( - from lhs: OnionRequestAPIDestination, - to rhs: OnionRequestAPIDestination, - using previousEncryptionResult: AES.GCM.EncryptionResult - ) -> AnyPublisher { - var parameters: JSON - - switch rhs { - case .snode(let snode): - let snodeED25519PublicKey = snode.ed25519PublicKey - parameters = [ "destination" : snodeED25519PublicKey ] - - case .server(let host, let target, _, let scheme, let port): - let scheme = scheme ?? "https" - let port = port ?? (scheme == "https" ? 443 : 80) - parameters = [ "host" : host, "target" : target, "method" : "POST", "protocol" : scheme, "port" : port ] - } - - parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() - - let x25519PublicKey: String = { - switch lhs { - case .snode(let snode): return snode.x25519PublicKey - case .server(_, _, let serverX25519PublicKey, _, _): - return serverX25519PublicKey - } - }() - - return encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters) - .tryMap { data -> AES.GCM.EncryptionResult in try AES.GCM.encrypt(data, for: x25519PublicKey) } - .eraseToAnyPublisher() - } -} diff --git a/SessionSnodeKit/Networking/OnionRequestAPI.swift b/SessionSnodeKit/Networking/OnionRequestAPI.swift index 4e31b7d21..e5ad51e65 100644 --- a/SessionSnodeKit/Networking/OnionRequestAPI.swift +++ b/SessionSnodeKit/Networking/OnionRequestAPI.swift @@ -9,33 +9,71 @@ import GRDB import SessionUtilitiesKit public extension Network.RequestType { - static func onionRequest(_ payload: Data, to snode: Snode, timeout: TimeInterval = HTTP.defaultTimeout) -> Network.RequestType { + static func onionRequest( + _ payload: Data, + to snode: Snode, + swarmPublicKey: String?, + timeout: TimeInterval = Network.defaultTimeout + ) -> Network.RequestType { return Network.RequestType( id: "onionRequest", - url: snode.address, + url: "quic://\(snode.ip):\(snode.lmqPort)", method: "POST", body: payload, - args: [payload, snode, timeout] - ) { OnionRequestAPI.sendOnionRequest(payload, to: snode, timeout: timeout) } + args: [payload, snode, swarmPublicKey, timeout] + ) { + OnionRequestAPI.sendOnionRequest( + with: payload, + to: OnionRequestAPIDestination.snode(snode), + swarmPublicKey: swarmPublicKey, + timeout: timeout, + using: $0 + ) + } } - static func onionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval = HTTP.defaultTimeout) -> Network.RequestType { + static func onionRequest( + _ request: URLRequest, + to server: String, + endpoint: E, + with x25519PublicKey: String, + timeout: TimeInterval = Network.defaultTimeout + ) -> Network.RequestType { return Network.RequestType( id: "onionRequest", url: request.url?.absoluteString, method: request.httpMethod, headers: request.allHTTPHeaderFields, body: request.httpBody, - args: [request, server, x25519PublicKey, timeout] - ) { OnionRequestAPI.sendOnionRequest(request, to: server, with: x25519PublicKey, timeout: timeout) } + args: [request, server, endpoint, x25519PublicKey, timeout] + ) { + guard let url = request.url, let host = request.url?.host else { + return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher() + } + + return OnionRequestAPI.sendOnionRequest( + with: request.httpBody, + to: OnionRequestAPIDestination.server( + method: request.httpMethod, + scheme: url.scheme, + host: host, + endpoint: endpoint, + port: url.port.map { UInt16($0) }, + headers: request.allHTTPHeaderFields, + x25519PublicKey: x25519PublicKey + ), + swarmPublicKey: nil, + timeout: timeout, + using: $0 + ) + } } } /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. public enum OnionRequestAPI { private static var buildPathsPublisher: Atomic?> = Atomic(nil) - private static var pathFailureCount: Atomic<[[Snode]: UInt]> = Atomic([:]) - private static var snodeFailureCount: Atomic<[Snode: UInt]> = Atomic([:]) + internal static var pathFailureCount: Atomic<[[Snode]: UInt]> = Atomic([:]) public static var guardSnodes: Atomic> = Atomic([]) // Not a set to ensure we consistently show the same path to the user @@ -75,30 +113,6 @@ public enum OnionRequestAPI { // MARK: - Private API - /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. - private static func testSnode(_ snode: Snode, using dependencies: Dependencies) -> AnyPublisher { - let url = "\(snode.address):\(snode.port)/get_stats/v1" - let timeout: TimeInterval = 3 // Use a shorter timeout for testing - - return LibSession - .sendRequest( - ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, - snode: snode, - endpoint: SnodeAPI.Endpoint.getInfo.rawValue - ) - .decoded(as: SnodeAPI.GetInfoResponse.self, using: dependencies) - .tryMap { _, response -> Void in - guard let version: Version = response.version else { throw OnionRequestAPIError.missingSnodeVersion } - guard version >= Version(major: 2, minor: 0, patch: 7) else { - SNLog("Unsupported snode version: \(version.stringValue).") - throw OnionRequestAPIError.unsupportedSnodeVersion(version.stringValue) - } - - return () - } - .eraseToAnyPublisher() - } - /// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with /// `Error.insufficientSnodes` if not enough (reliable) snodes are available. private static func getGuardSnodes( @@ -117,7 +131,7 @@ public enum OnionRequestAPI { let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { - return Fail(error: OnionRequestAPIError.insufficientSnodes) + return Fail(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -125,7 +139,7 @@ public enum OnionRequestAPI { // randomElement() uses the system's default random generator, which // is cryptographically secure guard let candidate = unusedSnodes.randomElement() else { - return Fail(error: OnionRequestAPIError.insufficientSnodes) + return Fail(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -133,7 +147,11 @@ public enum OnionRequestAPI { SNLog("Testing guard snode: \(candidate).") // Loop until a reliable guard snode is found - return testSnode(candidate, using: dependencies) + return SnodeAPI + .testSnode( + snode: candidate, + using: dependencies + ) .map { _ in candidate } .catch { _ in return Just(()) @@ -194,7 +212,7 @@ public enum OnionRequestAPI { let pathSnodeCount: UInt = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) guard unusedSnodes.count >= pathSnodeCount else { - return Fail<[[Snode]], Error>(error: OnionRequestAPIError.insufficientSnodes) + return Fail<[[Snode]], Error>(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -294,7 +312,7 @@ public enum OnionRequestAPI { return buildPaths(reusing: paths, using: dependencies) .flatMap { paths in guard let path: [Snode] = paths.filter({ !$0.contains(snode) }).randomElement() else { - return Fail<[Snode], Error>(error: OnionRequestAPIError.insufficientSnodes) + return Fail<[Snode], Error>(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -312,7 +330,7 @@ public enum OnionRequestAPI { .store(in: &cancellable) guard let path: [Snode] = paths.randomElement() else { - return Fail<[Snode], Error>(error: OnionRequestAPIError.insufficientSnodes) + return Fail<[Snode], Error>(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -331,12 +349,12 @@ public enum OnionRequestAPI { .eraseToAnyPublisher() } - return Fail<[Snode], Error>(error: OnionRequestAPIError.insufficientSnodes) + return Fail<[Snode], Error>(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } guard let path: [Snode] = paths.randomElement() else { - return Fail<[Snode], Error>(error: OnionRequestAPIError.insufficientSnodes) + return Fail<[Snode], Error>(error: SnodeAPIError.insufficientSnodes) .eraseToAnyPublisher() } @@ -348,7 +366,7 @@ public enum OnionRequestAPI { } } - private static func dropGuardSnode(_ snode: Snode) { + internal static func dropGuardSnode(_ snode: Snode) { guardSnodes.mutate { snodes in snodes = snodes.filter { $0 != snode } } } @@ -356,14 +374,14 @@ public enum OnionRequestAPI { // We repair the path here because we can do it sync. In the case where we drop a whole // path we leave the re-building up to getPath(excluding:using:) because re-building the path // in that case is async. - OnionRequestAPI.snodeFailureCount.mutate { $0[snode] = 0 } + SnodeAPI.snodeFailureCount.mutate { $0[snode] = 0 } var oldPaths = paths guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return } var path = oldPaths[pathIndex] guard let snodeIndex = path.firstIndex(of: snode) else { return } path.remove(at: snodeIndex) let unusedSnodes = SnodeAPI.snodePool.wrappedValue.subtracting(oldPaths.flatMap { $0 }) - guard !unusedSnodes.isEmpty else { throw OnionRequestAPIError.insufficientSnodes } + guard !unusedSnodes.isEmpty else { throw SnodeAPIError.insufficientSnodes } // randomElement() uses the system's default random generator, which is cryptographically secure path.append(unusedSnodes.randomElement()!) // Don't test the new snode as this would reveal the user's IP @@ -377,7 +395,7 @@ public enum OnionRequestAPI { } } - private static func drop(_ path: [Snode]) { + internal static func drop(_ path: [Snode]) { OnionRequestAPI.pathFailureCount.mutate { $0[path] = 0 } var paths = OnionRequestAPI.paths guard let pathIndex = paths.firstIndex(of: path) else { return } @@ -395,130 +413,14 @@ public enum OnionRequestAPI { try? paths.save(db) } } - - /// Builds an onion around `payload` and returns the result. - private static func buildOnion( - around payload: Data, - targetedAt destination: OnionRequestAPIDestination, - using dependencies: Dependencies - ) -> AnyPublisher { - var guardSnode: Snode! - var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination - var encryptionResult: AES.GCM.EncryptionResult! - var snodeToExclude: Snode? - - if case .snode(let snode) = destination { snodeToExclude = snode } - - return getPath(excluding: snodeToExclude, using: dependencies) - .flatMap { path -> AnyPublisher in - guardSnode = path.first! - - // Encrypt in reverse order, i.e. the destination first - return encrypt(payload, for: destination) - .flatMap { r -> AnyPublisher in - targetSnodeSymmetricKey = r.symmetricKey - - // Recursively encrypt the layers of the onion (again in reverse order) - encryptionResult = r - var path = path - var rhs = destination - - func addLayer() -> AnyPublisher { - guard !path.isEmpty else { - return Just(encryptionResult) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - let lhs = OnionRequestAPIDestination.snode(path.removeLast()) - return OnionRequestAPI - .encryptHop(from: lhs, to: rhs, using: encryptionResult) - .flatMap { r -> AnyPublisher in - encryptionResult = r - rhs = lhs - return addLayer() - } - .eraseToAnyPublisher() - } - - return addLayer() - } - .eraseToAnyPublisher() - } - .map { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) } - .eraseToAnyPublisher() - } - // MARK: - Public API - - /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest( - _ payload: Data, - to snode: Snode, - timeout: TimeInterval = HTTP.defaultTimeout - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - /// **Note:** Currently the service nodes only support V3 Onion Requests - return sendOnionRequest( - with: payload, - to: OnionRequestAPIDestination.snode(snode), - version: .v3, - timeout: timeout - ) - } - - /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest( - _ request: URLRequest, - to server: String, - with x25519PublicKey: String, - timeout: TimeInterval = HTTP.defaultTimeout - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - guard let url = request.url, let host = request.url?.host else { - return Fail(error: OnionRequestAPIError.invalidURL) - .eraseToAnyPublisher() - } - - let scheme: String? = url.scheme - let port: UInt16? = url.port.map { UInt16($0) } - - guard let payload: Data = generateV4Payload(for: request) else { - return Fail(error: OnionRequestAPIError.invalidRequestInfo) - .eraseToAnyPublisher() - } - - return OnionRequestAPI - .sendOnionRequest( - with: payload, - to: OnionRequestAPIDestination.server( - host: host, - target: OnionRequestAPIVersion.v4.rawValue, - x25519PublicKey: x25519PublicKey, - scheme: scheme, - port: port - ), - version: .v4, - timeout: timeout - ) - .handleEvents( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): - SNLog("Couldn't reach server: \(url) due to error: \(error).") - } - } - ) - .eraseToAnyPublisher() - } - - public static func sendOnionRequest( - with payload: Data, + fileprivate static func sendOnionRequest( + with body: Data?, to destination: OnionRequestAPIDestination, - version: OnionRequestAPIVersion, - timeout: TimeInterval = HTTP.defaultTimeout, - using dependencies: Dependencies = Dependencies() + swarmPublicKey: String?, + timeout: TimeInterval, + using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - let snodeToExclude: Snode? = { switch destination { case .snode(let snode): return snode @@ -528,20 +430,15 @@ public enum OnionRequestAPI { return getPath(excluding: snodeToExclude, using: dependencies) .tryFlatMap { path -> AnyPublisher<(ResponseInfoType, Data?), Error> in - guard let guardSnode: Snode = path.first else { throw OnionRequestAPIError.insufficientSnodes } - - return LibSession - .sendOnionRequest( - path: path, - ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, - to: destination, - payload: payload//Data()//body - ) + LibSession.sendOnionRequest( + to: destination, + body: body, + path: path, + swarmPublicKey: swarmPublicKey, + ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, + using: dependencies + ) } - .handleEvents( - receiveCompletion: { result in - } - ) .eraseToAnyPublisher() } @@ -556,7 +453,7 @@ public enum OnionRequestAPI { let endpoint: String = url.path .appending(url.query.map { value in "?\(value)" }) - let requestInfo: HTTP.RequestInfo = HTTP.RequestInfo( + let requestInfo: Network.RequestInfo = Network.RequestInfo( method: (request.httpMethod ?? "GET"), // The default (if nil) is 'GET' endpoint: endpoint, headers: (request.allHTTPHeaderFields ?? [:]) @@ -582,168 +479,4 @@ public enum OnionRequestAPI { return (prefixData + requestInfoData + suffixData) } - - private static func handleResponse( - responseData: Data, - destinationSymmetricKey: Data, - version: OnionRequestAPIVersion, - destination: OnionRequestAPIDestination - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - switch version { - // V2 and V3 Onion Requests have the same structure for responses - case .v2, .v3: - let json: JSON - - if let processedJson = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON { - json = processedJson - } - else if let result: String = String(data: responseData, encoding: .utf8) { - json = [ "result": result ] - } - else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - guard let base64EncodedIVAndCiphertext = json["result"] as? String, let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AES.GCM.ivSize else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - do { - let data = try AES.GCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey) - - guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON, let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - if statusCode == 406 { // Clock out of sync - SNLog("The user's clock is out of sync with the service node network.") - return Fail(error: SnodeAPIError.clockOutOfSync) - .eraseToAnyPublisher() - } - - if statusCode == 401 { // Signature verification failed - SNLog("Failed to verify the signature.") - return Fail(error: SnodeAPIError.signatureVerificationFailed) - .eraseToAnyPublisher() - } - - if let bodyAsString = json["body"] as? String { - guard let bodyAsData = bodyAsString.data(using: .utf8) else { - return Fail(error: HTTPError.invalidResponse) - .eraseToAnyPublisher() - } - guard let body = try? JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { - return Fail( - error: OnionRequestAPIError.httpRequestFailedAtDestination( - statusCode: UInt(statusCode), - data: bodyAsData, - destination: destination - ) - ).eraseToAnyPublisher() - } - - if let timestamp = body["t"] as? Int64 { - let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000)) - SnodeAPI.clockOffsetMs.mutate { $0 = offset } - } - - guard 200...299 ~= statusCode else { - return Fail( - error: OnionRequestAPIError.httpRequestFailedAtDestination( - statusCode: UInt(statusCode), - data: bodyAsData, - destination: destination - ) - ).eraseToAnyPublisher() - } - - return Just((HTTP.ResponseInfo(code: statusCode, headers: [:]), bodyAsData)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - guard 200...299 ~= statusCode else { - return Fail( - error: OnionRequestAPIError.httpRequestFailedAtDestination( - statusCode: UInt(statusCode), - data: data, - destination: destination - ) - ).eraseToAnyPublisher() - } - - return Just((HTTP.ResponseInfo(code: statusCode, headers: [:]), data)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - catch { - return Fail(error: error) - .eraseToAnyPublisher() - } - - // V4 Onion Requests have a very different structure for responses - case .v4: - guard responseData.count >= AES.GCM.ivSize else { - return Fail(error: HTTPError.invalidResponse) - .eraseToAnyPublisher() - } - - do { - let data: Data = try AES.GCM.decrypt(responseData, with: destinationSymmetricKey) - - // Process the bencoded response - guard let processedResponse: (info: ResponseInfoType, body: Data?) = process(bencodedData: data) else { - return Fail(error: HTTPError.invalidResponse) - .eraseToAnyPublisher() - } - - // Custom handle a clock out of sync error (v4 returns '425' but included the '406' - // just in case) - guard processedResponse.info.code != 406 && processedResponse.info.code != 425 else { - SNLog("The user's clock is out of sync with the service node network.") - return Fail(error: SnodeAPIError.clockOutOfSync) - .eraseToAnyPublisher() - } - - guard processedResponse.info.code != 401 else { // Signature verification failed - SNLog("Failed to verify the signature.") - return Fail(error: SnodeAPIError.signatureVerificationFailed) - .eraseToAnyPublisher() - } - - // Handle error status codes - guard 200...299 ~= processedResponse.info.code else { - return Fail(error: OnionRequestAPIError.httpRequestFailedAtDestination( - statusCode: UInt(processedResponse.info.code), - data: data, - destination: destination - )).eraseToAnyPublisher() - } - - return Just(processedResponse) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - catch { - return Fail(error: error) - .eraseToAnyPublisher() - } - } - } - - public static func process(bencodedData data: Data) -> (info: ResponseInfoType, body: Data?)? { - guard let response: BencodeResponse = try? Bencode.decodeResponse(from: data) else { - return nil - } - - // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just - // in case) - guard response.info.code != 406 && response.info.code != 425 else { return nil } - guard response.info.code != 401 else { return nil } - - return (response.info, response.data) - } } diff --git a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift index c0a858850..1b2a6fa01 100644 --- a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift +++ b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift @@ -4,11 +4,11 @@ import Foundation import Combine import SessionUtilitiesKit -public extension HTTP.PreparedRequest { +public extension Network.PreparedRequest { /// Send an onion request for the prepared data func send(using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, R), Error> { // If we have a cached response then user that directly - if let cachedResponse: HTTP.PreparedRequest.CachedResponse = self.cachedResponse { + if let cachedResponse: Network.PreparedRequest.CachedResponse = self.cachedResponse { return Just(cachedResponse) .setFailureType(to: Error.self) .handleEvents( @@ -31,64 +31,64 @@ public extension HTTP.PreparedRequest { .onionRequest( request, to: serverTarget.server, - endpoint: serverTarget.rawEndpoint, + endpoint: serverTarget.endpoint, with: serverTarget.x25519PublicKey, timeout: timeout ), using: dependencies ) - case let snodeTarget as HTTP.SnodeTarget: - guard let payload: Data = request.httpBody else { throw SnodeAPIError.invalidPreparedRequest } + case let snodeTarget as Network.SnodeTarget: + guard let payload: Data = request.httpBody else { throw NetworkError.invalidPreparedRequest } return dependencies.network .send( .onionRequest( payload, to: snodeTarget.snode, - associatedWith: snodeTarget.associatedPublicKey, + swarmPublicKey: snodeTarget.swarmPublicKey, timeout: timeout ), using: dependencies ) - case let randomSnode as HTTP.RandomSnodeTarget: - guard let payload: Data = request.httpBody else { throw SnodeAPIError.invalidPreparedRequest } + case let randomSnode as Network.RandomSnodeTarget: + guard let payload: Data = request.httpBody else { throw NetworkError.invalidPreparedRequest } - return SnodeAPI.getSwarm(for: randomSnode.publicKey, using: dependencies) + return SnodeAPI.getSwarm(for: randomSnode.swarmPublicKey, using: dependencies) .tryFlatMapWithRandomSnode(retry: SnodeAPI.maxRetryCount, using: dependencies) { snode in dependencies.network .send( .onionRequest( payload, to: snode, - associatedWith: randomSnode.publicKey, + swarmPublicKey: randomSnode.swarmPublicKey, timeout: timeout ), using: dependencies ) } - case let randomSnode as HTTP.RandomSnodeLatestNetworkTimeTarget: - guard request.httpBody != nil else { throw SnodeAPIError.invalidPreparedRequest } + case let randomSnode as Network.RandomSnodeLatestNetworkTimeTarget: + guard request.httpBody != nil else { throw NetworkError.invalidPreparedRequest } - return SnodeAPI.getSwarm(for: randomSnode.publicKey, using: dependencies) + return SnodeAPI.getSwarm(for: randomSnode.swarmPublicKey, using: dependencies) .tryFlatMapWithRandomSnode(retry: SnodeAPI.maxRetryCount, using: dependencies) { snode in - try SnodeAPI + SnodeAPI .getNetworkTime(from: snode, using: dependencies) .tryFlatMap { timestampMs in guard let updatedRequest: URLRequest = try? randomSnode .urlRequestWithUpdatedTimestampMs(timestampMs, dependencies), let payload: Data = updatedRequest.httpBody - else { throw SnodeAPIError.invalidPreparedRequest } + else { throw NetworkError.invalidPreparedRequest } return dependencies.network .send( .onionRequest( payload, to: snode, - associatedWith: randomSnode.publicKey, + swarmPublicKey: randomSnode.swarmPublicKey, timeout: timeout ), using: dependencies @@ -106,7 +106,7 @@ public extension HTTP.PreparedRequest { } } - default: throw SnodeAPIError.invalidPreparedRequest + default: throw NetworkError.invalidPreparedRequest } } .decoded(with: self, using: dependencies) @@ -124,9 +124,9 @@ public extension HTTP.PreparedRequest { public extension Optional { func send( using dependencies: Dependencies - ) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == HTTP.PreparedRequest { + ) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == Network.PreparedRequest { guard let instance: Wrapped = self else { - return Fail(error: SnodeAPIError.invalidPreparedRequest) + return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } diff --git a/SessionSnodeKit/Networking/Request+SnodeAPI.swift b/SessionSnodeKit/Networking/Request+SnodeAPI.swift index 9750bafc5..7c66d063b 100644 --- a/SessionSnodeKit/Networking/Request+SnodeAPI.swift +++ b/SessionSnodeKit/Networking/Request+SnodeAPI.swift @@ -7,10 +7,10 @@ import SessionUtilitiesKit // MARK: - SnodeTarget -internal extension HTTP { +internal extension Network { struct SnodeTarget: RequestTarget, Equatable { let snode: Snode - let associatedPublicKey: String + let swarmPublicKey: String? var url: URL? { URL(string: "snode:\(snode.x25519PublicKey)") } var urlPathAndParamsString: String { return "" } @@ -19,27 +19,27 @@ internal extension HTTP { // MARK: - RandomSnodeTarget -internal extension HTTP { +internal extension Network { struct RandomSnodeTarget: RequestTarget, Equatable { - let publicKey: String + let swarmPublicKey: String - var url: URL? { URL(string: "snode:\(publicKey)") } + var url: URL? { URL(string: "snode:\(swarmPublicKey)") } var urlPathAndParamsString: String { return "" } } } // MARK: - RandomSnodeLatestNetworkTimeTarget -internal extension HTTP { +internal extension Network { struct RandomSnodeLatestNetworkTimeTarget: RequestTarget, Equatable { - let publicKey: String + let swarmPublicKey: String let urlRequestWithUpdatedTimestampMs: ((UInt64, Dependencies) throws -> URLRequest) - var url: URL? { URL(string: "snode:\(publicKey)") } + var url: URL? { URL(string: "snode:\(swarmPublicKey)") } var urlPathAndParamsString: String { return "" } - static func == (lhs: HTTP.RandomSnodeLatestNetworkTimeTarget, rhs: HTTP.RandomSnodeLatestNetworkTimeTarget) -> Bool { - lhs.publicKey == rhs.publicKey + static func == (lhs: Network.RandomSnodeLatestNetworkTimeTarget, rhs: Network.RandomSnodeLatestNetworkTimeTarget) -> Bool { + lhs.swarmPublicKey == rhs.swarmPublicKey } } } @@ -53,14 +53,14 @@ public extension Request { snode: Snode, headers: [HTTPHeader: String] = [:], body: T? = nil, - associatedWith publicKey: String + swarmPublicKey: String? ) { self = Request( method: method, endpoint: endpoint, - target: HTTP.SnodeTarget( + target: Network.SnodeTarget( snode: snode, - associatedPublicKey: publicKey + swarmPublicKey: swarmPublicKey ), headers: headers, body: body @@ -74,15 +74,15 @@ public extension Request { init( method: HTTPMethod = .get, endpoint: Endpoint, - publicKey: String, + swarmPublicKey: String, headers: [HTTPHeader: String] = [:], body: T? = nil ) { self = Request( method: method, endpoint: endpoint, - target: HTTP.RandomSnodeTarget( - publicKey: publicKey + target: Network.RandomSnodeTarget( + swarmPublicKey: swarmPublicKey ), headers: headers, body: body @@ -96,7 +96,7 @@ public extension Request { init( method: HTTPMethod = .get, endpoint: Endpoint, - publicKey: String, + swarmPublicKey: String, headers: [HTTPHeader: String] = [:], requiresLatestNetworkTime: Bool, body: T? = nil @@ -104,13 +104,13 @@ public extension Request { self = Request( method: method, endpoint: endpoint, - target: HTTP.RandomSnodeLatestNetworkTimeTarget( - publicKey: publicKey, + target: Network.RandomSnodeLatestNetworkTimeTarget( + swarmPublicKey: swarmPublicKey, urlRequestWithUpdatedTimestampMs: { timestampMs, dependencies in try Request( method: method, endpoint: endpoint, - publicKey: publicKey, + swarmPublicKey: swarmPublicKey, headers: headers, body: body?.with(timestampMs: timestampMs) ).generateUrlRequest(using: dependencies) diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index 4ee67a45a..eedc400ea 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -1,4 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import Combine @@ -9,11 +11,10 @@ import SessionUtilitiesKit public extension Network.RequestType { static func message( _ message: SnodeMessage, - in namespace: SnodeAPI.Namespace, - using dependencies: Dependencies = Dependencies() + in namespace: SnodeAPI.Namespace ) -> Network.RequestType { return Network.RequestType(id: "snodeAPI.sendMessage", args: [message, namespace]) { - SnodeAPI.sendMessage(message, in: namespace, using: dependencies) + SnodeAPI.sendMessage(message, in: namespace, using: $0) } } } @@ -45,29 +46,51 @@ public final class SnodeAPI { // MARK: - Settings - private static let maxRetryCount: Int = 8 + internal static let maxRetryCount: Int = 8 private static let minSwarmSnodeCount: Int = 3 private static let seedNodePool: Set = { guard !Features.useTestnet else { - // public.loki.foundation return [ Snode( ip: "144.76.164.202", - lmqPort: 20200, - x25519PublicKey: "", - ed25519PublicKey: "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef" + lmqPort: 35400, + x25519PublicKey: "80adaead94db3b0402a6057869bdbe63204a28e93589fd95a035480ed6c03b45", + ed25519PublicKey: "decaf007f26d3d6f9b845ad031ffdf6d04638c25bb10b8fffbbe99135303c4b9" ) ] } return [ - // seed2.getsession.org Snode( ip: "144.76.164.202", + lmqPort: 20200, + x25519PublicKey: "be83fe1221fdd85e4d9d2b62e2a34ba82eaf73da45700185d25aff4575ec6018", + ed25519PublicKey: "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef" + ), + Snode( + ip: "88.99.102.229", + lmqPort: 20201, + x25519PublicKey: "05c8c236cf6c4013b8ca930a343fdc62c413ba038a16bb12e75632e0179d404a", + ed25519PublicKey: "1f101f0acee4db6f31aaa8b4df134e85ca8a4878efaef7f971e88ab144c1a7ce" + ), + Snode( + ip: "195.16.73.17", + lmqPort: 20202, + x25519PublicKey: "22ced8efd4e5faf15531e9b9244b2c1de299342892b97d19268c4db69ab6350f", + ed25519PublicKey: "1f202f00f4d2d4acc01e20773999a291cf3e3136c325474d159814e06199919f" + ), + Snode( + ip: "104.194.11.120", lmqPort: 20203, - x25519PublicKey: "", - ed25519PublicKey: "1f003f0b6544c1050c9a052deafdb8cd1b4d2fbbf1dfb9d80f47ee2a0c316112" + x25519PublicKey: "330ad0d67b58f39a6f46fbeaf5c3622860dfa584e9d787f70c3702031712767a", + ed25519PublicKey: "1f303f1d7523c46fa5398826740d13282d26b5de90fbae5749442f66afb6d78b" ), + Snode( + ip: "104.194.8.115", + lmqPort: 20204, + x25519PublicKey: "929c5fc60efa1834a2d4a77a4a33387c1c3d5afc2b192c2ba0e040b29388b216", + ed25519PublicKey: "1f604f1c858a121a681d8f9b470ef72e6946ee1b9c5ad15a35e16b50c28db7b0" + ) ] }() private static let snodeFailureThreshold: Int = 3 @@ -107,7 +130,7 @@ public final class SnodeAPI { newValue.forEach { try? $0.save(db) } } - private static func dropSnodeFromSnodePool(_ snode: Snode) { + internal static func dropSnodeFromSnodePool(_ snode: Snode) { var snodePool = SnodeAPI.snodePool.wrappedValue snodePool.remove(snode) setSnodePool(to: snodePool) @@ -134,7 +157,7 @@ public final class SnodeAPI { loadedSwarms.mutate { $0.insert(publicKey) } } - private static func setSwarm(to newValue: Set, for publicKey: String, persist: Bool = true) { + internal static func setSwarm(to newValue: Set, for publicKey: String, persist: Bool = true) { swarmCache.mutate { $0[publicKey] = newValue } guard persist else { return } @@ -151,7 +174,7 @@ public final class SnodeAPI { setSwarm(to: swarm, for: publicKey) } - // MARK: - Public API + // MARK: - Snode API public static func hasCachedSnodesIncludingExpired() -> Bool { loadSnodePoolIfNeeded() @@ -225,117 +248,51 @@ public final class SnodeAPI { } } - public static func getSessionID( - for onsName: String, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { - let validationCount = 3 - - // The name must be lowercased - let onsName = onsName.lowercased() - - // Hash the ONS name using BLAKE2b - let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) - - guard let nameHash = sodium.wrappedValue.genericHash.hash(message: nameAsData) else { - return Fail(error: SnodeAPIError.hashingFailed) - .eraseToAnyPublisher() - } - - // Ask 3 different snodes for the Session ID associated with the given name hash - let base64EncodedNameHash = nameHash.toBase64() - - return Publishers - .MergeMany( - (0.. AnyPublisher in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .oxenDaemonRPCCall, - body: OxenDaemonRPCRequest( - endpoint: .daemonOnsResolve, - body: ONSResolveRequest( - type: 0, // type 0 means Session - base64EncodedNameHash: base64EncodedNameHash - ) - ) - ), - to: snode, - associatedWith: nil, - using: dependencies - ) - .decoded(as: ONSResolveResponse.self) - .tryMap { _, response -> String in - try response.sessionId( - sodium: sodium.wrappedValue, - nameBytes: nameAsData, - nameHashBytes: nameHash - ) - } - .retry(4) - .eraseToAnyPublisher() - } - } - ) - .collect() - .tryMap { results -> String in - guard results.count == validationCount, Set(results).count == 1 else { - throw SnodeAPIError.validationFailed - } - - return results[0] - } - .eraseToAnyPublisher() - } - public static func getSwarm( - for publicKey: String, + for swarmPublicKey: String, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher, Error> { - loadSwarmIfNeeded(for: publicKey) + loadSwarmIfNeeded(for: swarmPublicKey) - if let cachedSwarm = swarmCache.wrappedValue[publicKey], cachedSwarm.count >= minSwarmSnodeCount { + if let cachedSwarm = swarmCache.wrappedValue[swarmPublicKey], cachedSwarm.count >= minSwarmSnodeCount { return Just(cachedSwarm) .setFailureType(to: Error.self) .eraseToAnyPublisher() } - SNLog("Getting swarm for: \((publicKey == getUserHexEncodedPublicKey()) ? "self" : publicKey).") + SNLog("Getting swarm for: \((swarmPublicKey == getUserHexEncodedPublicKey()) ? "self" : swarmPublicKey).") return getRandomSnode() - .flatMap { snode in - SnodeAPI.send( - request: SnodeRequest( - endpoint: .getSwarm, - body: GetSwarmRequest(pubkey: publicKey) - ), - to: snode, - associatedWith: publicKey, - using: dependencies - ) - .retry(4) - .eraseToAnyPublisher() + .tryFlatMap { snode in + try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .getSwarm, + snode: snode, + swarmPublicKey: swarmPublicKey, + body: GetSwarmRequest(pubkey: swarmPublicKey) + ), + responseType: GetSwarmResponse.self, + using: dependencies + ) + .send(using: dependencies) + .retry(4) + .map { _, response in response.snodes } + .handleEvents( + receiveOutput: { snodes in setSwarm(to: snodes, for: swarmPublicKey) } + ) + .eraseToAnyPublisher() } - .decoded(as: GetSwarmResponse.self, using: dependencies) - .map { _, response in response.snodes } - .handleEvents( - receiveOutput: { snodes in setSwarm(to: snodes, for: publicKey) } - ) - .eraseToAnyPublisher() } - // MARK: - Retrieve + // MARK: - Batching & Polling public static func poll( namespaces: [SnodeAPI.Namespace], refreshingConfigHashes: [String] = [], from snode: Snode, - associatedWith publicKey: String, - using dependencies: Dependencies = Dependencies() + swarmPublicKey: String, + using dependencies: Dependencies ) -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> { guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { return Fail(error: SnodeAPIError.noKeyPair) @@ -355,7 +312,7 @@ public final class SnodeAPI { SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo( for: snode, namespace: namespace, - associatedWith: publicKey, + associatedWith: swarmPublicKey, using: dependencies ) @@ -363,21 +320,22 @@ public final class SnodeAPI { .fetchLastNotExpired( for: snode, namespace: namespace, - associatedWith: publicKey, + associatedWith: swarmPublicKey, using: dependencies )? .hash } } - .flatMap { namespaceLastHash -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> in - var requests: [SnodeAPI.BatchRequest.Info] = [] - + .tryFlatMap { namespaceLastHash -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> in + var requests: [any ErasedPreparedRequest] = [] + // If we have any config hashes to refresh TTLs then add those requests first if !refreshingConfigHashes.isEmpty { requests.append( - BatchRequest.Info( - request: SnodeRequest( + try SnodeAPI.prepareRequest( + request: Request( endpoint: .expire, + swarmPublicKey: swarmPublicKey, body: UpdateExpiryRequest( messageHashes: refreshingConfigHashes, expiryMs: UInt64( @@ -391,7 +349,8 @@ public final class SnodeAPI { subkey: nil // TODO: Need to get this ) ), - responseType: UpdateExpiryResponse.self + responseType: UpdateExpiryResponse.self, + using: dependencies ) ) } @@ -399,17 +358,18 @@ public final class SnodeAPI { // Determine the maxSize each namespace in the request should take up let namespaceMaxSizeMap: [SnodeAPI.Namespace: Int64] = SnodeAPI.Namespace.maxSizeMap(for: namespaces) let fallbackSize: Int64 = (namespaceMaxSizeMap.values.min() ?? 1) - + // Add the various 'getMessages' requests requests.append( - contentsOf: namespaces.map { namespace -> SnodeAPI.BatchRequest.Info in + contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in // Check if this namespace requires authentication guard namespace.requiresReadAuthentication else { - return BatchRequest.Info( - request: SnodeRequest( + return try SnodeAPI.prepareRequest( + request: Request( endpoint: .getMessages, + swarmPublicKey: swarmPublicKey, body: LegacyGetMessagesRequest( - pubkey: publicKey, + pubkey: swarmPublicKey, lastHash: (namespaceLastHash[namespace] ?? ""), namespace: namespace, maxCount: nil, @@ -417,17 +377,19 @@ public final class SnodeAPI { .defaulting(to: fallbackSize) ) ), - responseType: GetMessagesResponse.self + responseType: GetMessagesResponse.self, + using: dependencies ) } - - return BatchRequest.Info( - request: SnodeRequest( + + return try SnodeAPI.prepareRequest( + request: Request( endpoint: .getMessages, + swarmPublicKey: swarmPublicKey, body: GetMessagesRequest( lastHash: (namespaceLastHash[namespace] ?? ""), namespace: namespace, - pubkey: publicKey, + pubkey: swarmPublicKey, subkey: nil, // TODO: Need to get this timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), ed25519PublicKey: userED25519KeyPair.publicKey, @@ -436,38 +398,37 @@ public final class SnodeAPI { .defaulting(to: fallbackSize) ) ), - responseType: GetMessagesResponse.self + responseType: GetMessagesResponse.self, + using: dependencies ) } ) - + // Actually send the request - let responseTypes = requests.map { $0.responseType } - - return SnodeAPI - .send( - request: SnodeRequest( + return try SnodeAPI + .prepareRequest( + request: Request( endpoint: .batch, - body: BatchRequest(requests: requests) + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) ), - to: snode, - associatedWith: publicKey, + responseType: Network.BatchResponse.self, + requireAllBatchResponses: true, using: dependencies ) - .decoded(as: responseTypes, using: dependencies) - .map { (batchResponse: HTTP.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)] in - let messageResponses: [HTTP.BatchSubResponse] = batchResponse.responses - .compactMap { $0 as? HTTP.BatchSubResponse } + .send(using: dependencies) + .map { (_: ResponseInfoType, batchResponse: Network.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)] in + let messageResponses: [Network.BatchSubResponse] = batchResponse + .compactMap { $0 as? Network.BatchSubResponse } /// Since we have extended the TTL for a number of messages we need to make sure we update the local /// `SnodeReceivedMessageInfo.expirationDateMs` values so we don't end up deleting them /// incorrectly before they actually expire on the swarm if !refreshingConfigHashes.isEmpty, - let refreshTTLSubReponse: HTTP.BatchSubResponse = batchResponse - .responses - .first(where: { $0 is HTTP.BatchSubResponse }) - .asType(HTTP.BatchSubResponse.self), + let refreshTTLSubReponse: Network.BatchSubResponse = batchResponse + .first(where: { $0 is Network.BatchSubResponse }) + .asType(Network.BatchSubResponse.self), let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body, let validResults: [String: UpdateExpiryResponseResult] = try? refreshTTLResponse.validResultMap( sodium: sodium.wrappedValue, @@ -496,17 +457,17 @@ public final class SnodeAPI { return zip(namespaces, messageResponses) .reduce(into: [:]) { result, next in guard let messageResponse: GetMessagesResponse = next.1.body else { return } - + let namespace: SnodeAPI.Namespace = next.0 result[namespace] = ( - info: next.1.responseInfo, + info: next.1, data: ( messages: messageResponse.messages .compactMap { rawMessage -> SnodeReceivedMessage? in SnodeReceivedMessage( snode: snode, - publicKey: publicKey, + publicKey: swarmPublicKey, namespace: namespace, rawMessage: rawMessage ) @@ -517,8 +478,8 @@ public final class SnodeAPI { } } .eraseToAnyPublisher() - } - .eraseToAnyPublisher() + } + .eraseToAnyPublisher() } /// **Note:** This is the direct request to retrieve messages so should be retrieved automatically from the `poll()` method, in order to call @@ -527,7 +488,7 @@ public final class SnodeAPI { public static func getMessages( in namespace: SnodeAPI.Namespace, from snode: Snode, - associatedWith publicKey: String, + swarmPublicKey: String, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<(info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?), Error> { return Deferred { @@ -536,7 +497,7 @@ public final class SnodeAPI { SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo( for: snode, namespace: namespace, - associatedWith: publicKey, + associatedWith: swarmPublicKey, using: dependencies ) @@ -544,7 +505,7 @@ public final class SnodeAPI { .fetchLastNotExpired( for: snode, namespace: namespace, - associatedWith: publicKey, + associatedWith: swarmPublicKey, using: dependencies )? .hash @@ -553,25 +514,25 @@ public final class SnodeAPI { } } .tryFlatMap { lastHash -> AnyPublisher<(info: ResponseInfoType, data: GetMessagesResponse?, lastHash: String?), Error> in - guard namespace.requiresReadAuthentication else { - return SnodeAPI - .send( - request: SnodeRequest( + return try SnodeAPI + .prepareRequest( + request: Request( endpoint: .getMessages, + snode: snode, + swarmPublicKey: swarmPublicKey, body: LegacyGetMessagesRequest( - pubkey: publicKey, + pubkey: swarmPublicKey, lastHash: (lastHash ?? ""), namespace: namespace, maxCount: nil, maxSize: nil ) ), - to: snode, - associatedWith: publicKey, + responseType: GetMessagesResponse.self, using: dependencies ) - .decoded(as: GetMessagesResponse.self, using: dependencies) + .send(using: dependencies) .map { info, data in (info, data, lastHash) } .eraseToAnyPublisher() } @@ -580,25 +541,26 @@ public final class SnodeAPI { throw SnodeAPIError.noKeyPair } - return SnodeAPI - .send( - request: SnodeRequest( + return try SnodeAPI + .prepareRequest( + request: Request( endpoint: .getMessages, + snode: snode, + swarmPublicKey: swarmPublicKey, body: GetMessagesRequest( lastHash: (lastHash ?? ""), namespace: namespace, - pubkey: publicKey, + pubkey: swarmPublicKey, subkey: nil, timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), ed25519PublicKey: userED25519KeyPair.publicKey, ed25519SecretKey: userED25519KeyPair.secretKey ) ), - to: snode, - associatedWith: publicKey, + responseType: GetMessagesResponse.self, using: dependencies ) - .decoded(as: GetMessagesResponse.self, using: dependencies) + .send(using: dependencies) .map { info, data in (info, data, lastHash) } .eraseToAnyPublisher() } @@ -611,7 +573,7 @@ public final class SnodeAPI { .compactMap { rawMessage -> SnodeReceivedMessage? in SnodeReceivedMessage( snode: snode, - publicKey: publicKey, + publicKey: swarmPublicKey, namespace: namespace, rawMessage: rawMessage ) @@ -624,9 +586,77 @@ public final class SnodeAPI { .eraseToAnyPublisher() } + public static func getSessionID( + for onsName: String, + using dependencies: Dependencies = Dependencies() + ) -> AnyPublisher { + let validationCount = 3 + + // The name must be lowercased + let onsName = onsName.lowercased() + + // Hash the ONS name using BLAKE2b + let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) + + guard let nameHash = sodium.wrappedValue.genericHash.hash(message: nameAsData) else { + return Fail(error: SnodeAPIError.hashingFailed) + .eraseToAnyPublisher() + } + + // Ask 3 different snodes for the Session ID associated with the given name hash + let base64EncodedNameHash = nameHash.toBase64() + + return Publishers + .MergeMany( + (0.. AnyPublisher in + try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .oxenDaemonRPCCall, + snode: snode, + body: OxenDaemonRPCRequest( + endpoint: .daemonOnsResolve, + body: ONSResolveRequest( + type: 0, // type 0 means Session + base64EncodedNameHash: base64EncodedNameHash + ) + ) + ), + responseType: ONSResolveResponse.self, + using: dependencies + ) + .tryMap { _, response -> String in + try response.sessionId( + sodium: sodium.wrappedValue, + nameBytes: nameAsData, + nameHashBytes: nameHash + ) + } + .send(using: dependencies) + .retry(4) + .map { _, sessionId in sessionId } + .eraseToAnyPublisher() + } + } + ) + .collect() + .tryMap { results -> String in + guard results.count == validationCount, Set(results).count == 1 else { + throw SnodeAPIError.validationFailed + } + + return results[0] + } + .eraseToAnyPublisher() + } + public static func getExpiries( from snode: Snode, - associatedWith publicKey: String, + swarmPublicKey: String, of serverHashes: [String], using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<(ResponseInfoType, GetExpiriesResponse), Error> { @@ -640,25 +670,28 @@ public final class SnodeAPI { // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - return SnodeAPI - .send( - request: SnodeRequest( - endpoint: .getExpiries, - body: GetExpiriesRequest( - messageHashes: serverHashes, - pubkey: publicKey, - subkey: nil, - timestampMs: sendTimestamp, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - to: snode, - associatedWith: publicKey, - using: dependencies - ) - .decoded(as: GetExpiriesResponse.self, using: dependencies) - .eraseToAnyPublisher() + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .getExpiries, + snode: snode, + swarmPublicKey: swarmPublicKey, + body: GetExpiriesRequest( + messageHashes: serverHashes, + pubkey: swarmPublicKey, + subkey: nil, + timestampMs: sendTimestamp, + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + ), + responseType: GetExpiriesResponse.self, + using: dependencies + ) + .send(using: dependencies) + } + catch { return Fail(error: error).eraseToAnyPublisher() } } // MARK: - Store @@ -668,80 +701,73 @@ public final class SnodeAPI { in namespace: Namespace, using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, SendMessagesResponse), Error> { - let publicKey: String = message.recipient + let swarmPublicKey: String = message.recipient let userX25519PublicKey: String = getUserHexEncodedPublicKey() - let sendTimestamp: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs()) - // Create a convenience method to send a message to an individual Snode - func sendMessage(to snode: Snode) throws -> AnyPublisher<(any ResponseInfoType, SendMessagesResponse), Error> { - guard namespace.requiresWriteAuthentication else { - return SnodeAPI - .send( - request: SnodeRequest( + do { + let request: Network.PreparedRequest = try { + // Check if this namespace requires authentication + guard namespace.requiresWriteAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( endpoint: .sendMessage, + swarmPublicKey: swarmPublicKey, body: LegacySendMessagesRequest( message: message, namespace: namespace ) ), - to: snode, - associatedWith: publicKey, + responseType: SendMessagesResponse.self, using: dependencies ) - .decoded(as: SendMessagesResponse.self, using: dependencies) - .eraseToAnyPublisher() - } - - guard let userED25519KeyPair: KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else { - throw SnodeAPIError.noKeyPair - } - - return SnodeAPI - .send( - request: SnodeRequest( + } + + guard let userED25519KeyPair: KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else { + throw SnodeAPIError.noKeyPair + } + + return try SnodeAPI.prepareRequest( + request: Request( endpoint: .sendMessage, + swarmPublicKey: swarmPublicKey, body: SendMessageRequest( message: message, namespace: namespace, subkey: nil, - timestampMs: sendTimestamp, + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), ed25519PublicKey: userED25519KeyPair.publicKey, ed25519SecretKey: userED25519KeyPair.secretKey ) ), - to: snode, - associatedWith: publicKey, + responseType: SendMessagesResponse.self, using: dependencies ) - .decoded(as: SendMessagesResponse.self, using: dependencies) - .eraseToAnyPublisher() + }() + + return request + .tryMap { info, response -> SendMessagesResponse in + try response.validateResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: userX25519PublicKey + ) + + return response + } + .send(using: dependencies) } - - return getSwarm(for: publicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<(ResponseInfoType, SendMessagesResponse), Error> in - try sendMessage(to: snode) - .tryMap { info, response -> (ResponseInfoType, SendMessagesResponse) in - try response.validateResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: userX25519PublicKey - ) - - return (info, response) - } - .eraseToAnyPublisher() - } + catch { return Fail(error: error).eraseToAnyPublisher() } } public static func sendConfigMessages( _ messages: [(message: SnodeMessage, namespace: Namespace)], allObsoleteHashes: [String], using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { + ) -> AnyPublisher { guard !messages.isEmpty, let recipient: String = messages.first?.message.recipient else { - return Fail(error: SnodeAPIError.generic) + return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } // TODO: Need to get either the closed group subKey or the userEd25519 key for auth @@ -750,83 +776,88 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - let userX25519PublicKey: String = getUserHexEncodedPublicKey() - let publicKey: String = recipient - var requests: [SnodeAPI.BatchRequest.Info] = messages - .map { message, namespace in - // Check if this namespace requires authentication - guard namespace.requiresWriteAuthentication else { - return BatchRequest.Info( - request: SnodeRequest( + do { + let userX25519PublicKey: String = getUserHexEncodedPublicKey() + let swarmPublicKey: String = recipient + var requests: [any ErasedPreparedRequest] = try messages + .map { message, namespace in + // Check if this namespace requires authentication + guard namespace.requiresWriteAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .sendMessage, + swarmPublicKey: swarmPublicKey, + body: LegacySendMessagesRequest( + message: message, + namespace: namespace + ) + ), + responseType: SendMessagesResponse.self, + using: dependencies + ) + } + + return try SnodeAPI.prepareRequest( + request: Request( endpoint: .sendMessage, - body: LegacySendMessagesRequest( + swarmPublicKey: swarmPublicKey, + body: SendMessageRequest( message: message, - namespace: namespace + namespace: namespace, + subkey: nil, // TODO: Need to get this + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey ) ), - responseType: SendMessagesResponse.self + responseType: SendMessagesResponse.self, + using: dependencies ) } - - return BatchRequest.Info( - request: SnodeRequest( - endpoint: .sendMessage, - body: SendMessageRequest( - message: message, - namespace: namespace, - subkey: nil, // TODO: Need to get this - timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - responseType: SendMessagesResponse.self + + // If we had any previous config messages then we should delete them + if !allObsoleteHashes.isEmpty { + requests.append( + try SnodeAPI.prepareRequest( + request: Request( + endpoint: .deleteMessages, + swarmPublicKey: swarmPublicKey, + body: DeleteMessagesRequest( + messageHashes: allObsoleteHashes, + requireSuccessfulDeletion: false, + pubkey: userX25519PublicKey, + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + ), + responseType: DeleteMessagesResponse.self, + using: dependencies + ) ) } - - // If we had any previous config messages then we should delete them - if !allObsoleteHashes.isEmpty { - requests.append( - BatchRequest.Info( - request: SnodeRequest( - endpoint: .deleteMessages, - body: DeleteMessagesRequest( - messageHashes: allObsoleteHashes, - requireSuccessfulDeletion: false, - pubkey: userX25519PublicKey, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .sequence, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) ), - responseType: DeleteMessagesResponse.self + responseType: Network.BatchResponse.self, + requireAllBatchResponses: false, + using: dependencies ) - ) + .send(using: dependencies) + .map { _, response in response } + .eraseToAnyPublisher() } - - let responseTypes = requests.map { $0.responseType } - - return getSwarm(for: publicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .sequence, - body: BatchRequest(requests: requests) - ), - to: snode, - associatedWith: publicKey, - using: dependencies - ) - .eraseToAnyPublisher() - .decoded(as: responseTypes, requireAllResults: false, using: dependencies) - .eraseToAnyPublisher() - } + catch { return Fail(error: error).eraseToAnyPublisher() } } // MARK: - Edit public static func updateExpiry( - publicKey: String, + swarmPublicKey: String, serverHashes: [String], updatedExpiryMs: Int64, shortenOnly: Bool? = nil, @@ -840,48 +871,48 @@ public final class SnodeAPI { // ShortenOnly and extendOnly cannot be true at the same time guard shortenOnly == nil || extendOnly == nil else { - return Fail(error: SnodeAPIError.generic) + return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - return getSwarm(for: publicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: UpdateExpiryResponseResult], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .expire, - body: UpdateExpiryRequest( - messageHashes: serverHashes, - expiryMs: UInt64(updatedExpiryMs), - shorten: shortenOnly, - extend: extendOnly, - pubkey: publicKey, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey, - subkey: nil - ) - ), - to: snode, - associatedWith: publicKey, - using: dependencies - ) - .decoded(as: UpdateExpiryResponse.self, using: dependencies) - .tryMap { _, response -> [String: UpdateExpiryResponseResult] in - try response.validResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: getUserHexEncodedPublicKey(), - validationData: serverHashes + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .expire, + swarmPublicKey: swarmPublicKey, + body: UpdateExpiryRequest( + messageHashes: serverHashes, + expiryMs: UInt64(updatedExpiryMs), + shorten: shortenOnly, + extend: extendOnly, + pubkey: swarmPublicKey, + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey, + subkey: nil ) - } - .eraseToAnyPublisher() - } + ), + responseType: UpdateExpiryResponse.self, + using: dependencies + ) + .send(using: dependencies) + .tryMap { _, response -> [String: UpdateExpiryResponseResult] in + try response.validResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: getUserHexEncodedPublicKey(), + validationData: serverHashes + ) + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } public static func revokeSubkey( - publicKey: String, + swarmPublicKey: String, subkeyToRevoke: String, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { @@ -890,41 +921,41 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - return getSwarm(for: publicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .revokeSubkey, - body: RevokeSubkeyRequest( - subkeyToRevoke: subkeyToRevoke, - pubkey: publicKey, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - to: snode, - associatedWith: publicKey, - using: dependencies - ) - .decoded(as: RevokeSubkeyResponse.self, using: dependencies) - .tryMap { _, response -> Void in - try response.validateResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: getUserHexEncodedPublicKey(), - validationData: subkeyToRevoke + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .revokeSubaccount, + swarmPublicKey: swarmPublicKey, + body: RevokeSubkeyRequest( + subkeyToRevoke: subkeyToRevoke, + pubkey: swarmPublicKey, + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey ) - - return () - } - .eraseToAnyPublisher() - } + ), + responseType: RevokeSubkeyResponse.self, + using: dependencies + ) + .send(using: dependencies) + .tryMap { _, response -> Void in + try response.validateResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: getUserHexEncodedPublicKey(), + validationData: subkeyToRevoke + ) + + return () + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } // MARK: Delete public static func deleteMessages( - publicKey: String, + swarmPublicKey: String, serverHashes: [String], using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<[String: Bool], Error> { @@ -935,47 +966,47 @@ public final class SnodeAPI { let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - return getSwarm(for: publicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .deleteMessages, - body: DeleteMessagesRequest( - messageHashes: serverHashes, - requireSuccessfulDeletion: false, - pubkey: userX25519PublicKey, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - to: snode, - associatedWith: publicKey, - using: dependencies + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteMessages, + swarmPublicKey: swarmPublicKey, + body: DeleteMessagesRequest( + messageHashes: serverHashes, + requireSuccessfulDeletion: false, + pubkey: userX25519PublicKey, + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + ), + responseType: DeleteMessagesResponse.self, + using: dependencies + ) + .send(using: dependencies) + .tryMap { _, response -> [String: Bool] in + let validResultMap: [String: Bool] = try response.validResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: userX25519PublicKey, + validationData: serverHashes ) - .decoded(as: DeleteMessagesResponse.self, using: dependencies) - .tryMap { _, response -> [String: Bool] in - let validResultMap: [String: Bool] = try response.validResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: userX25519PublicKey, - validationData: serverHashes + + // If `validResultMap` didn't throw then at least one service node + // deleted successfully so we should mark the hash as invalid so we + // don't try to fetch updates using that hash going forward (if we + // do we would end up re-fetching all old messages) + Storage.shared.writeAsync { db in + try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: serverHashes ) - - // If `validResultMap` didn't throw then at least one service node - // deleted successfully so we should mark the hash as invalid so we - // don't try to fetch updates using that hash going forward (if we - // do we would end up re-fetching all old messages) - Storage.shared.writeAsync { db in - try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( - db, - potentiallyInvalidHashes: serverHashes - ) - } - - return validResultMap } - .eraseToAnyPublisher() - } + + return validResultMap + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. @@ -990,38 +1021,39 @@ public final class SnodeAPI { let userX25519PublicKey: String = getUserHexEncodedPublicKey() - return getSwarm(for: userX25519PublicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in - getNetworkTime(from: snode) - .flatMap { timestampMs -> AnyPublisher<[String: Bool], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .deleteAll, - body: DeleteAllMessagesRequest( - namespace: namespace, - pubkey: userX25519PublicKey, - timestampMs: timestampMs, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - to: snode, - associatedWith: nil, - using: dependencies - ) - .decoded(as: DeleteAllMessagesResponse.self, using: dependencies) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: userX25519PublicKey, - validationData: timestampMs - ) - } - .eraseToAnyPublisher() + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAll, + swarmPublicKey: userX25519PublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllMessagesRequest( + namespace: namespace, + pubkey: userX25519PublicKey, + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + ), + responseType: DeleteAllMessagesResponse.self, + using: dependencies + ) + .send(using: dependencies) + .tryMap { info, response -> [String: Bool] in + guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else { + throw NetworkError.invalidResponse } - .eraseToAnyPublisher() - } + + return try response.validResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: userX25519PublicKey, + validationData: targetInfo.timestampMs + ) + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. @@ -1037,67 +1069,67 @@ public final class SnodeAPI { let userX25519PublicKey: String = getUserHexEncodedPublicKey() - return getSwarm(for: userX25519PublicKey) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in - getNetworkTime(from: snode) - .flatMap { timestampMs -> AnyPublisher<[String: Bool], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .deleteAllBefore, - body: DeleteAllBeforeRequest( - beforeMs: beforeMs, - namespace: namespace, - pubkey: userX25519PublicKey, - timestampMs: timestampMs, - ed25519PublicKey: userED25519KeyPair.publicKey, - ed25519SecretKey: userED25519KeyPair.secretKey - ) - ), - to: snode, - associatedWith: nil, - using: dependencies - ) - .decoded(as: DeleteAllBeforeResponse.self, using: dependencies) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - sodium: sodium.wrappedValue, - userX25519PublicKey: userX25519PublicKey, - validationData: beforeMs - ) - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAllBefore, + swarmPublicKey: userX25519PublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllBeforeRequest( + beforeMs: beforeMs, + namespace: namespace, + pubkey: userX25519PublicKey, + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), + ed25519PublicKey: userED25519KeyPair.publicKey, + ed25519SecretKey: userED25519KeyPair.secretKey + ) + ), + responseType: DeleteAllBeforeResponse.self, + using: dependencies + ) + .send(using: dependencies) + .tryMap { _, response -> [String: Bool] in + try response.validResultMap( + sodium: sodium.wrappedValue, + userX25519PublicKey: userX25519PublicKey, + validationData: beforeMs + ) + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } // MARK: - Internal API public static func getNetworkTime( from snode: Snode, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) -> AnyPublisher { - return SnodeAPI - .send( - request: SnodeRequest<[String: String]>( - endpoint: .getInfo, - body: [:] - ), - to: snode, - associatedWith: nil, - using: dependencies - ) - .decoded(as: GetNetworkTimestampResponse.self, using: dependencies) - .map { _, response in - // Assume we've fetched the networkTime in order to send a message to the specified snode, in - // which case we want to update the 'clockOffsetMs' value for subsequent requests - let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) - SnodeAPI.clockOffsetMs.mutate { $0 = offset } - - return response.timestamp - } - .eraseToAnyPublisher() + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .getInfo, + snode: snode, + body: [String: String]() + ), + responseType: GetNetworkTimestampResponse.self, + using: dependencies + ) + .send(using: dependencies) + .map { _, response in + // Assume we've fetched the networkTime in order to send a message to the specified snode, in + // which case we want to update the 'clockOffsetMs' value for subsequent requests + let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + SnodeAPI.clockOffsetMs.mutate { $0 = offset } + + return response.timestamp + } + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } internal static func getRandomSnode() -> AnyPublisher { @@ -1107,57 +1139,6 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - private static func getSnodePoolFromSeedNode( - using dependencies: Dependencies - ) -> AnyPublisher, Error> { - guard let targetSeedNode: Snode = seedNodePool.randomElement() else { - return Fail(error: SnodeAPIError.snodePoolUpdatingFailed) - .eraseToAnyPublisher() - } - - SNLog("Populating snode pool using seed node: \(targetSeedNode).") - - return LibSession - .sendRequest( - ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, - snode: targetSeedNode, - endpoint: SnodeAPI.Endpoint.jsonGetNServiceNodes.rawValue, - payload: GetServiceNodesRequest( - activeOnly: true, - limit: 256, - fields: GetServiceNodesRequest.Fields( - publicIp: true, - pubkeyEd25519: true, - pubkeyX25519: true, - storageLmqPort: true - ) - ) - ) - .decoded(as: SnodePoolResponse.self, using: dependencies) - .mapError { error in - switch error { - case HTTPError.parsingFailed: return SnodeAPIError.snodePoolUpdatingFailed - default: return error - } - } - .map { _, snodePool -> Set in - snodePool.result - .serviceNodeStates - .compactMap { $0.value } - .asSet() - } - .retry(2) - .handleEvents( - receiveCompletion: { result in - switch result { - case .finished: SNLog("Got snode pool from seed node: \(targetSeedNode).") - case .failure: SNLog("Failed to contact seed node at: \(targetSeedNode).") - } - } - ) - .eraseToAnyPublisher() - } - private static func getSnodePoolFromSnode( using dependencies: Dependencies ) -> AnyPublisher, Error> { @@ -1177,45 +1158,48 @@ public final class SnodeAPI { // Don't specify a limit in the request. Service nodes return a shuffled // list of nodes so if we specify a limit the 3 responses we get might have // very little overlap. - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .oxenDaemonRPCCall, - body: OxenDaemonRPCRequest( - endpoint: .daemonGetServiceNodes, - body: GetServiceNodesRequest( - activeOnly: true, - limit: nil, - fields: GetServiceNodesRequest.Fields( - publicIp: true, - pubkeyEd25519: true, - pubkeyX25519: true, - storageLmqPort: true + do { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .oxenDaemonRPCCall, + snode: snode, + body: OxenDaemonRPCRequest( + endpoint: .daemonGetServiceNodes, + body: GetServiceNodesRequest( + activeOnly: true, + limit: nil, + fields: GetServiceNodesRequest.Fields( + publicIp: true, + pubkeyEd25519: true, + pubkeyX25519: true, + storageLmqPort: true + ) ) ) - ) - ), - to: snode, - associatedWith: nil, - using: dependencies - ) - .decoded(as: SnodePoolResponse.self, using: dependencies) - .mapError { error -> Error in - switch error { - case HTTPError.parsingFailed: - return SnodeAPIError.snodePoolUpdatingFailed - - default: return error + ), + responseType: SnodePoolResponse.self, + using: dependencies + ) + .send(using: dependencies) + .mapError { error -> Error in + switch error { + case NetworkError.parsingFailed: + return SnodeAPIError.snodePoolUpdatingFailed + + default: return error + } } - } - .map { _, snodePool -> Set in - snodePool.result - .serviceNodeStates - .compactMap { $0.value } - .asSet() - } - .retry(4) - .eraseToAnyPublisher() + .map { _, snodePool -> Set in + snodePool.result + .serviceNodeStates + .compactMap { $0.value } + .asSet() + } + .retry(4) + .eraseToAnyPublisher() + } + catch { return Fail(error: error).eraseToAnyPublisher() } } ) .collect() @@ -1232,107 +1216,89 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - private static func send( - request: SnodeRequest, - to snode: Snode, - associatedWith publicKey: String?, + // MARK: - Direct Requests + + private static func getSnodePoolFromSeedNode( using dependencies: Dependencies - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - guard let payload: Data = try? JSONEncoder().encode(request) else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - guard Features.useOnionRequests else { - return LibSession - .sendRequest( - ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, - snode: snode, - endpoint: request.endpoint.rawValue, - payload: request.body - ) - .mapError { error in - switch error { - case HTTPError.httpRequestFailed(let statusCode, let data): - return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey, using: dependencies) ?? error) - - default: return error - } - } + ) -> AnyPublisher, Error> { + guard let targetSeedNode: Snode = seedNodePool.randomElement() else { + return Fail(error: SnodeAPIError.snodePoolUpdatingFailed) .eraseToAnyPublisher() } - return dependencies.network - .send(.onionRequest(payload, to: snode)) + SNLog("Populating snode pool using seed node: \(targetSeedNode).") + + return LibSession + .sendDirectRequest( + endpoint: SnodeAPI.Endpoint.oxenDaemonRPCCall, + body: OxenDaemonRPCRequest( + endpoint: .daemonGetServiceNodes, + body: GetServiceNodesRequest( + activeOnly: true, + limit: 256, + fields: GetServiceNodesRequest.Fields( + publicIp: true, + pubkeyEd25519: true, + pubkeyX25519: true, + storageLmqPort: true + ) + ) + ), + snode: targetSeedNode, + swarmPublicKey: nil, + ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, + using: dependencies + ) + .decoded(as: SnodePoolResponse.self, using: dependencies) .mapError { error in switch error { - case HTTPError.httpRequestFailed(let statusCode, let data): - return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey, using: dependencies) ?? error) - + case NetworkError.parsingFailed: return SnodeAPIError.snodePoolUpdatingFailed default: return error } } + .map { _, snodePool -> Set in + snodePool.result + .serviceNodeStates + .compactMap { $0.value } + .asSet() + } + .retry(2) .handleEvents( - receiveOutput: { _, maybeData in - // Extract and store hard fork information if returned - guard - let data: Data = maybeData, - let snodeResponse: SnodeResponse = try? JSONDecoder() - .decode(SnodeResponse.self, from: data) - else { return } - - if snodeResponse.hardFork[1] > softfork { - softfork = snodeResponse.hardFork[1] - UserDefaults.standard[.softfork] = softfork - } - - if snodeResponse.hardFork[0] > hardfork { - hardfork = snodeResponse.hardFork[0] - UserDefaults.standard[.hardfork] = hardfork - softfork = snodeResponse.hardFork[1] - UserDefaults.standard[.softfork] = softfork + receiveCompletion: { result in + switch result { + case .finished: SNLog("Got snode pool from seed node: \(targetSeedNode).") + case .failure: SNLog("Failed to contact seed node at: \(targetSeedNode).") } } ) .eraseToAnyPublisher() } - // MARK: - Parsing - - // The parsing utilities below use a best attempt approach to parsing; they warn for parsing - // failures but don't throw exceptions. - - private static func parseSnodes(from responseData: Data?) -> Set { - guard - let responseData: Data = responseData, - let responseJson: JSON = try? JSONSerialization.jsonObject( - with: responseData, - options: [ .fragmentsAllowed ] - ) as? JSON - else { - SNLog("Failed to parse snodes from response data.") - return [] - } - guard let rawSnodes = responseJson["snodes"] as? [JSON] else { - SNLog("Failed to parse snodes from: \(responseJson).") - return [] - } - - guard let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: []) else { - return [] - } - - // FIXME: Hopefully at some point this different Snode structure will be deprecated and can be removed - if - let swarmSnodes: [SwarmSnode] = try? JSONDecoder().decode([Failable].self, from: snodeData).compactMap({ $0.value }), - !swarmSnodes.isEmpty - { - return swarmSnodes.map { $0.toSnode() }.asSet() - } - - return ((try? JSONDecoder().decode([Failable].self, from: snodeData)) ?? []) - .compactMap { $0.value } - .asSet() + /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. + internal static func testSnode( + snode: Snode, + using dependencies: Dependencies + ) -> AnyPublisher { + return LibSession + .sendDirectRequest( + endpoint: SnodeAPI.Endpoint.getInfo, + body: NoBody.null, + snode: snode, + swarmPublicKey: nil, + ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey, + using: dependencies + ) + .decoded(as: SnodeAPI.GetInfoResponse.self, using: dependencies) + .tryMap { _, response -> Void in + guard let version: SessionUtilitiesKit.Version = response.version else { throw SnodeAPIError.missingSnodeVersion } + guard version >= Version(major: 2, minor: 0, patch: 7) else { + SNLog("Unsupported snode version: \(version.stringValue).") + throw SnodeAPIError.unsupportedSnodeVersion(version.stringValue) + } + + return () + } + .eraseToAnyPublisher() } // MARK: - Error Handling @@ -1342,7 +1308,7 @@ public final class SnodeAPI { withStatusCode statusCode: UInt, data: Data?, forSnode snode: Snode, - associatedWith publicKey: String? = nil, + swarmPublicKey publicKey: String? = nil, using dependencies: Dependencies ) -> Error? { func handleBadSnode() { @@ -1413,13 +1379,103 @@ public final class SnodeAPI { return nil } + + // MARK: - Convenience + + private static func prepareRequest( + request: Request, + responseType: R.Type, + requireAllBatchResponses: Bool = true, + retryCount: Int = 0, + timeout: TimeInterval = Network.defaultTimeout, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return Network.PreparedRequest( + request: request, + urlRequest: try request.generateUrlRequest(using: dependencies), + responseType: responseType, + requireAllBatchResponses: requireAllBatchResponses, + retryCount: retryCount, + timeout: timeout + ) + .handleEvents( + receiveOutput: { _, response in + switch response { + // Extract and store hard fork information if returned + case let snodeResponse as SnodeResponse: + guard snodeResponse.hardFork.count > 1 else { break } + + if snodeResponse.hardFork[1] > softfork { + softfork = snodeResponse.hardFork[1] + UserDefaults.standard[.softfork] = softfork + } + + if snodeResponse.hardFork[0] > hardfork { + hardfork = snodeResponse.hardFork[0] + UserDefaults.standard[.hardfork] = hardfork + softfork = snodeResponse.hardFork[1] + UserDefaults.standard[.softfork] = softfork + } + + default: break + } + } + ) + } } -@objc(SNSnodeAPI) -public final class SNSnodeAPI: NSObject { - @objc(currentOffsetTimestampMs) - public static func currentOffsetTimestampMs() -> UInt64 { - return UInt64(SnodeAPI.currentOffsetTimestampMs()) +// MARK: - Publisher Convenience + +public extension Publisher where Output == Set { + func tryFlatMapWithRandomSnode( + maxPublishers: Subscribers.Demand = .unlimited, + retry retries: Int = 0, + drainBehaviour: Atomic = .alwaysRandom, + using dependencies: Dependencies, + _ transform: @escaping (Snode) throws -> P + ) -> AnyPublisher where T == P.Output, P: Publisher, P.Failure == Error { + return self + .mapError { $0 } + .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in + // If we don't want to reuse a specific snode multiple times then just grab a + // random one from the swarm every time + var remainingSnodes: Set = { + switch drainBehaviour.wrappedValue { + case .alwaysRandom: return swarm + case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash): + // If we've used all of the snodes or the swarm has changed then reset the used list + guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else { + drainBehaviour.mutate { $0 = $0.reset() } + return swarm + } + + return swarm.subtracting(usedSnodes) + } + }() + + return Just(()) + .setFailureType(to: Error.self) + .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in + let snode: Snode = try { + switch drainBehaviour.wrappedValue { + case .limitedReuse(_, .some(let targetSnode), _, _, _): return targetSnode + default: break + } + + // Select the next snode + return try dependencies.popRandomElement(&remainingSnodes) ?? { + throw SnodeAPIError.ranOutOfRandomSnodes + }() + }() + drainBehaviour.mutate { $0 = $0.use(snode: snode, from: swarm) } + + return try transform(snode) + .eraseToAnyPublisher() + } + .retry(retries) + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() } } @@ -1439,7 +1495,7 @@ public extension Publisher where Output == Set { return Just(()) .setFailureType(to: Error.self) .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in - let snode: Snode = try remainingSnodes.popRandomElement() ?? { throw SnodeAPIError.generic }() + let snode: Snode = try remainingSnodes.popRandomElement() ?? { throw SnodeAPIError.ranOutOfRandomSnodes }() return try transform(snode) .eraseToAnyPublisher() @@ -1450,3 +1506,59 @@ public extension Publisher where Output == Set { .eraseToAnyPublisher() } } + +// MARK: - Request Convenience + +private extension Request { + init( + endpoint: SnodeAPI.Endpoint, + swarmPublicKey: String, + body: B + ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint { + self = Request( + method: .post, + endpoint: endpoint, + swarmPublicKey: swarmPublicKey, + body: SnodeRequest( + endpoint: endpoint, + body: body + ) + ) + } + + init( + endpoint: SnodeAPI.Endpoint, + snode: Snode, + swarmPublicKey: String? = nil, + body: B + ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint { + self = Request( + method: .post, + endpoint: endpoint, + snode: snode, + body: SnodeRequest( + endpoint: endpoint, + body: body + ), + swarmPublicKey: swarmPublicKey + ) + } + + init( + endpoint: SnodeAPI.Endpoint, + swarmPublicKey: String, + requiresLatestNetworkTime: Bool, + body: B + ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint, B: Encodable & UpdatableTimestamp { + self = Request( + method: .post, + endpoint: endpoint, + swarmPublicKey: swarmPublicKey, + requiresLatestNetworkTime: requiresLatestNetworkTime, + body: SnodeRequest( + endpoint: endpoint, + body: body + ) + ) + } +} diff --git a/SessionSnodeKit/SessionUtil/LibSession+Networking.swift b/SessionSnodeKit/SessionUtil/LibSession+Networking.swift index 44b7c43e9..c6090d9f1 100644 --- a/SessionSnodeKit/SessionUtil/LibSession+Networking.swift +++ b/SessionSnodeKit/SessionUtil/LibSession+Networking.swift @@ -1,4 +1,6 @@ // Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import Combine @@ -8,233 +10,344 @@ import SessionUtilitiesKit // MARK: - LibSession public extension LibSession { - private static func sendRequest( - ed25519SecretKey: [UInt8], - targetPubkey: String, - targetIp: String, - targetPort: UInt16, - endpoint: String, - payload: [UInt8]?, - callback: @escaping (Bool, Bool, Int16, Data?) -> Void - ) { - class CWrapper { - let callback: (Bool, Bool, Int16, Data?) -> Void + struct ServiceNodeChanges { + enum Change: UInt32 { + case none = 0 + case invalidPath = 1 + case replaceSwarm = 2 + case updatePath = 3 + } + + let change: Change + let nodes: [Snode] + let nodeFailureCount: [UInt] + let nodeInvalid: [Bool] + let pathFailureCount: UInt + + init(cChanges: network_service_node_changes) { + self.change = (Change(rawValue: cChanges.type.rawValue) ?? .none) + self.pathFailureCount = UInt(cChanges.failure_count) + + guard cChanges.nodes_count > 0 else { + self.nodes = [] + self.nodeInvalid = [] + self.nodeFailureCount = [] + return + } - public init(_ callback: @escaping (Bool, Bool, Int16, Data?) -> Void) { - self.callback = callback + var pendingNodes: [Snode] = [] + var pendingNodeFailureCount: [UInt] = [] + var pendingNodeInvalid: [Bool] = [] + let cNodes: UnsafePointer = UnsafePointer(cChanges.nodes) + (0.. Void + private var pointersToDeallocate: [UnsafeRawPointer?] = [] + + public init(_ callback: @escaping (Bool, Bool, Int16, Data?, ServiceNodeChanges) -> Void) { + self.callback = callback } - let callbackWrapper: CWrapper = CWrapper(callback) - let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque() - let cRemoteAddress: remote_address = remote_address( - pubkey: targetPubkey.toLibSession(), - ip: targetIp.toLibSession(), - port: targetPort - ) - let cEndpoint: [CChar] = endpoint.cArray - let cPayload: [UInt8] = (payload ?? []) + public func addUnsafePointerToCleanup(_ pointer: UnsafePointer?) { + pointersToDeallocate.append(UnsafeRawPointer(pointer)) + } - network_send_request( - ed25519SecretKey, - cRemoteAddress, - cEndpoint, - cEndpoint.count, - cPayload, - cPayload.count, - { success, timeout, statusCode, dataPtr, dataLen, ctx in - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - Unmanaged.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data) - }, - cWrapperPtr - ) + deinit { + pointersToDeallocate.forEach { $0?.deallocate() } + } } - private static func sendOnionRequest( - path: [Snode], - ed25519SecretKey: [UInt8], - to destination: OnionRequestAPIDestination, - payload: [UInt8]?, - callback: @escaping (Bool, Bool, Int16, Data?) -> Void - ) { - class CWrapper { - let callback: (Bool, Bool, Int16, Data?) -> Void - - public init(_ callback: @escaping (Bool, Bool, Int16, Data?) -> Void) { - self.callback = callback - } - } + // MARK: - Internal Functions + + private static func cSwarm(for swarmPublicKey: String?) -> (ptr: UnsafePointer?, count: Int) { + guard let swarm: Set = swarmPublicKey.map({ SnodeAPI.swarmCache.wrappedValue[$0] }) else { return (nil, 0) } - let callbackWrapper: CWrapper = CWrapper(callback) - let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque() - let cPayload: [UInt8] = (payload ?? []) - var x25519Pubkeys: [UnsafePointer?] = path.map { $0.x25519PublicKey.cArray }.unsafeCopy() - var ed25519Pubkeys: [UnsafePointer?] = path.map { $0.ed25519PublicKey.cArray }.unsafeCopy() - let cNodes: UnsafePointer? = path + let cSwarm: UnsafePointer? = swarm .enumerated() .map { index, snode in - onion_request_service_node( + network_service_node( ip: snode.ip.toLibSession(), lmq_port: snode.lmqPort, - x25519_pubkey_hex: x25519Pubkeys[index], - ed25519_pubkey_hex: ed25519Pubkeys[index], - failure_count: 0 + x25519_pubkey_hex: snode.x25519PublicKey.toLibSession(), + ed25519_pubkey_hex: snode.ed25519PublicKey.toLibSession(), + failure_count: UInt8(SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0), + invalid: false ) } .unsafeCopy() - let cOnionPath: onion_request_path = onion_request_path( - nodes: cNodes, - nodes_count: path.count, - failure_count: 0 - ) - switch destination { - case .snode(let snode): - let cX25519Pubkey: UnsafePointer? = snode.x25519PublicKey.cArray.unsafeCopy() - let cEd25519Pubkey: UnsafePointer? = snode.ed25519PublicKey.cArray.unsafeCopy() + return (cSwarm, swarm.count) + } + + private static func processError( + _ success: Bool, + _ timeout: Bool, + _ statusCode: Int16, + _ data: Data?, + _ changes: ServiceNodeChanges, + _ swarmPublicKey: String?, + using dependencies: Dependencies + ) -> Error? { + guard !success || statusCode < 200 || statusCode > 299 else { return nil } + guard !timeout else { return NetworkError.timeout } + + /// Handle status codes with specific meanings + switch (statusCode, data.map { String(data: $0, encoding: .ascii) }) { + case (400, .none): + return NetworkError.badRequest(error: NetworkError.unknown.errorDescription ?? "Bad Request", rawData: data) - network_send_onion_request_to_snode_destination( - cOnionPath, - ed25519SecretKey, - onion_request_service_node( - ip: snode.ip.toLibSession(), - lmq_port: snode.lmqPort, - x25519_pubkey_hex: cX25519Pubkey, - ed25519_pubkey_hex: cEd25519Pubkey, - failure_count: 0 - ), - cPayload, - cPayload.count, - { success, timeout, statusCode, dataPtr, dataLen, ctx in - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - Unmanaged.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data) - }, - cWrapperPtr - ) + case (400, .some(let responseString)): return NetworkError.badRequest(error: responseString, rawData: data) - case .server(let host, let target, let x25519PublicKey, let scheme, let port): - let cMethod: [CChar] = "GET".cArray - let targetScheme: String = (scheme ?? "https") + case (401, _): + SNLog("Unauthorised (Failed to verify the signature).") + return NetworkError.unauthorised - network_send_onion_request_to_server_destination( - cOnionPath, - ed25519SecretKey, - cMethod, - host.cArray, - target.cArray, - targetScheme.cArray, - x25519PublicKey.cArray, - (port ?? (targetScheme == "https" ? 443 : 80)), - nil, - nil, - 0, - cPayload, - cPayload.count, - { success, timeout, statusCode, dataPtr, dataLen, ctx in - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - Unmanaged.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data) - }, - cWrapperPtr - ) + case (404, _): return NetworkError.notFound + + /// A snode will return a `406` but onion requests v4 seems to return `425` so handle both + case (406, _), (425, _): + SNLog("The user's clock is out of sync with the service node network.") + return SnodeAPIError.clockOutOfSync + + case (421, _): + switch swarmPublicKey { + case .none: SNLog("Got a 421 without an associated public key.") + case .some(let publicKey): + if + let data: Data = data, + let swarmResponse: GetSwarmResponse = try? data.decoded(as: GetSwarmResponse.self, using: dependencies), + !swarmResponse.snodes.isEmpty + { + SnodeAPI.setSwarm(to: swarmResponse.snodes, for: publicKey) + } + } + return SnodeAPIError.unassociatedPubkey + + case (429, _): return SnodeAPIError.rateLimited + case (500, _), (502, _), (503, _): return SnodeAPIError.unreachable + case (_, .none): return NetworkError.unknown + case (_, .some(let responseString)): return NetworkError.requestFailed(error: responseString, rawData: data) } } - private static func sendRequest( - ed25519SecretKey: [UInt8]?, + // MARK: - Public Interface + + static func addNetworkLogger() { + network_add_logger({ logPtr, msgLen in + guard let log: String = String(pointer: logPtr, length: msgLen, encoding: .utf8) else { + print("[quic:info] Null log") + return + } + + print(log.trimmingCharacters(in: .whitespacesAndNewlines)) + }) + } + + static func sendDirectRequest( + endpoint: any EndpointType, + body: T?, snode: Snode, - endpoint: String, - payloadBytes: [UInt8]? + swarmPublicKey: String?, + ed25519SecretKey: [UInt8]?, + using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { return Deferred { - Future { resolver in - guard let ed25519SecretKey: [UInt8] = ed25519SecretKey else { + Future<(ResponseInfoType, Data?), Error> { resolver in + guard let cEd25519SecretKey: [UInt8] = ed25519SecretKey else { return resolver(Result.failure(SnodeAPIError.missingSecretKey)) } - LibSession.sendRequest( - ed25519SecretKey: ed25519SecretKey, - targetPubkey: snode.ed25519PublicKey, - targetIp: snode.ip, - targetPort: snode.lmqPort, - endpoint: endpoint,//.rawValue, - payload: payloadBytes, - callback: { success, timeout, statusCode, data in - switch SnodeAPIError(success: success, timeout: timeout, statusCode: statusCode, data: data) { - case .some(let error): resolver(Result.failure(error)) - case .none: resolver(Result.success((HTTP.ResponseInfo(code: Int(statusCode), headers: [:]), data))) + // Prepare the parameters + let cPayloadBytes: [UInt8] + + switch body { + case .none: cPayloadBytes = [] + case let data as Data: cPayloadBytes = Array(data) + case let bytes as [UInt8]: cPayloadBytes = bytes + default: + guard let encodedBody: Data = try? JSONEncoder().encode(body) else { + return resolver(Result.failure(SnodeAPIError.invalidPayload)) } + + cPayloadBytes = Array(encodedBody) + } + let cTarget: network_service_node = network_service_node( + ip: snode.ip.toLibSession(), + lmq_port: snode.lmqPort, + x25519_pubkey_hex: snode.x25519PublicKey.toLibSession(), + ed25519_pubkey_hex: snode.ed25519PublicKey.toLibSession(), + failure_count: UInt8(SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0), + invalid: false + ) + let cSwarmInfo: (ptr: UnsafePointer?, count: Int) = cSwarm(for: swarmPublicKey) + let callbackWrapper: CWrapper = CWrapper { success, timeout, statusCode, data, changes in + switch processError(success, timeout, statusCode, data, changes, swarmPublicKey, using: dependencies) { + case .some(let error): resolver(Result.failure(error)) + case .none: resolver(Result.success((Network.ResponseInfo(code: Int(statusCode), headers: [:]), data))) } + } + callbackWrapper.addUnsafePointerToCleanup(cSwarmInfo.ptr) + let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque() + + // Trigger the request + network_send_request( + cEd25519SecretKey, + cTarget, + endpoint.path.cArray.nullTerminated(), + cPayloadBytes, + cPayloadBytes.count, + cSwarmInfo.ptr, + cSwarmInfo.count, + { success, timeout, statusCode, dataPtr, dataLen, cChanges, ctx in + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + let changes: ServiceNodeChanges = ServiceNodeChanges(cChanges: cChanges) + Unmanaged.fromOpaque(ctx!).takeRetainedValue() + .callback(success, timeout, statusCode, data, changes) + }, + cWrapperPtr ) } }.eraseToAnyPublisher() } - static func sendRequest( - ed25519SecretKey: [UInt8]?, - snode: Snode, - endpoint: String - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - return sendRequest(ed25519SecretKey: ed25519SecretKey, snode: snode, endpoint: endpoint, payloadBytes: nil) - } - - static func sendRequest( - ed25519SecretKey: [UInt8]?, - snode: Snode, - endpoint: String, - payload: T - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - let payloadBytes: [UInt8] - - switch payload { - case let data as Data: payloadBytes = Array(data) - case let bytes as [UInt8]: payloadBytes = bytes - default: - guard let encodedPayload: Data = try? JSONEncoder().encode(payload) else { - return Fail(error: SnodeAPIError.invalidPayload).eraseToAnyPublisher() - } - - payloadBytes = Array(encodedPayload) - } - - return sendRequest(ed25519SecretKey: ed25519SecretKey, snode: snode, endpoint: endpoint, payloadBytes: payloadBytes) - } - static func sendOnionRequest( + to destination: OnionRequestAPIDestination, + body: T?, path: [Snode], + swarmPublicKey: String?, ed25519SecretKey: [UInt8]?, - to destination: OnionRequestAPIDestination, - payload: T + using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - let payloadBytes: [UInt8] - switch payload { - case let data as Data: payloadBytes = Array(data) - case let bytes as [UInt8]: payloadBytes = bytes - default: - guard let encodedPayload: Data = try? JSONEncoder().encode(payload) else { - return Fail(error: SnodeAPIError.invalidPayload).eraseToAnyPublisher() - } - - payloadBytes = Array(encodedPayload) - } - return Deferred { - Future { resolver in - guard let ed25519SecretKey: [UInt8] = ed25519SecretKey else { + Future<(ResponseInfoType, Data?), Error> { resolver in + guard let cEd25519SecretKey: [UInt8] = ed25519SecretKey else { return resolver(Result.failure(SnodeAPIError.missingSecretKey)) } - LibSession.sendOnionRequest( - path: path, - ed25519SecretKey: ed25519SecretKey, - to: destination, - payload: payloadBytes, - callback: { success, timeout, statusCode, data in - switch SnodeAPIError(success: success, timeout: timeout, statusCode: statusCode, data: data) { - case .some(let error): resolver(Result.failure(error)) - case .none: resolver(Result.success((HTTP.ResponseInfo(code: Int(statusCode), headers: [:]), data))) + // Prepare the parameters + let cPayloadBytes: [UInt8] + + switch body { + case .none: cPayloadBytes = [] + case let data as Data: cPayloadBytes = Array(data) + case let bytes as [UInt8]: cPayloadBytes = bytes + default: + guard let encodedBody: Data = try? JSONEncoder().encode(body) else { + return resolver(Result.failure(SnodeAPIError.invalidPayload)) } + + cPayloadBytes = Array(encodedBody) + } + let cNodes: UnsafePointer? = path + .enumerated() + .map { index, snode in + network_service_node( + ip: snode.ip.toLibSession(), + lmq_port: snode.lmqPort, + x25519_pubkey_hex: snode.x25519PublicKey.toLibSession(), + ed25519_pubkey_hex: snode.ed25519PublicKey.toLibSession(), + failure_count: UInt8(SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0), + invalid: false + ) } + .unsafeCopy() + let cOnionPath: onion_request_path = onion_request_path( + nodes: cNodes, + nodes_count: path.count, + failure_count: UInt8(OnionRequestAPI.pathFailureCount.wrappedValue[path] ?? 0) ) + let cSwarmInfo: (ptr: UnsafePointer?, count: Int) = cSwarm(for: swarmPublicKey) + let callbackWrapper: CWrapper = CWrapper { success, timeout, statusCode, data, changes in + switch processError(success, timeout, statusCode, data, changes, swarmPublicKey, using: dependencies) { + case .some(let error): resolver(Result.failure(error)) + case .none: resolver(Result.success((Network.ResponseInfo(code: Int(statusCode), headers: [:]), data))) + } + } + callbackWrapper.addUnsafePointerToCleanup(cNodes) + callbackWrapper.addUnsafePointerToCleanup(cSwarmInfo.ptr) + let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque() + + // Trigger the request + switch destination { + case .snode(let snode): + network_send_onion_request_to_snode_destination( + cOnionPath, + cEd25519SecretKey, + onion_request_service_node_destination( + ip: snode.ip.toLibSession(), + lmq_port: snode.lmqPort, + x25519_pubkey_hex: snode.x25519PublicKey.toLibSession(), + ed25519_pubkey_hex: snode.ed25519PublicKey.toLibSession(), + failure_count: UInt8(SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0), + invalid: false, + swarm: cSwarmInfo.ptr, + swarm_count: cSwarmInfo.count + ), + cPayloadBytes, + cPayloadBytes.count, + { success, timeout, statusCode, dataPtr, dataLen, cChanges, ctx in + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + let changes: ServiceNodeChanges = ServiceNodeChanges(cChanges: cChanges) + Unmanaged.fromOpaque(ctx!).takeRetainedValue() + .callback(success, timeout, statusCode, data, changes) + }, + cWrapperPtr + ) + + case .server(let method, let scheme, let host, let endpoint, let port, let headers, let x25519PublicKey): + let targetScheme: String = (scheme ?? "https") + let headerInfo: [(key: String, value: String)]? = headers?.map { ($0.key, $0.value) } + var cHeaderKeys: [UnsafePointer?] = (headerInfo ?? []) + .map { $0.key.cArray.nullTerminated() } + .unsafeCopy() + var cHeaderValues: [UnsafePointer?] = (headerInfo ?? []) + .map { $0.value.cArray.nullTerminated() } + .unsafeCopy() + + // Add a cleanup callback to deallocate the header arrays + cHeaderKeys.forEach { callbackWrapper.addUnsafePointerToCleanup($0) } + cHeaderValues.forEach { callbackWrapper.addUnsafePointerToCleanup($0) } + + network_send_onion_request_to_server_destination( + cOnionPath, + cEd25519SecretKey, + (method ?? "GET").cArray.nullTerminated(), + targetScheme.cArray.nullTerminated(), + host.cArray.nullTerminated(), + endpoint.path.cArray.nullTerminated(), + (port ?? (targetScheme == "https" ? 443 : 80)), + x25519PublicKey.cArray, + &cHeaderKeys, + &cHeaderValues, + cHeaderKeys.count, + cPayloadBytes, + cPayloadBytes.count, + { success, timeout, statusCode, dataPtr, dataLen, cChanges, ctx in + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + let changes: ServiceNodeChanges = ServiceNodeChanges(cChanges: cChanges) + Unmanaged.fromOpaque(ctx!).takeRetainedValue() + .callback(success, timeout, statusCode, data, changes) + }, + cWrapperPtr + ) + } } }.eraseToAnyPublisher() } diff --git a/SessionSnodeKit/Types/OnionRequestAPIDestination.swift b/SessionSnodeKit/Types/OnionRequestAPIDestination.swift index 4ac56481c..556b3cd40 100644 --- a/SessionSnodeKit/Types/OnionRequestAPIDestination.swift +++ b/SessionSnodeKit/Types/OnionRequestAPIDestination.swift @@ -1,15 +1,26 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation +import SessionUtilitiesKit -public enum OnionRequestAPIDestination: CustomStringConvertible, Codable { +public enum OnionRequestAPIDestination: CustomStringConvertible { case snode(Snode) - case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?) + case server( + method: String?, + scheme: String?, + host: String, + endpoint: any EndpointType, + port: UInt16?, + headers: [HTTPHeader: String]?, + x25519PublicKey: String + ) public var description: String { switch self { case .snode(let snode): return "Service node \(snode.ip):\(snode.lmqPort)" - case .server(let host, _, _, _, _): return host + case .server(_, _, let host, _, _, _, _): return host } } } diff --git a/SessionSnodeKit/Types/OnionRequestAPIError.swift b/SessionSnodeKit/Types/OnionRequestAPIError.swift deleted file mode 100644 index 2d18bc7d2..000000000 --- a/SessionSnodeKit/Types/OnionRequestAPIError.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SessionUtilitiesKit - -public enum OnionRequestAPIError: LocalizedError { - case httpRequestFailedAtDestination(statusCode: UInt, data: Data, destination: OnionRequestAPIDestination) - case insufficientSnodes - case invalidURL - case missingSnodeVersion - case snodePublicKeySetMissing - case unsupportedSnodeVersion(String) - case invalidRequestInfo - - public var errorDescription: String? { - switch self { - case .httpRequestFailedAtDestination(let statusCode, let data, let destination): - if statusCode == 429 { return "Rate limited." } - if let processedResponseBodyData: Data = OnionRequestAPI.process(bencodedData: data)?.body, let errorResponse: String = String(data: processedResponseBodyData, encoding: .utf8) { - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." - } - if let errorResponse: String = String(data: data, encoding: .utf8) { - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." - } - - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)." - - case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path." - case .invalidURL: return "Invalid URL" - case .missingSnodeVersion: return "Missing Service Node version." - case .snodePublicKeySetMissing: return "Missing Service Node public key set." - case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)." - case .invalidRequestInfo: return "Invalid Request Info" - } - } -} diff --git a/SessionSnodeKit/Types/OnionRequestAPIVersion.swift b/SessionSnodeKit/Types/OnionRequestAPIVersion.swift deleted file mode 100644 index f4bc31a8f..000000000 --- a/SessionSnodeKit/Types/OnionRequestAPIVersion.swift +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public enum OnionRequestAPIVersion: String, Codable { - case v2 = "/loki/v2/lsrpc" - case v3 = "/loki/v3/lsrpc" - case v4 = "/oxen/v4/lsrpc" -} diff --git a/SessionSnodeKit/Types/SnodeAPIEndpoint.swift b/SessionSnodeKit/Types/SnodeAPIEndpoint.swift index c55fba8b2..45cec1c60 100644 --- a/SessionSnodeKit/Types/SnodeAPIEndpoint.swift +++ b/SessionSnodeKit/Types/SnodeAPIEndpoint.swift @@ -28,7 +28,7 @@ public extension SnodeAPI { // jsonRPCCall proxied calls - case jsonGetNServiceNodes + case jsonGetServiceNodes // oxenDaemonRPCCall proxied calls @@ -36,7 +36,7 @@ public extension SnodeAPI { case daemonGetServiceNodes public static var name: String { "SnodeAPI.Endpoint" } - public static var batchRequestVariant: HTTP.BatchRequest.Child.Variant = .storageServer + public static var batchRequestVariant: Network.BatchRequest.Child.Variant = .storageServer public var path: String { switch self { @@ -61,7 +61,7 @@ public extension SnodeAPI { // jsonRPCCall proxied calls - case .jsonGetNServiceNodes: return "get_n_service_nodes" + case .jsonGetServiceNodes: return "get_service_nodes" // oxenDaemonRPCCall proxied calls diff --git a/SessionSnodeKit/Types/SnodeAPIError.swift b/SessionSnodeKit/Types/SnodeAPIError.swift index 551228343..5727c8ca0 100644 --- a/SessionSnodeKit/Types/SnodeAPIError.swift +++ b/SessionSnodeKit/Types/SnodeAPIError.swift @@ -6,7 +6,6 @@ import Foundation import SessionUtilitiesKit public enum SnodeAPIError: LocalizedError { - case generic case clockOutOfSync case snodePoolUpdatingFailed case inconsistentSnodePools @@ -14,10 +13,14 @@ public enum SnodeAPIError: LocalizedError { case signingFailed case signatureVerificationFailed case invalidIP - case emptySnodePool case responseFailedValidation case rateLimited - case invalidPreparedRequest + case missingSnodeVersion + case unsupportedSnodeVersion(String) + + // Onion Request Errors + case emptySnodePool + case insufficientSnodes case ranOutOfRandomSnodes // ONS @@ -28,15 +31,11 @@ public enum SnodeAPIError: LocalizedError { // Quic case invalidPayload case missingSecretKey - case requestFailed(error: String, rawData: Data?) - case timeout case unreachable case unassociatedPubkey - case unknown public var errorDescription: String? { switch self { - case .generic: return "An error occurred." case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time." case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool." case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network." @@ -44,10 +43,14 @@ public enum SnodeAPIError: LocalizedError { case .signingFailed: return "Couldn't sign message." case .signatureVerificationFailed: return "Failed to verify the signature." case .invalidIP: return "Invalid IP." - case .emptySnodePool: return "Service Node pool is empty." case .responseFailedValidation: return "Response failed validation." case .rateLimited: return "Rate limited." - case .invalidPreparedRequest: return "Invalid PreparedRequest provided." + case .missingSnodeVersion: return "Missing Service Node version." + case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)." + + // Onion Request Errors + case .emptySnodePool: return "Service Node pool is empty." + case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path." case .ranOutOfRandomSnodes: return "Ran out of random snodes to send the request through." // ONS @@ -58,90 +61,8 @@ public enum SnodeAPIError: LocalizedError { // Quic case .invalidPayload: return "Invalid payload." case .missingSecretKey: return "Missing secret key." - case .requestFailed(let error, _): return error - case .timeout: return "The request timed out." case .unreachable: return "The service node is unreachable." case .unassociatedPubkey: return "The service node is no longer associated with the public key." - case .unknown: return "An unknown error occurred." - } - } -} - -public extension SnodeAPIError { - init?( - _ success: Bool, - _ timeout: Bool, - _ statusCode: Int16, - _ data: Data?, - _ updatedPath: LibSession.OnionPath?, - _ publicKey: String?, - using dependencies: Dependencies - ) { - guard !success || statusCode < 200 || statusCode > 299 else { return nil } - guard !timeout else { - self = .timeout - return - } - - // Handle status codes with specific meanings - switch (statusCode, data.map { String(data: $0, encoding: .utf8) }) { - /// A snode will return a `406` but onion requests v4 seems to return `425` so handle both - case (406, _), (425, _): - SNLog("The user's clock is out of sync with the service node network.") - self = .clockOutOfSync - - case (401, _): - SNLog("Failed to verify the signature.") - self = .signatureVerificationFailed - - case (421, _): - // TODO: Need to handle the snode response, otherwise drop from the snode - switch publicKey { - case .none: SNLog("Got a 421 without an associated public key.") - case .some(let publicKey): - if - let data: Data = data, - let swarmResponse: GetSwarmResponse = try? data.decoded(as: GetSwarmResponse.self, using: dependencies), - !swarmResponse.snodes.isEmpty - { - SnodeAPI.setSwarm(to: swarmResponse.snodes, for: publicKey) - } - } - self = .unassociatedPubkey - - case (429, _): self = .rateLimited - case (500, _), (502, _), (503, _): self = .unreachable - case (_, .none): self = .unknown - case (_, .some(let responseString)): self = .requestFailed(error: responseString, rawData: data) - } - - // Process the updatedPath - guard let updatedPath: LibSession.OnionPath = updatedPath else { return } - - zip(updatedPath.path, updatedPath.nodeFailureCount, updatedPath.nodeInvalid).forEach { snode, failureCount, invalid in - guard invalid else { - SNLog("Couldn't reach snode at: \(snode); setting failure count to \(failureCount).") - SnodeAPI.snodeFailureCount.mutate { $0[snode] = failureCount } - return - } - - SNLog("Failure threshold reached for: \(snode); dropping it.") - if let publicKey = publicKey { - SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) - } - SnodeAPI.dropSnodeFromSnodePool(snode) - SnodeAPI.snodeFailureCount.mutate { $0[snode] = 0 } - SNLog("Snode pool count: \(SnodeAPI.snodePool.wrappedValue.count).") - } - - // The guardSnode (first in the path) will be marked as invalid if the path has failed too many times at which - // point we drop it and the path - switch Array(zip(updatedPath.path, updatedPath.nodeInvalid)).first { - case .some((let snode, true)): - OnionRequestAPI.dropGuardSnode(snode) - OnionRequestAPI.drop(updatedPath.path) - - default: OnionRequestAPI.pathFailureCount.mutate { $0[updatedPath.path] = updatedPath.pathFailureCount } } } } diff --git a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift index 091b24af9..4b2923c74 100644 --- a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift +++ b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift @@ -158,7 +158,7 @@ public extension Publisher where Output == (ResponseInfoType, Data?), Failure == ) -> AnyPublisher<(ResponseInfoType, R), Error> { self .tryMap { responseInfo, maybeData -> (ResponseInfoType, R) in - guard let data: Data = maybeData else { throw HTTPError.parsingFailed } + guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return (responseInfo, try data.decoded(as: type, using: dependencies)) } diff --git a/SessionUtilitiesKit/General/Data+Utilities.swift b/SessionUtilitiesKit/General/Data+Utilities.swift index 1114ad973..cc4d17aeb 100644 --- a/SessionUtilitiesKit/General/Data+Utilities.swift +++ b/SessionUtilitiesKit/General/Data+Utilities.swift @@ -14,7 +14,7 @@ public extension Data { return try decoder.decode(type, from: self) } - catch { throw HTTPError.parsingFailed } + catch { throw NetworkError.parsingFailed } } func removingIdPrefixIfNeeded() -> Data { diff --git a/SessionUtilitiesKit/General/Features.swift b/SessionUtilitiesKit/General/Features.swift index ad235bf31..611fda892 100644 --- a/SessionUtilitiesKit/General/Features.swift +++ b/SessionUtilitiesKit/General/Features.swift @@ -3,7 +3,6 @@ import Foundation public final class Features { - public static let useOnionRequests: Bool = true public static let useTestnet: Bool = false public static let useNewDisappearingMessagesConfig: Bool = Date().timeIntervalSince1970 > 1710284400 } diff --git a/SessionUtilitiesKit/Networking/BatchRequest.swift b/SessionUtilitiesKit/Networking/BatchRequest.swift index 9eb5c1656..f5b3fbe18 100644 --- a/SessionUtilitiesKit/Networking/BatchRequest.swift +++ b/SessionUtilitiesKit/Networking/BatchRequest.swift @@ -3,10 +3,10 @@ import Foundation public protocol BatchRequestChildRetrievable { - var requests: [HTTP.BatchRequest.Child] { get } + var requests: [Network.BatchRequest.Child] { get } } -public extension HTTP { +public extension Network { struct BatchRequest: Encodable, BatchRequestChildRetrievable { /// The servers currently have a limit for the number of requests a `BatchRequest` can have, when using this we should avoid /// trying to make calls that exceed this limit as they will fail diff --git a/SessionUtilitiesKit/Networking/BatchResponse.swift b/SessionUtilitiesKit/Networking/BatchResponse.swift index 4027f4e7f..eb47e6211 100644 --- a/SessionUtilitiesKit/Networking/BatchResponse.swift +++ b/SessionUtilitiesKit/Networking/BatchResponse.swift @@ -3,8 +3,8 @@ import Foundation import Combine -public extension HTTP { - // MARK: - HTTP.BatchResponse +public extension Network { + // MARK: - Network.BatchResponse struct BatchResponse: Decodable, Collection { public let data: [Any] @@ -64,11 +64,11 @@ public extension HTTP { public static func from( batchEndpoints: [any EndpointType], - response: HTTP.BatchResponse + response: Network.BatchResponse ) throws -> Self { let convertedEndpoints: [E] = batchEndpoints.compactMap { $0 as? E } - guard convertedEndpoints.count == response.data.count else { throw HTTPError.parsingFailed } + guard convertedEndpoints.count == response.data.count else { throw NetworkError.parsingFailed } return BatchResponseMap( data: zip(convertedEndpoints, response.data) @@ -121,19 +121,19 @@ public extension HTTP { public protocol ErasedBatchResponseMap { static func from( batchEndpoints: [any EndpointType], - response: HTTP.BatchResponse + response: Network.BatchResponse ) throws -> Self } // MARK: - BatchSubResponse Coding -extension HTTP.BatchSubResponse: Encodable where T: Encodable {} -extension HTTP.BatchSubResponse: Decodable { +extension Network.BatchSubResponse: Encodable where T: Encodable {} +extension Network.BatchSubResponse: Decodable { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) let body: T? = ((try? (T.self as? Decodable.Type)?.decoded(with: container, forKey: .body)) as? T) - self = HTTP.BatchSubResponse( + self = Network.BatchSubResponse( code: try container.decode(Int.self, forKey: .code), headers: ((try? container.decode([String: String].self, forKey: .headers)) ?? [:]), body: body, @@ -154,17 +154,17 @@ protocol ErasedBatchSubResponse: ResponseInfoType { // MARK: - Convenience -internal extension HTTP.BatchResponse { +internal extension Network.BatchResponse { static func decodingResponses( from data: Data?, as types: [Decodable.Type], requireAllResults: Bool, using dependencies: Dependencies = Dependencies() - ) throws -> HTTP.BatchResponse { + ) throws -> Network.BatchResponse { // Need to split the data into an array of data so each item can be Decoded correctly - guard let data: Data = data else { throw HTTPError.parsingFailed } + guard let data: Data = data else { throw NetworkError.parsingFailed } guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } let dataArray: [Data] @@ -174,7 +174,7 @@ internal extension HTTP.BatchResponse { dataArray = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) } guard !requireAllResults || dataArray.count == types.count else { - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } case let anyDict as [String: Any]: @@ -185,14 +185,14 @@ internal extension HTTP.BatchResponse { !requireAllResults || resultsArray.count == types.count ) - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } dataArray = resultsArray - default: throw HTTPError.parsingFailed + default: throw NetworkError.parsingFailed } - return HTTP.BatchResponse( + return Network.BatchResponse( data: try zip(dataArray, types) .map { data, type in try type.decoded(from: data, using: dependencies) } ) diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift deleted file mode 100644 index a2b6d1bef..000000000 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine - -public enum HTTP { - private struct Certificates { - let isValid: Bool - let certificates: [SecCertificate] - } - - private static let seedNodeURLSession = URLSession(configuration: .ephemeral, delegate: seedNodeURLSessionDelegate, delegateQueue: nil) - private static let seedNodeURLSessionDelegate = SeedNodeURLSessionDelegateImplementation() - private static let snodeURLSession = URLSession(configuration: .ephemeral, delegate: snodeURLSessionDelegate, delegateQueue: nil) - private static let snodeURLSessionDelegate = SnodeURLSessionDelegateImplementation() - - // MARK: - Certificates - - /// **Note:** These certificates will need to be regenerated and replaced at the start of April 2025, iOS has a restriction after iOS 13 - /// where certificates can have a maximum lifetime of 825 days (https://support.apple.com/en-au/HT210176) as a result we - /// can't use the 10 year certificates that the other platforms use - private static let storageSeedCertificates: Atomic = { - let certFileNames: [String] = [ - "seed1-2023-2y", - "seed2-2023-2y", - "seed3-2023-2y" - ] - let paths: [String] = certFileNames.compactMap { Bundle.main.path(forResource: $0, ofType: "der") } - let certData: [Data] = paths.compactMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) } - let certificates: [SecCertificate] = certData.compactMap { SecCertificateCreateWithData(nil, $0 as CFData) } - - guard certificates.count == certFileNames.count else { - return Atomic(Certificates(isValid: false, certificates: [])) - } - - return Atomic(Certificates(isValid: true, certificates: certificates)) - }() - - // MARK: - Settings - - public static let defaultTimeout: TimeInterval = 10 - - // MARK: - Seed Node URL Session Delegate Implementation - - private final class SeedNodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate { - - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - guard HTTP.storageSeedCertificates.wrappedValue.isValid else { - SNLog("Failed to set load seed node certificates.") - return completionHandler(.cancelAuthenticationChallenge, nil) - } - guard let trust = challenge.protectionSpace.serverTrust else { - return completionHandler(.cancelAuthenticationChallenge, nil) - } - // Mark the seed node certificates as trusted - guard SecTrustSetAnchorCertificates(trust, HTTP.storageSeedCertificates.wrappedValue.certificates as CFArray) == errSecSuccess else { - SNLog("Failed to set seed node certificates.") - return completionHandler(.cancelAuthenticationChallenge, nil) - } - - // Check that the presented certificate is one of the seed node certificates - var error: CFError? - guard SecTrustEvaluateWithError(trust, &error) else { - // Extract the result for further processing (since we are defaulting to `invalid` we - // don't care if extracting the result type fails) - var result: SecTrustResultType = .invalid - _ = SecTrustGetTrustResult(trust, &result) - - switch result { - case .proceed, .unspecified: - /// Unspecified indicates that evaluation reached an (implicitly trusted) anchor certificate without any evaluation - /// failures, but never encountered any explicitly stated user-trust preference. This is the most common return - /// value. The Keychain Access utility refers to this value as the "Use System Policy," which is the default user setting. - return completionHandler(.useCredential, URLCredential(trust: trust)) - - case .recoverableTrustFailure: - /// A recoverable failure generally suggests that the certificate was mostly valid but something minor didn't line up, - /// while we don't want to recover in this case it's probably a good idea to include the reason in the logs to simplify - /// debugging if it does end up happening - let reason: String = { - guard - let validationResult: [String: Any] = SecTrustCopyResult(trust) as? [String: Any], - let details: [String: Any] = (validationResult["TrustResultDetails"] as? [[String: Any]])? - .reduce(into: [:], { result, next in next.forEach { result[$0.key] = $0.value } }) - else { return "Unknown" } - - return "\(details)" - }() - - SNLog("Failed to validate a seed certificate with a recoverable error: \(reason)") - return completionHandler(.cancelAuthenticationChallenge, nil) - - default: - SNLog("Failed to validate a seed certificate with an unrecoverable error.") - return completionHandler(.cancelAuthenticationChallenge, nil) - } - } - - return completionHandler(.useCredential, URLCredential(trust: trust)) - } - } - - // MARK: - Snode URL Session Delegate Implementation - - private final class SnodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate { - - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - // Snode to snode communication uses self-signed certificates but clients can safely ignore this - completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) - } - } - - // MARK: - Execution - - public static func execute( - _ method: HTTPMethod, - _ url: String, - timeout: TimeInterval = HTTP.defaultTimeout, - useSeedNodeURLSession: Bool = false - ) -> AnyPublisher { - return execute( - method, - url, - body: nil, - timeout: timeout, - useSeedNodeURLSession: useSeedNodeURLSession - ) - } - - public static func execute( - _ method: HTTPMethod, - _ url: String, - body: Data?, - timeout: TimeInterval = HTTP.defaultTimeout, - useSeedNodeURLSession: Bool = false - ) -> AnyPublisher { - guard let url: URL = URL(string: url) else { - return Fail(error: HTTPError.invalidURL) - .eraseToAnyPublisher() - } - - let urlSession: URLSession = (useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession) - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.httpBody = body - request.timeoutInterval = timeout - request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent") - request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value - request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value - - return urlSession - .dataTaskPublisher(for: request) - .mapError { error in - SNLog("\(method.rawValue) request to \(url) failed due to error: \(error).") - - // Override the actual error so that we can correctly catch failed requests - // in sendOnionRequest(invoking:on:with:) - switch (error as NSError).code { - case NSURLErrorTimedOut: return HTTPError.timeout - default: return HTTPError.httpRequestFailed(statusCode: 0, data: nil) - } - } - .flatMap { data, response in - guard let response = response as? HTTPURLResponse else { - SNLog("\(method.rawValue) request to \(url) failed.") - return Fail(error: HTTPError.httpRequestFailed(statusCode: 0, data: data)) - .eraseToAnyPublisher() - } - let statusCode = UInt(response.statusCode) - // TODO: Remove all the JSON handling? - guard 200...299 ~= statusCode else { - var json: JSON? = nil - if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON { - json = processedJson - } - else if let result: String = String(data: data, encoding: .utf8) { - json = [ "result": result ] - } - - let jsonDescription: String = (json?.prettifiedDescription ?? "no debugging info provided") - SNLog("\(method.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).") - return Fail(error: HTTPError.httpRequestFailed(statusCode: statusCode, data: data)) - .eraseToAnyPublisher() - } - - return Just(data) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } -} diff --git a/SessionUtilitiesKit/Networking/HTTPError.swift b/SessionUtilitiesKit/Networking/HTTPError.swift deleted file mode 100644 index 91ce0d48a..000000000 --- a/SessionUtilitiesKit/Networking/HTTPError.swift +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation - -public enum HTTPError: LocalizedError, Equatable { - case generic - case invalidURL - case invalidJSON - case parsingFailed - case invalidResponse - case maxFileSizeExceeded - case httpRequestFailed(statusCode: UInt, data: Data?) - case timeout - - public var errorDescription: String? { - switch self { - case .generic: return "An error occurred." - case .invalidURL: return "Invalid URL." - case .invalidJSON: return "Invalid JSON." - case .parsingFailed, .invalidResponse: return "Invalid response." - case .maxFileSizeExceeded: return "Maximum file size exceeded." - case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." - case .timeout: return "The request timed out." - } - } -} diff --git a/SessionUtilitiesKit/Networking/NetworkError.swift b/SessionUtilitiesKit/Networking/NetworkError.swift new file mode 100644 index 000000000..948b16d7a --- /dev/null +++ b/SessionUtilitiesKit/Networking/NetworkError.swift @@ -0,0 +1,33 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation + +public enum NetworkError: LocalizedError, Equatable { + case invalidURL + case invalidPreparedRequest + case notFound + case parsingFailed + case invalidResponse + case maxFileSizeExceeded + case unauthorised + case badRequest(error: String, rawData: Data?) + case requestFailed(error: String, rawData: Data?) + case timeout + case unknown + + public var errorDescription: String? { + switch self { + case .invalidURL: return "Invalid URL." + case .invalidPreparedRequest: return "Invalid PreparedRequest provided." + case .notFound: return "Not Found." + case .parsingFailed, .invalidResponse: return "Invalid response." + case .maxFileSizeExceeded: return "Maximum file size exceeded." + case .unauthorised: return "Unauthorised (Failed to verify the signature)." + case .badRequest(let error, _), .requestFailed(let error, _): return error + case .timeout: return "The request timed out." + case .unknown: return "An unknown error occurred." + } + } +} diff --git a/SessionUtilitiesKit/Networking/NetworkType.swift b/SessionUtilitiesKit/Networking/NetworkType.swift index be6e0030c..d6fd4998a 100644 --- a/SessionUtilitiesKit/Networking/NetworkType.swift +++ b/SessionUtilitiesKit/Networking/NetworkType.swift @@ -8,6 +8,8 @@ public protocol NetworkType { } public class Network: NetworkType { + public static let defaultTimeout: TimeInterval = 10 + public struct RequestType { public let id: String public let url: String? diff --git a/SessionUtilitiesKit/Networking/PreparedRequest.swift b/SessionUtilitiesKit/Networking/PreparedRequest.swift index 202a508ad..aac24022a 100644 --- a/SessionUtilitiesKit/Networking/PreparedRequest.swift +++ b/SessionUtilitiesKit/Networking/PreparedRequest.swift @@ -4,9 +4,9 @@ import Foundation import Combine import GRDB -// MARK: - HTTP.PreparedRequest +// MARK: - Network.PreparedRequest -public extension HTTP { +public extension Network { struct PreparedRequest { public struct CachedResponse { fileprivate let info: ResponseInfoType @@ -33,12 +33,12 @@ public extension HTTP { public let endpoint: (any EndpointType) public let endpointName: String public let batchEndpoints: [any EndpointType] - public let batchRequestVariant: HTTP.BatchRequest.Child.Variant + public let batchRequestVariant: Network.BatchRequest.Child.Variant public let batchResponseTypes: [Decodable.Type] public let requireAllBatchResponses: Bool public let excludedSubRequestHeaders: [String] - private let jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer, HTTP.BatchRequest.Child.CodingKeys) throws -> ())? + private let jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer, Network.BatchRequest.Child.CodingKeys) throws -> ())? private let jsonBodyEncoder: ((inout SingleValueEncodingContainer) throws -> ())? private let b64: String? private let bytes: [UInt8]? @@ -51,7 +51,7 @@ public extension HTTP { retryCount: Int = 0, timeout: TimeInterval ) where R: Decodable { - let batchRequests: [HTTP.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests + let batchRequests: [Network.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests let batchEndpoints: [E] = (batchRequests? .compactMap { $0.request.batchRequestEndpoint(of: E.self) }) .defaulting(to: []) @@ -82,7 +82,7 @@ public extension HTTP { !subRequestResponseConverters.isEmpty else { return { info, response in - guard let validResponse: R = response as? R else { throw HTTPError.invalidResponse } + guard let validResponse: R = response as? R else { throw NetworkError.invalidResponse } return validResponse } @@ -93,20 +93,20 @@ public extension HTTP { return { info, response in let convertedResponse: Any = try { switch response { - case let batchResponse as HTTP.BatchResponse: - return HTTP.BatchResponse( + case let batchResponse as Network.BatchResponse: + return Network.BatchResponse( data: try subRequestResponseConverters .map { index, responseConverter in guard batchResponse.count > index else { - throw HTTPError.invalidResponse + throw NetworkError.invalidResponse } return try responseConverter(info, batchResponse[index]) } ) - case let batchResponseMap as HTTP.BatchResponseMap: - return HTTP.BatchResponseMap( + case let batchResponseMap as Network.BatchResponseMap: + return Network.BatchResponseMap( data: try subRequestResponseConverters .reduce(into: [E: Any]()) { result, subResponse in let index: Int = subResponse.0 @@ -115,20 +115,20 @@ public extension HTTP { guard batchEndpoints.count > index, let targetResponse: Any = batchResponseMap[batchEndpoints[index]] - else { throw HTTPError.invalidResponse } + else { throw NetworkError.invalidResponse } let endpoint: E = batchEndpoints[index] result[endpoint] = try responseConverter(info, targetResponse) } ) - default: throw HTTPError.invalidResponse + default: throw NetworkError.invalidResponse } }() guard let validResponse: R = convertedResponse as? R else { SNLog("[PreparedRequest] Unable to convert responses for missing response") - throw HTTPError.invalidResponse + throw NetworkError.invalidResponse } return validResponse @@ -148,7 +148,7 @@ public extension HTTP { // indexes to get the correct response return { data in switch data.originalData { - case let batchResponse as HTTP.BatchResponse: + case let batchResponse as Network.BatchResponse: subRequestEventHandlers.forEach { index, eventHandler in guard batchResponse.count > index else { SNLog("[PreparedRequest] Unable to handle output events for missing response") @@ -158,7 +158,7 @@ public extension HTTP { eventHandler(data.info, batchResponse[index], batchResponse[index]) } - case let batchResponseMap as HTTP.BatchResponseMap: + case let batchResponseMap as Network.BatchResponseMap: subRequestEventHandlers.forEach { index, eventHandler in guard batchEndpoints.count > index, @@ -205,7 +205,7 @@ public extension HTTP { self.batchEndpoints = batchEndpoints self.batchRequestVariant = E.batchRequestVariant - self.batchResponseTypes = batchResponseTypes.defaulting(to: [HTTP.BatchSubResponse.self]) + self.batchResponseTypes = batchResponseTypes.defaulting(to: [Network.BatchSubResponse.self]) self.requireAllBatchResponses = requireAllBatchResponses self.excludedSubRequestHeaders = E.excludedSubRequestHeaders @@ -258,11 +258,11 @@ public extension HTTP { endpointName: String, path: String, batchEndpoints: [any EndpointType], - batchRequestVariant: HTTP.BatchRequest.Child.Variant, + batchRequestVariant: Network.BatchRequest.Child.Variant, batchResponseTypes: [Decodable.Type], requireAllBatchResponses: Bool, excludedSubRequestHeaders: [String], - jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer, HTTP.BatchRequest.Child.CodingKeys) throws -> ())?, + jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer, Network.BatchRequest.Child.CodingKeys) throws -> ())?, jsonBodyEncoder: ((inout SingleValueEncodingContainer) throws -> ())?, b64: String?, bytes: [UInt8]? @@ -302,7 +302,7 @@ public extension HTTP { public protocol ErasedPreparedRequest { var endpointName: String { get } - var batchRequestVariant: HTTP.BatchRequest.Child.Variant { get } + var batchRequestVariant: Network.BatchRequest.Child.Variant { get } var batchResponseTypes: [Decodable.Type] { get } var excludedSubRequestHeaders: [String] { get } @@ -315,7 +315,7 @@ public protocol ErasedPreparedRequest { func encodeForBatchRequest(to encoder: Encoder) throws } -extension HTTP.PreparedRequest: ErasedPreparedRequest { +extension Network.PreparedRequest: ErasedPreparedRequest { public var erasedResponseConverter: ((ResponseInfoType, Any) throws -> Any) { let originalType: Decodable.Type = self.originalType let converter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter @@ -323,7 +323,7 @@ extension HTTP.PreparedRequest: ErasedPreparedRequest { return { info, data in switch data { case let subResponse as ErasedBatchSubResponse: - return HTTP.BatchSubResponse( + return Network.BatchSubResponse( code: subResponse.code, headers: subResponse.headers, body: try originalType.from(subResponse.erasedBody).map { try converter(info, $0) } @@ -381,7 +381,7 @@ extension HTTP.PreparedRequest: ErasedPreparedRequest { SNLog("Attempted to encode unsupported request type \(endpointName) as a batch subrequest") case .sogs: - var container: KeyedEncodingContainer = encoder.container(keyedBy: HTTP.BatchRequest.Child.CodingKeys.self) + var container: KeyedEncodingContainer = encoder.container(keyedBy: Network.BatchRequest.Child.CodingKeys.self) // Exclude request signature headers (not used for sub-requests) let excludedSubRequestHeaders: [String] = excludedSubRequestHeaders.map { $0.lowercased() } @@ -408,13 +408,13 @@ extension HTTP.PreparedRequest: ErasedPreparedRequest { // MARK: - Transformations -public extension HTTP.PreparedRequest { +public extension Network.PreparedRequest { func signed( _ db: Database, - with requestSigner: (Database, HTTP.PreparedRequest, Dependencies) throws -> URLRequest, + with requestSigner: (Database, Network.PreparedRequest, Dependencies) throws -> URLRequest, using dependencies: Dependencies - ) throws -> HTTP.PreparedRequest { - return HTTP.PreparedRequest( + ) throws -> Network.PreparedRequest { + return Network.PreparedRequest( request: try requestSigner(db, self, dependencies), target: target, originalType: originalType, @@ -446,11 +446,11 @@ public extension HTTP.PreparedRequest { /// Due to the way prepared requests work we need to cast between different types and as a result can't avoid potentially /// throwing when mapping so the `map` function just calls through to the `tryMap` function, but we have both to make /// the interface more consistent for dev use - func map(transform: @escaping (ResponseInfoType, R) throws -> O) -> HTTP.PreparedRequest { + func map(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest { return tryMap(transform: transform) } - func tryMap(transform: @escaping (ResponseInfoType, R) throws -> O) -> HTTP.PreparedRequest { + func tryMap(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest { let originalConverter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter let responseConverter: ((ResponseInfoType, Any) throws -> O) = { info, response in let validResponse: R = try originalConverter(info, response) @@ -458,7 +458,7 @@ public extension HTTP.PreparedRequest { return try transform(info, validResponse) } - return HTTP.PreparedRequest( + return Network.PreparedRequest( request: request, target: target, originalType: originalType, @@ -468,7 +468,7 @@ public extension HTTP.PreparedRequest { cachedResponse: cachedResponse.map { data in (try? responseConverter(data.info, data.convertedData)) .map { convertedData in - HTTP.PreparedRequest.CachedResponse( + Network.PreparedRequest.CachedResponse( info: data.info, originalData: data.originalData, convertedData: convertedData @@ -513,7 +513,7 @@ public extension HTTP.PreparedRequest { receiveOutput: (((ResponseInfoType, R)) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, receiveCancel: (() -> Void)? = nil - ) -> HTTP.PreparedRequest { + ) -> Network.PreparedRequest { let subscriptionHandler: (() -> Void)? = { switch (self.subscriptionHandler, receiveSubscription) { case (.none, .none): return nil @@ -567,7 +567,7 @@ public extension HTTP.PreparedRequest { } }() - return HTTP.PreparedRequest( + return Network.PreparedRequest( request: request, target: target, originalType: originalType, @@ -599,20 +599,20 @@ public extension HTTP.PreparedRequest { // MARK: - Response -public extension HTTP.PreparedRequest { +public extension Network.PreparedRequest { static func cached( _ cachedResponse: R, endpoint: E - ) -> HTTP.PreparedRequest where R: Decodable { - return HTTP.PreparedRequest( + ) -> Network.PreparedRequest where R: Decodable { + return Network.PreparedRequest( request: URLRequest(url: URL(fileURLWithPath: "")), - target: HTTP.ServerTarget(server: "", endpoint: endpoint, path: "", queryParameters: [:], x25519PublicKey: ""), + target: Network.ServerTarget(server: "", endpoint: endpoint, queryParameters: [:], x25519PublicKey: ""), originalType: R.self, responseType: R.self, retryCount: 0, timeout: 0, - cachedResponse: HTTP.PreparedRequest.CachedResponse( - info: HTTP.ResponseInfo(code: 0, headers: [:]), + cachedResponse: Network.PreparedRequest.CachedResponse( + info: Network.ResponseInfo(code: 0, headers: [:]), originalData: cachedResponse, convertedData: cachedResponse ), @@ -641,7 +641,7 @@ public extension HTTP.PreparedRequest { // MARK: - HTTP.PreparedRequest.CachedResponse public extension Publisher where Failure == Error { - func eraseToAnyPublisher() -> AnyPublisher<(ResponseInfoType, R), Error> where Output == HTTP.PreparedRequest.CachedResponse { + func eraseToAnyPublisher() -> AnyPublisher<(ResponseInfoType, R), Error> where Output == Network.PreparedRequest.CachedResponse { return self .map { ($0.info, $0.convertedData) } .eraseToAnyPublisher() @@ -662,16 +662,16 @@ public extension Decodable { public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error { func decoded( - with preparedRequest: HTTP.PreparedRequest, + with preparedRequest: Network.PreparedRequest, using dependencies: Dependencies - ) -> AnyPublisher.CachedResponse, Error> { + ) -> AnyPublisher.CachedResponse, Error> { self - .tryMap { responseInfo, maybeData -> HTTP.PreparedRequest.CachedResponse in + .tryMap { responseInfo, maybeData -> Network.PreparedRequest.CachedResponse in // Depending on the 'originalType' we need to process the response differently let targetData: Any = try { switch preparedRequest.originalType { case let erasedBatchResponse as ErasedBatchResponseMap.Type: - let response: HTTP.BatchResponse = try HTTP.BatchResponse.decodingResponses( + let response: Network.BatchResponse = try Network.BatchResponse.decodingResponses( from: maybeData, as: preparedRequest.batchResponseTypes, requireAllResults: preparedRequest.requireAllBatchResponses, @@ -683,8 +683,8 @@ public extension Publisher where Output == (ResponseInfoType, Data?), Failure == response: response ) - case is HTTP.BatchResponse.Type: - return try HTTP.BatchResponse.decodingResponses( + case is Network.BatchResponse.Type: + return try Network.BatchResponse.decodingResponses( from: maybeData, as: preparedRequest.batchResponseTypes, requireAllResults: preparedRequest.requireAllBatchResponses, @@ -693,7 +693,7 @@ public extension Publisher where Output == (ResponseInfoType, Data?), Failure == case is NoResponse.Type: return NoResponse() case is Optional.Type: return maybeData as Any - case is Data.Type: return try maybeData ?? { throw HTTPError.parsingFailed }() + case is Data.Type: return try maybeData ?? { throw NetworkError.parsingFailed }() case is _OptionalProtocol.Type: guard let data: Data = maybeData else { return maybeData as Any } @@ -701,14 +701,14 @@ public extension Publisher where Output == (ResponseInfoType, Data?), Failure == return try preparedRequest.originalType.decoded(from: data, using: dependencies) default: - guard let data: Data = maybeData else { throw HTTPError.parsingFailed } + guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return try preparedRequest.originalType.decoded(from: data, using: dependencies) } }() // Generate and return the converted data - return HTTP.PreparedRequest.CachedResponse( + return Network.PreparedRequest.CachedResponse( info: responseInfo, originalData: targetData, convertedData: try preparedRequest.responseConverter(responseInfo, targetData) diff --git a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift index 25a55073b..791085a93 100644 --- a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift +++ b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift @@ -519,9 +519,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio assetDescription: assetDescription, priority: priority, success: { request, asset in resolver(Result.success((asset, request))) }, - failure: { request in - resolver(Result.failure(HTTPError.generic)) - } + failure: { request in resolver(Result.failure(NetworkError.invalidResponse)) } ) assetRequest.shouldIgnoreSignalProxy = shouldIgnoreSignalProxy self?.assetRequestQueue.append(assetRequest) diff --git a/SessionUtilitiesKit/Networking/Request.swift b/SessionUtilitiesKit/Networking/Request.swift index 9a356052a..f9fc5baf9 100644 --- a/SessionUtilitiesKit/Networking/Request.swift +++ b/SessionUtilitiesKit/Networking/Request.swift @@ -3,6 +3,8 @@ import Foundation // MARK: - Convenience Types public struct Empty: Codable { + public static let null: Empty? = nil + public init() {} } @@ -11,14 +13,14 @@ public typealias NoResponse = Empty public protocol EndpointType: Hashable { static var name: String { get } - static var batchRequestVariant: HTTP.BatchRequest.Child.Variant { get } + static var batchRequestVariant: Network.BatchRequest.Child.Variant { get } static var excludedSubRequestHeaders: [HTTPHeader] { get } var path: String { get } } public extension EndpointType { - static var batchRequestVariant: HTTP.BatchRequest.Child.Variant { .unsupported } + static var batchRequestVariant: Network.BatchRequest.Child.Variant { .unsupported } static var excludedSubRequestHeaders: [HTTPHeader] { [] } } @@ -61,7 +63,7 @@ public struct Request { case let bodyString as String: // The only acceptable string body is a base64 encoded one guard let encodedData: Data = Data(base64Encoded: bodyString) else { - throw HTTPError.parsingFailed + throw NetworkError.parsingFailed } return encodedData @@ -80,7 +82,7 @@ public struct Request { // MARK: - Request Generation public func generateUrlRequest(using dependencies: Dependencies) throws -> URLRequest { - guard let url: URL = target.url else { throw HTTPError.invalidURL } + guard let url: URL = target.url else { throw NetworkError.invalidURL } var urlRequest: URLRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue diff --git a/SessionUtilitiesKit/Networking/RequestInfo.swift b/SessionUtilitiesKit/Networking/RequestInfo.swift index 156cd54da..a3b05bbbb 100644 --- a/SessionUtilitiesKit/Networking/RequestInfo.swift +++ b/SessionUtilitiesKit/Networking/RequestInfo.swift @@ -2,7 +2,7 @@ import Foundation -public extension HTTP { +public extension Network { struct RequestInfo: Codable { let method: String let endpoint: String diff --git a/SessionUtilitiesKit/Networking/RequestTarget.swift b/SessionUtilitiesKit/Networking/RequestTarget.swift index 773d7094f..a9cb87c7d 100644 --- a/SessionUtilitiesKit/Networking/RequestTarget.swift +++ b/SessionUtilitiesKit/Networking/RequestTarget.swift @@ -10,8 +10,10 @@ public protocol RequestTarget: Equatable { } public protocol ServerRequestTarget: RequestTarget { + associatedtype Endpoint: EndpointType + var server: String { get } - var rawEndpoint: String { get } + var endpoint: Endpoint { get } var x25519PublicKey: String { get } } @@ -31,29 +33,28 @@ public extension ServerRequestTarget { // MARK: - ServerTarget -public extension HTTP { - struct ServerTarget: ServerRequestTarget { +public extension Network { + struct ServerTarget: ServerRequestTarget { + public typealias Endpoint = E + public let server: String - public let rawEndpoint: String - let path: String + public let endpoint: Endpoint let queryParameters: [HTTPQueryParam: String] public let x25519PublicKey: String public var url: URL? { URL(string: "\(server)\(urlPathAndParamsString)") } - public var urlPathAndParamsString: String { pathFor(path: path, queryParams: queryParameters) } + public var urlPathAndParamsString: String { pathFor(path: endpoint.path, queryParams: queryParameters) } // MARK: - Initialization - public init( + public init( server: String, endpoint: E, - path: String, queryParameters: [HTTPQueryParam: String], x25519PublicKey: String ) { self.server = server - self.rawEndpoint = endpoint.path - self.path = path + self.endpoint = endpoint self.queryParameters = queryParameters self.x25519PublicKey = x25519PublicKey } @@ -75,10 +76,9 @@ public extension Request { self = Request( method: method, endpoint: endpoint, - target: HTTP.ServerTarget( + target: Network.ServerTarget( server: server, endpoint: endpoint, - path: endpoint.path, queryParameters: queryParameters, x25519PublicKey: x25519PublicKey ), diff --git a/SessionUtilitiesKit/Networking/ResponseInfo.swift b/SessionUtilitiesKit/Networking/ResponseInfo.swift index ddc3572e4..041aa1ed2 100644 --- a/SessionUtilitiesKit/Networking/ResponseInfo.swift +++ b/SessionUtilitiesKit/Networking/ResponseInfo.swift @@ -7,7 +7,7 @@ public protocol ResponseInfoType: Decodable { var headers: [String: String] { get } } -public extension HTTP { +public extension Network { struct ResponseInfo: ResponseInfoType { public let code: Int public let headers: [String: String] diff --git a/SessionUtilitiesKit/Utilities/Bencode.swift b/SessionUtilitiesKit/Utilities/Bencode.swift index c478a5c2d..368411d3d 100644 --- a/SessionUtilitiesKit/Utilities/Bencode.swift +++ b/SessionUtilitiesKit/Utilities/Bencode.swift @@ -57,7 +57,7 @@ public enum Bencode { decodedData.remainingData.isEmpty == true, // Ensure there is no left over data let resultArray: [Any] = decodedData.value as? [Any], resultArray.count > 0 - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } return BencodeResponse( info: try Bencode.decode(T.self, decodedValue: resultArray[0], using: dependencies), @@ -80,7 +80,7 @@ public enum Bencode { guard let decodedData: (value: Any, remainingData: Data) = decodeData(data), decodedData.remainingData.isEmpty == true // Ensure there is no left over data - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } return try Bencode.decode(T.self, decodedValue: decodedData.value, using: dependencies) } @@ -95,7 +95,7 @@ public enum Bencode { case (let bencodeString as BencodeString, is String.Type), (let bencodeString as BencodeString, is Optional.Type): - return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }()) + return try (bencodeString.value as? T ?? { throw NetworkError.parsingFailed }()) case (let bencodeString as BencodeString, _): return try bencodeString.rawValue.decoded(as: T.self, using: dependencies) @@ -104,7 +104,7 @@ public enum Bencode { guard let jsonifiedInfo: Any = try? jsonify(decodedValue), let infoData: Data = try? JSONSerialization.data(withJSONObject: jsonifiedInfo) - else { throw HTTPError.parsingFailed } + else { throw NetworkError.parsingFailed } return try infoData.decoded(as: T.self, using: dependencies) } diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index f054cd21f..389578667 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -1689,7 +1689,7 @@ fileprivate struct TestDetails: Codable { } fileprivate struct InvalidDetails: Codable { - func encode(to encoder: Encoder) throws { throw HTTPError.parsingFailed } + func encode(to encoder: Encoder) throws { throw NetworkError.parsingFailed } } fileprivate enum TestJob: JobExecutor { diff --git a/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift b/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift index 25d787409..9e2c0c0a0 100644 --- a/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift +++ b/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift @@ -177,7 +177,7 @@ class BatchResponseSpec: QuickSpec { as: [Int.self], requireAllResults: true ) - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: -- fails if the data is not JSON @@ -188,7 +188,7 @@ class BatchResponseSpec: QuickSpec { as: [Int.self], requireAllResults: true ) - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: -- fails if the data is not a JSON array @@ -199,7 +199,7 @@ class BatchResponseSpec: QuickSpec { as: [Int.self], requireAllResults: true ) - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: -- and requiring all responses @@ -216,7 +216,7 @@ class BatchResponseSpec: QuickSpec { ], requireAllResults: true ) - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: ---- fails if one of the JSON array values fails to decode @@ -245,7 +245,7 @@ class BatchResponseSpec: QuickSpec { ], requireAllResults: true ) - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } } @@ -263,7 +263,7 @@ class BatchResponseSpec: QuickSpec { ], requireAllResults: false ) - }.toNot(throwError(HTTPError.parsingFailed)) + }.toNot(throwError(NetworkError.parsingFailed)) } } } diff --git a/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift b/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift index 4978ccc8d..ae50357d0 100644 --- a/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift +++ b/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift @@ -118,7 +118,7 @@ class PreparedRequestSpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkUntilComplete() - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) } // MARK: -- fails if the data is not JSON @@ -131,7 +131,7 @@ class PreparedRequestSpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkUntilComplete() - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) } // MARK: -- fails if the data is not a JSON array @@ -144,7 +144,7 @@ class PreparedRequestSpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkUntilComplete() - expect(error).to(matchError(HTTPError.parsingFailed)) + expect(error).to(matchError(NetworkError.parsingFailed)) } } } diff --git a/SessionUtilitiesKitTests/Networking/RequestSpec.swift b/SessionUtilitiesKitTests/Networking/RequestSpec.swift index b7df5e4a4..5bfec29b0 100644 --- a/SessionUtilitiesKitTests/Networking/RequestSpec.swift +++ b/SessionUtilitiesKitTests/Networking/RequestSpec.swift @@ -91,7 +91,7 @@ class RequestSpec: QuickSpec { expect { try request.generateUrlRequest(using: dependencies) } - .to(throwError(HTTPError.invalidURL)) + .to(throwError(NetworkError.invalidURL)) } // MARK: ---- with a base64 string body @@ -124,7 +124,7 @@ class RequestSpec: QuickSpec { expect { try request.generateUrlRequest(using: dependencies) } - .to(throwError(HTTPError.parsingFailed)) + .to(throwError(NetworkError.parsingFailed)) } } diff --git a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift index 0ed525738..76db48afa 100644 --- a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift +++ b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift @@ -110,7 +110,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: ------ throws a parsing error when given an invalid key @@ -121,7 +121,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } // MARK: ------ decodes correctly when trying to decode an int to a bool with custom handling @@ -132,7 +132,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.toNot(throwError(HTTPError.parsingFailed)) + }.toNot(throwError(NetworkError.parsingFailed)) } // MARK: ------ throws a parsing error when trying to decode an int to a bool @@ -143,7 +143,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } } @@ -193,7 +193,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } } @@ -234,7 +234,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } } @@ -275,7 +275,7 @@ class BencodeSpec: QuickSpec { expect { let result: BencodeResponse = try Bencode.decodeResponse(from: data) _ = result - }.to(throwError(HTTPError.parsingFailed)) + }.to(throwError(NetworkError.parsingFailed)) } } }