[WIP] Cleaned up interface, error handling and redundant code

pull/960/head
Morgan Pretty 3 months ago
parent 1ee4c29a16
commit 7eb7eda74e

@ -1 +1 @@
Subproject commit 6dab3b99208b9be410952174e72cb38bb0dedb27
Subproject commit 4e79b252d480c24e1cb543c5a65d4d4f5a7b5fdd

@ -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 = "<group>"; };
C3C2A5B7255385EC00C340D1 /* Snode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snode.swift; sourceTree = "<group>"; };
C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
C3C2A5BC255385EE00C340D1 /* HTTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; };
C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = "<group>"; };
C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = "<group>"; };
@ -1980,7 +1968,6 @@
FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = "<group>"; };
FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = "<group>"; };
FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIRequest.swift; sourceTree = "<group>"; };
FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = "<group>"; };
FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = "<group>"; };
FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = "<group>"; };
@ -2040,12 +2027,6 @@
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = "<group>"; };
FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = "<group>"; };
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed1-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed2-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed3-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = "<group>"; };
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = "<group>"; };
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = "<group>"; };
FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = "<group>"; };
@ -2078,7 +2059,7 @@
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = "<group>"; };
FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = "<group>"; };
FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPQueryParam.swift; sourceTree = "<group>"; };
FDF8487629405906007DCAE5 /* HTTPError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = "<group>"; };
FDF8487629405906007DCAE5 /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
FDF8487729405906007DCAE5 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = "<group>"; };
FDF8487829405906007DCAE5 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = "<group>"; };
FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = "<group>"; };
@ -2121,13 +2102,10 @@
FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = "<group>"; };
FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = "<group>"; };
FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = "<group>"; };
FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIVersion.swift; sourceTree = "<group>"; };
FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = "<group>"; };
FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = "<group>"; };
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIDestination.swift; sourceTree = "<group>"; };
FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIError.swift; sourceTree = "<group>"; };
FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = "<group>"; };
FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = "<group>"; };
FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+OnionRequestAPI.swift"; sourceTree = "<group>"; };
FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = "<group>"; };
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 = "<group>";
};
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 = "<group>";
};
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 */,

@ -1352,7 +1352,7 @@ extension ConversationVC:
guard cellViewModel.threadVariant == .community else { return }
Storage.shared
.readPublisher { db -> (HTTP.PreparedRequest<OpenGroupAPI.ReactionRemoveAllResponse>, OpenGroupAPI.PendingChange) in
.readPublisher { db -> (Network.PreparedRequest<OpenGroupAPI.ReactionRemoveAllResponse>, 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<OpenGroupAPI.ReactionRemoveAllResponse> = try OpenGroupAPI
let preparedRequest: Network.PreparedRequest<OpenGroupAPI.ReactionRemoveAllResponse> = try OpenGroupAPI
.preparedReactionDeleteAll(
db,
emoji: emoji,
@ -1453,7 +1453,7 @@ extension ConversationVC:
typealias OpenGroupInfo = (
pendingReaction: Reaction?,
pendingChange: OpenGroupAPI.PendingChange,
preparedRequest: HTTP.PreparedRequest<Int64?>
preparedRequest: Network.PreparedRequest<Int64?>
)
/// 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<Int64?> = try {
let preparedRequest: Network.PreparedRequest<Int64?> = 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<NoResponse> in
.readPublisher { db -> Network.PreparedRequest<NoResponse> in
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
throw StorageError.objectNotFound
}

@ -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

@ -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-----

@ -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-----

@ -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-----

@ -57,6 +57,8 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>seed1.getsession.org</key>
@ -85,7 +87,7 @@
<key>NSCameraUsageDescription</key>
<string>Session needs camera access to take pictures and scan QR codes.</string>
<key>NSFaceIDUsageDescription</key>
<string>Session&apos;s Screen Lock feature uses Face ID.</string>
<string>Session's Screen Lock feature uses Face ID.</string>
<key>NSHumanReadableCopyright</key>
<string>com.loki-project.loki-messenger</string>
<key>NSMicrophoneUsageDescription</key>

@ -32,19 +32,17 @@ enum Onboarding {
) -> AnyPublisher<String?, Error> {
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 }

@ -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<String?, Error> in
SNLog("Onboarding failed to retrieve existing profile information")
return Just(nil)

@ -172,7 +172,7 @@ final class NukeDataModal: Modal {
Publishers
.MergeMany(
Storage.shared
.read { db -> [(String, HTTP.PreparedRequest<OpenGroupAPI.DeleteInboxResponse>)] in
.read { db -> [(String, Network.PreparedRequest<OpenGroupAPI.DeleteInboxResponse>)] in
return try OpenGroup
.filter(OpenGroup.Columns.isActive == true)
.select(.server)

@ -75,19 +75,16 @@ public final class BackgroundPoller {
) -> AnyPublisher<Void, Error> {
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()
}

@ -1080,7 +1080,7 @@ extension Attachment {
let attachmentId: String = self.id
return Storage.shared
.writePublisher { db -> (HTTP.PreparedRequest<FileUploadResponse>?, String?, Data?, Data?) in
.writePublisher { db -> (Network.PreparedRequest<FileUploadResponse>?, 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<FileUploadResponse>? = try {
let preparedSendData: Network.PreparedRequest<FileUploadResponse>? = try {
switch destination {
case .openGroup(let openGroup):
return try OpenGroupAPI

@ -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

@ -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<R> {
return HTTP.PreparedRequest<R>(
) throws -> Network.PreparedRequest<R> {
return Network.PreparedRequest<R>(
request: request,
urlRequest: try request.generateUrlRequest(using: dependencies),
responseType: responseType,

@ -95,7 +95,7 @@ public enum AttachmentDownloadJob: JobExecutor {
else { throw AttachmentDownloadError.invalidUrl }
return Storage.shared
.readPublisher { db -> HTTP.PreparedRequest<Data>? in
.readPublisher { db -> Network.PreparedRequest<Data>? in
try OpenGroup.fetchOne(db, id: threadId)
.map { openGroup in
try OpenGroupAPI
@ -109,7 +109,7 @@ public enum AttachmentDownloadJob: JobExecutor {
}
}
.flatMap { maybePreparedRequest -> AnyPublisher<Data, Error> in
guard let preparedRequest: HTTP.PreparedRequest<Data> = maybePreparedRequest else {
guard let preparedRequest: Network.PreparedRequest<Data> = 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

@ -92,7 +92,7 @@ public enum ConfigurationSyncJob: JobExecutor {
)
}
}
.flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher<HTTP.BatchResponse, Error> in
.flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher<Network.BatchResponse, Error> 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<SendMessagesResponse> = (subResponse as? HTTP.BatchSubResponse<SendMessagesResponse>),
let typedResponse: Network.BatchSubResponse<SendMessagesResponse> = (subResponse as? Network.BatchSubResponse<SendMessagesResponse>),
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
)

@ -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,

@ -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
)

@ -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:

@ -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
}
}

@ -33,7 +33,7 @@ public enum OpenGroupAPI {
hasPerformedInitialPoll: Bool,
timeSinceLastPoll: TimeInterval,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<HTTP.BatchResponseMap<Endpoint>> {
) throws -> Network.PreparedRequest<Network.BatchResponseMap<Endpoint>> {
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<HTTP.BatchResponseMap<Endpoint>> {
) throws -> Network.PreparedRequest<Network.BatchResponseMap<Endpoint>> {
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<Endpoint>.self,
responseType: Network.BatchResponseMap<Endpoint>.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<HTTP.BatchResponseMap<Endpoint>> {
) throws -> Network.PreparedRequest<Network.BatchResponseMap<Endpoint>> {
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<Endpoint>.self,
responseType: Network.BatchResponseMap<Endpoint>.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<Capabilities> {
) throws -> Network.PreparedRequest<Capabilities> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -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<NoBody, Endpoint>(
@ -254,7 +254,7 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<Room> {
) throws -> Network.PreparedRequest<Room> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -278,7 +278,7 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<RoomPollInfo> {
) throws -> Network.PreparedRequest<RoomPollInfo> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -304,7 +304,7 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<CapabilitiesAndRoomResponse> {
) throws -> Network.PreparedRequest<CapabilitiesAndRoomResponse> {
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<Endpoint>) -> CapabilitiesAndRoomResponse in
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (response[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
.map { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomResponse in
let maybeCapabilities: Network.BatchSubResponse<Capabilities>? = (response[.capabilities] as? Network.BatchSubResponse<Capabilities>)
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<Room>? = (maybeRoomResponse as? HTTP.BatchSubResponse<Room>)
let maybeRoom: Network.BatchSubResponse<Room>? = (maybeRoomResponse as? Network.BatchSubResponse<Room>)
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<CapabilitiesAndRoomsResponse> {
) throws -> Network.PreparedRequest<CapabilitiesAndRoomsResponse> {
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<Endpoint>) -> CapabilitiesAndRoomsResponse in
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (response[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
let maybeRooms: HTTP.BatchSubResponse<[Room]>? = response.data
.map { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomsResponse in
let maybeCapabilities: Network.BatchSubResponse<Capabilities>? = (response[.capabilities] as? Network.BatchSubResponse<Capabilities>)
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<Message> {
) throws -> Network.PreparedRequest<Message> {
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<Message> {
) throws -> Network.PreparedRequest<Message> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -468,7 +468,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
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<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -528,7 +528,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
) throws -> Network.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -558,7 +558,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
) throws -> Network.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -588,7 +588,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
) throws -> Network.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -625,7 +625,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -650,7 +650,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
/// 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<ReactionAddResponse> {
) throws -> Network.PreparedRequest<ReactionAddResponse> {
/// 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<ReactionRemoveResponse> {
) throws -> Network.PreparedRequest<ReactionRemoveResponse> {
/// 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<ReactionRemoveAllResponse> {
) throws -> Network.PreparedRequest<ReactionRemoveAllResponse> {
/// 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<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -806,7 +806,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -829,7 +829,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -859,7 +859,7 @@ public enum OpenGroupAPI {
to roomToken: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<FileUploadResponse> {
) throws -> Network.PreparedRequest<FileUploadResponse> {
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<Data> {
) throws -> Network.PreparedRequest<Data> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -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<NoBody, Endpoint>(
@ -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<NoBody, Endpoint>(
@ -957,7 +957,7 @@ public enum OpenGroupAPI {
_ db: Database,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<DeleteInboxResponse> {
) throws -> Network.PreparedRequest<DeleteInboxResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
@ -981,7 +981,7 @@ public enum OpenGroupAPI {
toInboxFor blindedSessionId: String,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<SendDirectMessageResponse> {
) throws -> Network.PreparedRequest<SendDirectMessageResponse> {
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<NoBody, Endpoint>(
@ -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<NoBody, Endpoint>(
@ -1082,7 +1082,7 @@ public enum OpenGroupAPI {
from roomTokens: [String]? = nil,
on server: String,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
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<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
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<NoResponse> {
) throws -> Network.PreparedRequest<NoResponse> {
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<HTTP.BatchResponseMap<Endpoint>> {
) throws -> Network.PreparedRequest<Network.BatchResponseMap<Endpoint>> {
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<R>(
_ db: Database,
preparedRequest: HTTP.PreparedRequest<R>,
preparedRequest: Network.PreparedRequest<R>,
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<OpenGroupAPI.Endpoint> = preparedRequest.target as? Network.OpenGroupAPITarget<OpenGroupAPI.Endpoint>
else { throw OpenGroupAPIError.signingFailed }
var updatedRequest: URLRequest = preparedRequest.request
@ -1420,10 +1420,10 @@ public enum OpenGroupAPI {
private static func prepareRequest<T: Encodable, R: Decodable>(
request: Request<T, Endpoint>,
responseType: R.Type,
timeout: TimeInterval = HTTP.defaultTimeout,
timeout: TimeInterval = Network.defaultTimeout,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<R> {
return HTTP.PreparedRequest(
) throws -> Network.PreparedRequest<R> {
return Network.PreparedRequest(
request: request,
urlRequest: try request.generateUrlRequest(using: dependencies),
responseType: responseType,

@ -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<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
.readPublisher { db -> Network.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse> 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<Data>?) in
.readPublisher { db -> (Data?, Network.PreparedRequest<Data>?) 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()
}
}

@ -6,16 +6,18 @@ import SessionUtilitiesKit
// MARK: - OpenGroupAPITarget
internal extension HTTP {
struct OpenGroupAPITarget: ServerRequestTarget {
internal extension Network {
struct OpenGroupAPITarget<E: EndpointType>: 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

@ -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
]

@ -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))

@ -118,7 +118,7 @@ extension MessageReceiver {
case .group:
SNLog("Ignoring message with invalid sender.")
throw HTTPError.parsingFailed
throw NetworkError.parsingFailed
}
}()

@ -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:

@ -1,33 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
public struct PushNotificationAPIRequest<T: Encodable>: 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<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(endpoint.rawValue, forKey: .method)
try container.encode(body, forKey: .body)
}
}

@ -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<T, Endpoint>,
responseType: R.Type,
retryCount: Int = 0,
timeout: TimeInterval = HTTP.defaultTimeout,
timeout: TimeInterval = Network.defaultTimeout,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<R> {
return HTTP.PreparedRequest<R>(
) throws -> Network.PreparedRequest<R> {
return Network.PreparedRequest<R>(
request: request,
urlRequest: try request.generateUrlRequest(using: dependencies),
responseType: responseType,

@ -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
),

@ -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

@ -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

@ -126,7 +126,7 @@ extension OpenGroupAPI {
)
return dependencies.storage
.readPublisher { db -> (Int64, HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>) in
.readPublisher { db -> (Int64, Network.PreparedRequest<Network.BatchResponseMap<OpenGroupAPI.Endpoint>>) 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<OpenGroupAPI.Endpoint>,
response: Network.BatchResponseMap<OpenGroupAPI.Endpoint>,
failureCount: Int64,
using dependencies: Dependencies
) {
@ -381,7 +379,7 @@ extension OpenGroupAPI {
.filter { endpoint, data in
switch endpoint {
case .capabilities:
guard (data as? HTTP.BatchSubResponse<Capabilities>)?.body != nil else {
guard (data as? Network.BatchSubResponse<Capabilities>)?.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<RoomPollInfo>)?.body != nil else {
switch (data as? HTTP.BatchSubResponse<RoomPollInfo>)?.code {
guard (data as? Network.BatchSubResponse<RoomPollInfo>)?.body != nil else {
switch (data as? Network.BatchSubResponse<RoomPollInfo>)?.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<Message>]> = data as? HTTP.BatchSubResponse<[Failable<Message>]>,
let responseData: Network.BatchSubResponse<[Failable<Message>]> = data as? Network.BatchSubResponse<[Failable<Message>]>,
let responseBody: [Failable<Message>] = responseData.body
else {
switch (data as? HTTP.BatchSubResponse<[Failable<Message>]>)?.code {
switch (data as? Network.BatchSubResponse<[Failable<Message>]>)?.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<Capabilities> = data as? HTTP.BatchSubResponse<Capabilities>,
let responseData: Network.BatchSubResponse<Capabilities> = data as? Network.BatchSubResponse<Capabilities>,
let responseBody: Capabilities = responseData.body
else { return false }
@ -489,7 +487,7 @@ extension OpenGroupAPI {
case .roomPollInfo(let roomToken, _):
guard
let responseData: HTTP.BatchSubResponse<RoomPollInfo> = data as? HTTP.BatchSubResponse<RoomPollInfo>,
let responseData: Network.BatchSubResponse<RoomPollInfo> = data as? Network.BatchSubResponse<RoomPollInfo>,
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<Capabilities> = data as? HTTP.BatchSubResponse<Capabilities>,
let responseData: Network.BatchSubResponse<Capabilities> = data as? Network.BatchSubResponse<Capabilities>,
let responseBody: Capabilities = responseData.body
else { return }
@ -538,7 +536,7 @@ extension OpenGroupAPI {
case .roomPollInfo(let roomToken, _):
guard
let responseData: HTTP.BatchSubResponse<RoomPollInfo> = data as? HTTP.BatchSubResponse<RoomPollInfo>,
let responseData: Network.BatchSubResponse<RoomPollInfo> = data as? Network.BatchSubResponse<RoomPollInfo>,
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<Message>]> = data as? HTTP.BatchSubResponse<[Failable<Message>]>,
let responseData: Network.BatchSubResponse<[Failable<Message>]> = data as? Network.BatchSubResponse<[Failable<Message>]>,
let responseBody: [Failable<Message>] = 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 }

@ -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<SwarmDrainBehaviour>]> = Atomic([:])
internal var targetSnode: Atomic<Snode?> = Atomic(nil)
private var usedSnodes: Atomic<Set<Snode>> = 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<SwarmDrainBehaviour> = 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<Snode, Error> {
// 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<Snode> = 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<SwarmDrainBehaviour>,
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<Snode, Error> = 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<SwarmDrainBehaviour>,
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)

@ -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

@ -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))
}
}
}

@ -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())
}
}

@ -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

@ -57,7 +57,7 @@ extension Snode {
}
catch {
SNLog("Failed to parse snode: \(error.localizedDescription).")
throw HTTPError.invalidJSON
throw NetworkError.parsingFailed
}
}
}

@ -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
)
}
}
}

@ -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
)
}
}
}

@ -87,7 +87,7 @@ extension GetSwarmResponse._Snode {
}
catch {
SNLog("Failed to parse snode: \(error.localizedDescription).")
throw HTTPError.invalidJSON
throw NetworkError.parsingFailed
}
}
}

@ -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?

@ -19,7 +19,7 @@ internal extension SnodeAPI {
public init<T: Encodable, R: Codable>(request: SnodeRequest<T>, responseType: R.Type) {
self.child = Child(request: request)
self.responseType = HTTP.BatchSubResponse<R>.self
self.responseType = Network.BatchSubResponse<R>.self
}
public init<T: Encodable>(request: SnodeRequest<T>) {

@ -9,7 +9,7 @@ public struct SnodeRequest<T: Encodable>: 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<T: Encodable>: 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<T> {
return SnodeRequest(
endpoint: self.endpoint,
body: self.body.with(timestampMs: timestampMs)
)
}
}

@ -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<Data, Error> {
// 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<Int32>.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<AES.GCM.EncryptionResult, Error> {
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<AES.GCM.EncryptionResult, Error> {
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()
}
}

@ -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<Data?> {
static func onionRequest(
_ payload: Data,
to snode: Snode,
swarmPublicKey: String?,
timeout: TimeInterval = Network.defaultTimeout
) -> Network.RequestType<Data?> {
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<Data?> {
static func onionRequest<E: EndpointType>(
_ request: URLRequest,
to server: String,
endpoint: E,
with x25519PublicKey: String,
timeout: TimeInterval = Network.defaultTimeout
) -> Network.RequestType<Data?> {
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<AnyPublisher<[[Snode]], Error>?> = 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<Set<Snode>> = 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<Void, Error> {
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<OnionBuildingResult, Error> {
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<AES.GCM.EncryptionResult, Error> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination)
.flatMap { r -> AnyPublisher<AES.GCM.EncryptionResult, Error> 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<AES.GCM.EncryptionResult, Error> {
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<AES.GCM.EncryptionResult, Error> 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<HTTP.ResponseInfo> = 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)
}
}

@ -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<R>.CachedResponse = self.cachedResponse {
if let cachedResponse: Network.PreparedRequest<R>.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<R>(
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == HTTP.PreparedRequest<R> {
) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == Network.PreparedRequest<R> {
guard let instance: Wrapped = self else {
return Fail(error: SnodeAPIError.invalidPreparedRequest)
return Fail(error: NetworkError.invalidPreparedRequest)
.eraseToAnyPublisher()
}

@ -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)

File diff suppressed because it is too large Load Diff

@ -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<network_service_node> = UnsafePointer<network_service_node>(cChanges.nodes)
(0..<cChanges.nodes_count).forEach { index in
pendingNodes.append(
Snode(
ip: String(libSessionVal: cNodes[index].ip),
lmqPort: cNodes[index].lmq_port,
x25519PublicKey: String(libSessionVal: cNodes[index].x25519_pubkey_hex),
ed25519PublicKey: String(libSessionVal: cNodes[index].ed25519_pubkey_hex)
)
)
pendingNodeFailureCount.append(UInt(cNodes[index].failure_count))
pendingNodeInvalid.append(cNodes[index].invalid)
}
self.nodes = pendingNodes
self.nodeFailureCount = pendingNodeFailureCount
self.nodeInvalid = pendingNodeInvalid
}
}
private class CWrapper {
let callback: (Bool, Bool, Int16, Data?, ServiceNodeChanges) -> 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<T>(_ pointer: UnsafePointer<T>?) {
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<CWrapper>.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<network_service_node>?, count: Int) {
guard let swarm: Set<Snode> = 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<CChar>?] = path.map { $0.x25519PublicKey.cArray }.unsafeCopy()
var ed25519Pubkeys: [UnsafePointer<CChar>?] = path.map { $0.ed25519PublicKey.cArray }.unsafeCopy()
let cNodes: UnsafePointer<onion_request_service_node>? = path
let cSwarm: UnsafePointer<network_service_node>? = 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<CChar>? = snode.x25519PublicKey.cArray.unsafeCopy()
let cEd25519Pubkey: UnsafePointer<CChar>? = 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<CWrapper>.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<CWrapper>.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<T: Encodable>(
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<network_service_node>?, 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<CWrapper>.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<T: Encodable>(
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<T: Encodable>(
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<network_service_node>? = 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<network_service_node>?, 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<CWrapper>.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<CChar>?] = (headerInfo ?? [])
.map { $0.key.cArray.nullTerminated() }
.unsafeCopy()
var cHeaderValues: [UnsafePointer<CChar>?] = (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<CWrapper>.fromOpaque(ctx!).takeRetainedValue()
.callback(success, timeout, statusCode, data, changes)
},
cWrapperPtr
)
}
}
}.eraseToAnyPublisher()
}

@ -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
}
}
}

@ -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"
}
}
}

@ -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"
}

@ -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

@ -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 }
}
}
}

@ -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))
}

@ -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 {

@ -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
}

@ -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

@ -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<T> 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<CodingKeys> = 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) }
)

@ -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<Certificates> = {
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<Data, Error> {
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<Data, Error> {
guard let url: URL = URL(string: url) else {
return Fail<Data, Error>(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<Data, Error>(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<Data, Error>(error: HTTPError.httpRequestFailed(statusCode: statusCode, data: data))
.eraseToAnyPublisher()
}
return Just(data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

@ -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."
}
}
}

@ -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."
}
}
}

@ -8,6 +8,8 @@ public protocol NetworkType {
}
public class Network: NetworkType {
public static let defaultTimeout: TimeInterval = 10
public struct RequestType<T> {
public let id: String
public let url: String?

@ -4,9 +4,9 @@ import Foundation
import Combine
import GRDB
// MARK: - HTTP.PreparedRequest<R>
// MARK: - Network.PreparedRequest<R>
public extension HTTP {
public extension Network {
struct PreparedRequest<R> {
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>, HTTP.BatchRequest.Child.CodingKeys) throws -> ())?
private let jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys>, 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<E>:
return HTTP.BatchResponseMap(
case let batchResponseMap as Network.BatchResponseMap<E>:
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<E>:
case let batchResponseMap as Network.BatchResponseMap<E>:
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<R>.self])
self.batchResponseTypes = batchResponseTypes.defaulting(to: [Network.BatchSubResponse<R>.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>, HTTP.BatchRequest.Child.CodingKeys) throws -> ())?,
jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys>, 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<HTTP.BatchRequest.Child.CodingKeys> = encoder.container(keyedBy: HTTP.BatchRequest.Child.CodingKeys.self)
var container: KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys> = 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<R>, Dependencies) throws -> URLRequest,
with requestSigner: (Database, Network.PreparedRequest<R>, Dependencies) throws -> URLRequest,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<R> {
return HTTP.PreparedRequest(
) throws -> Network.PreparedRequest<R> {
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<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> HTTP.PreparedRequest<O> {
func map<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest<O> {
return tryMap(transform: transform)
}
func tryMap<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> HTTP.PreparedRequest<O> {
func tryMap<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest<O> {
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<O>(
return Network.PreparedRequest<O>(
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<O>.CachedResponse(
Network.PreparedRequest<O>.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<Error>) -> Void)? = nil,
receiveCancel: (() -> Void)? = nil
) -> HTTP.PreparedRequest<R> {
) -> Network.PreparedRequest<R> {
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<E: EndpointType>(
_ cachedResponse: R,
endpoint: E
) -> HTTP.PreparedRequest<R> where R: Decodable {
return HTTP.PreparedRequest(
) -> Network.PreparedRequest<R> 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<R>.CachedResponse(
info: HTTP.ResponseInfo(code: 0, headers: [:]),
cachedResponse: Network.PreparedRequest<R>.CachedResponse(
info: Network.ResponseInfo(code: 0, headers: [:]),
originalData: cachedResponse,
convertedData: cachedResponse
),
@ -641,7 +641,7 @@ public extension HTTP.PreparedRequest {
// MARK: - HTTP.PreparedRequest<R>.CachedResponse
public extension Publisher where Failure == Error {
func eraseToAnyPublisher<R>() -> AnyPublisher<(ResponseInfoType, R), Error> where Output == HTTP.PreparedRequest<R>.CachedResponse {
func eraseToAnyPublisher<R>() -> AnyPublisher<(ResponseInfoType, R), Error> where Output == Network.PreparedRequest<R>.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<R>(
with preparedRequest: HTTP.PreparedRequest<R>,
with preparedRequest: Network.PreparedRequest<R>,
using dependencies: Dependencies
) -> AnyPublisher<HTTP.PreparedRequest<R>.CachedResponse, Error> {
) -> AnyPublisher<Network.PreparedRequest<R>.CachedResponse, Error> {
self
.tryMap { responseInfo, maybeData -> HTTP.PreparedRequest<R>.CachedResponse in
.tryMap { responseInfo, maybeData -> Network.PreparedRequest<R>.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<Data>.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<R>.CachedResponse(
return Network.PreparedRequest<R>.CachedResponse(
info: responseInfo,
originalData: targetData,
convertedData: try preparedRequest.responseConverter(responseInfo, targetData)

@ -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)

@ -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<T: Encodable, Endpoint: EndpointType> {
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<T: Encodable, Endpoint: EndpointType> {
// 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

@ -2,7 +2,7 @@
import Foundation
public extension HTTP {
public extension Network {
struct RequestInfo: Codable {
let method: String
let endpoint: String

@ -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<E: EndpointType>: 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<E: EndpointType>(
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
),

@ -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]

@ -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<String>.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)
}

@ -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 {

@ -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))
}
}
}

@ -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))
}
}
}

@ -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))
}
}

@ -110,7 +110,7 @@ class BencodeSpec: QuickSpec {
expect {
let result: BencodeResponse<TestType> = 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<TestType> = 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<TestType3> = 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<TestType2> = 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<TestType> = 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<String> = 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<Int> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}.to(throwError(NetworkError.parsingFailed))
}
}
}

Loading…
Cancel
Save