diff --git a/Podfile b/Podfile index e2429bb66..e7bacf287 100644 --- a/Podfile +++ b/Podfile @@ -51,6 +51,12 @@ abstract_target 'GlobalDependencies' do pod 'Reachability' pod 'SAMKeychain' pod 'SwiftProtobuf', '~> 1.5.0' + + target 'SessionMessagingKitTests' do + inherit! :complete + + pod 'Nimble' + end end target 'SessionUtilitiesKit' do diff --git a/Podfile.lock b/Podfile.lock index fb59d482d..edb3b4fd7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -24,6 +24,7 @@ PODS: - Mantle (2.1.0): - Mantle/extobjc (= 2.1.0) - Mantle/extobjc (2.1.0) + - Nimble (9.2.1) - NVActivityIndicatorView (5.1.1): - NVActivityIndicatorView/Base (= 5.1.1) - NVActivityIndicatorView/Base (5.1.1) @@ -124,6 +125,7 @@ DEPENDENCIES: - CryptoSwift - Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`) - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) + - Nimble - NVActivityIndicatorView - PromiseKit - PureLayout (~> 3.1.8) @@ -141,6 +143,7 @@ SPEC REPOS: - AFNetworking - CocoaLumberjack - CryptoSwift + - Nimble - NVActivityIndicatorView - OpenSSL-Universal - PromiseKit @@ -190,6 +193,7 @@ SPEC CHECKSUMS: CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b + Nimble: e7e615c0335ee4bf5b0d786685451e62746117d5 NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2 PromiseKit: 3b2b6995e51a954c46dbc550ce3da44fbfb563c5 @@ -204,6 +208,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 19ce2820c263e8f3c114817f7ca2da73a9382b6a +PODFILE CHECKSUM: a4acbe047a767c48a709e93318532fbf345330dd COCOAPODS: 1.11.2 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c48c97224..b51ac4466 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -767,6 +767,7 @@ D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D24B5BD4169F568C00681372 /* AudioToolbox.framework */; }; D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; }; D48CEFD2222D323FEFEFC6CC /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8962372EEC51D3F56FE3A68A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */; }; + E197F4A653289312F13926E6 /* Pods_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */; }; EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */ = {isa = PBXBuildFile; fileRef = EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */; }; F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; @@ -826,7 +827,21 @@ FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */; }; FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */; }; FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; - FDC4387C27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */; }; + FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; }; + FDC4389A27BA002500C60D73 /* OpenGroupAPIV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPIV2Tests.swift */; }; + FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389C27BA01F000C60D73 /* TestStorage.swift */; }; + FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; + FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; + FDC438A827BB11CD00C60D73 /* UserPermissionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */; }; + FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; + FDC438AC27BB145200C60D73 /* UserDeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */; }; + FDC438AE27BB148700C60D73 /* UserDeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */; }; + FDC438B127BB159600C60D73 /* RequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B027BB159600C60D73 /* RequestInfo.swift */; }; + FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B227BB15B400C60D73 /* ResponseInfo.swift */; }; + FDC438B527BB15D400C60D73 /* Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B427BB15D400C60D73 /* Destination.swift */; }; + FDC438B727BB160000C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B627BB160000C60D73 /* Error.swift */; }; + FDC438B927BB161E00C60D73 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B827BB161E00C60D73 /* Version.swift */; }; + FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438BC27BB2AB400C60D73 /* Mockable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -949,6 +964,20 @@ remoteGlobalIDString = C3C2A678255388CC00C340D1; remoteInfo = SessionUtilitiesKit; }; + FDC4389327B9FFC700C60D73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = C3C2A6EF25539DE700C340D1; + remoteInfo = SessionMessagingKit; + }; + FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = D221A088169C9E5E00537ABF; + remoteInfo = Session; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -994,6 +1023,7 @@ /* Begin PBXFileReference section */ 038A3BABD5BA0CE41D8C17F5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0840117FDFD286D1CC14A2E1 /* Pods-SessionTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionTests/Pods-SessionTests.debug.xcconfig"; sourceTree = ""; }; 0D3D13FEE4FF6A2E2ED85322 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; 174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionProtocolKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionProtocolKit/Pods-SessionProtocolKit.app store release.xcconfig"; sourceTree = ""; }; 18D19142FD6E60FD0A5D89F7 /* Pods-LokiPushNotificationService.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LokiPushNotificationService.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-LokiPushNotificationService/Pods-LokiPushNotificationService.app store release.xcconfig"; sourceTree = ""; }; @@ -1201,6 +1231,7 @@ 9B3329176C10E9640865E65B /* Pods-GlobalDependencies-Session.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.debug.xcconfig"; sourceTree = ""; }; 9B533A9FA46206D3D99C9ADA /* Pods-SignalMessaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.debug.xcconfig"; sourceTree = ""; }; 9C0469AC557930C01552CC83 /* Pods-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUtilitiesKit/Pods-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; + A0FB43B511403A5FAFAC88B8 /* Pods-SessionMessagingKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKitTests.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests.app store release.xcconfig"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; A1C32D4D17A0652C000A904E /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; }; @@ -1835,8 +1866,10 @@ C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Shared.swift"; sourceTree = ""; }; C3F0A607255C98A6007BE2A3 /* Storage+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+SnodeAPI.swift"; sourceTree = ""; }; C5060C3B36A848B71CCE4685 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C8153B96A292A25045BE2C54 /* Pods-SessionTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionTests.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionTests/Pods-SessionTests.app store release.xcconfig"; sourceTree = ""; }; C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; C98441E849C3CA7FE8220D33 /* Pods-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionNotificationServiceExtension/Pods-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = ""; }; + CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionMessagingKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; D221A089169C9E5E00537ABF /* Session.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Session.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1849,6 +1882,7 @@ D221A0E7169DFFC500537ABF /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = ../../../../../../System/Library/Frameworks/AVFoundation.framework; sourceTree = ""; }; D24B5BD4169F568C00681372 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = ../../../../../../System/Library/Frameworks/AudioToolbox.framework; sourceTree = ""; }; D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + D2C155B76C8483CB9A6EA9B4 /* Pods_SessionTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE2DD605305BC6EFAD731723 /* Pods-Signal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.debug.xcconfig"; sourceTree = ""; }; DF728B4B438716EAF95CEC18 /* Pods-Signal.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.app store release.xcconfig"; sourceTree = ""; }; E19F30497676B0FA3553CCE6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1860,6 +1894,7 @@ EF764C331DB67CC5000D9A87 /* UIViewController+Permissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Permissions.h"; sourceTree = ""; }; EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Permissions.m"; sourceTree = ""; }; F121FB43E2A1C1CF7F2AFC23 /* Pods-SessionPushNotificationExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionPushNotificationExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionPushNotificationExtension/Pods-SessionPushNotificationExtension.debug.xcconfig"; sourceTree = ""; }; + F4DC483F404B65C59D9C2CF8 /* Pods-SessionMessagingKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests.debug.xcconfig"; sourceTree = ""; }; F62ECF7B8AF4F8089AA705B3 /* Pods-LokiPushNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LokiPushNotificationService.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LokiPushNotificationService/Pods-LokiPushNotificationService.debug.xcconfig"; sourceTree = ""; }; F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; @@ -1918,7 +1953,21 @@ FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Utilities.swift"; sourceTree = ""; }; FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = ""; }; FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; - FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyOnionRequestAPI.swift; sourceTree = ""; }; + FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionMessagingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FDC4389927BA002500C60D73 /* OpenGroupAPIV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIV2Tests.swift; sourceTree = ""; }; + FDC4389C27BA01F000C60D73 /* TestStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStorage.swift; sourceTree = ""; }; + FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = ""; }; + FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = ""; }; + FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissionsRequest.swift; sourceTree = ""; }; + FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = ""; }; + FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesRequest.swift; sourceTree = ""; }; + FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesResponse.swift; sourceTree = ""; }; + FDC438B027BB159600C60D73 /* RequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInfo.swift; sourceTree = ""; }; + FDC438B227BB15B400C60D73 /* ResponseInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = ""; }; + FDC438B427BB15D400C60D73 /* Destination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Destination.swift; sourceTree = ""; }; + FDC438B627BB160000C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + FDC438B827BB161E00C60D73 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; + FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2035,6 +2084,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FDC4388B27B9FFC700C60D73 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */, + E197F4A653289312F13926E6 /* Pods_SessionMessagingKitTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -2241,6 +2299,10 @@ 37A3185C08AE9AE72A9E0922 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */, 5B7FDA4BA2DDFF4612600FB8 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */, 7ABE4694B110C1BBCB0E46A2 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */, + F4DC483F404B65C59D9C2CF8 /* Pods-SessionMessagingKitTests.debug.xcconfig */, + A0FB43B511403A5FAFAC88B8 /* Pods-SessionMessagingKitTests.app store release.xcconfig */, + 0840117FDFD286D1CC14A2E1 /* Pods-SessionTests.debug.xcconfig */, + C8153B96A292A25045BE2C54 /* Pods-SessionTests.app store release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -3362,10 +3424,10 @@ isa = PBXGroup; children = ( C3C2A5B0255385C700C340D1 /* Meta */, + FDC438AF27BB158500C60D73 /* Models */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */, C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */, - FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */, C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */, C3C2A5B7255385EC00C340D1 /* Snode.swift */, C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */, @@ -3628,6 +3690,7 @@ C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, + FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, D221A08C169C9E5E00537ABF /* Frameworks */, D221A08A169C9E5E00537ABF /* Products */, 9404664EC513585B05DF1350 /* Pods */, @@ -3645,6 +3708,7 @@ C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */, C331FF1B2558F9D300070591 /* SessionUIKit.framework */, C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */, + FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -3697,6 +3761,8 @@ 038A3BABD5BA0CE41D8C17F5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */, C5060C3B36A848B71CCE4685 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */, 8962372EEC51D3F56FE3A68A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */, + CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */, + D2C155B76C8483CB9A6EA9B4 /* Pods_SessionTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -3764,6 +3830,12 @@ FDC4386027B4CDDF00C60D73 /* FileResponse.swift */, FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, FDC4386227B4D94E00C60D73 /* OGMessage.swift */, + FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, + FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, + FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */, + FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */, + FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */, FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */, FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */, FDC4384627B47F4D00C60D73 /* Deletion.swift */, @@ -3819,6 +3891,44 @@ path = Models; sourceTree = ""; }; + FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */ = { + isa = PBXGroup; + children = ( + FDC4389B27BA01E300C60D73 /* _TestUtilities */, + FDC4389827BA001800C60D73 /* Open Groups */, + ); + path = SessionMessagingKitTests; + sourceTree = ""; + }; + FDC4389827BA001800C60D73 /* Open Groups */ = { + isa = PBXGroup; + children = ( + FDC4389927BA002500C60D73 /* OpenGroupAPIV2Tests.swift */, + ); + path = "Open Groups"; + sourceTree = ""; + }; + FDC4389B27BA01E300C60D73 /* _TestUtilities */ = { + isa = PBXGroup; + children = ( + FDC438BC27BB2AB400C60D73 /* Mockable.swift */, + FDC4389C27BA01F000C60D73 /* TestStorage.swift */, + ); + path = _TestUtilities; + sourceTree = ""; + }; + FDC438AF27BB158500C60D73 /* Models */ = { + isa = PBXGroup; + children = ( + FDC438B827BB161E00C60D73 /* Version.swift */, + FDC438B627BB160000C60D73 /* Error.swift */, + FDC438B427BB15D400C60D73 /* Destination.swift */, + FDC438B027BB159600C60D73 /* RequestInfo.swift */, + FDC438B227BB15B400C60D73 /* ResponseInfo.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -4153,6 +4263,27 @@ productReference = D221A089169C9E5E00537ABF /* Session.app */; productType = "com.apple.product-type.application"; }; + FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FDC4389527B9FFC700C60D73 /* Build configuration list for PBXNativeTarget "SessionMessagingKitTests" */; + buildPhases = ( + A067C0B8A52FC6C6FDA49939 /* [CP] Check Pods Manifest.lock */, + FDC4388A27B9FFC700C60D73 /* Sources */, + FDC4388B27B9FFC700C60D73 /* Frameworks */, + FDC4388C27B9FFC700C60D73 /* Resources */, + 7D43E8AB603234C5ADEF2812 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + FDC4389427B9FFC700C60D73 /* PBXTargetDependency */, + FDC438BB27BB276F00C60D73 /* PBXTargetDependency */, + ); + name = SessionMessagingKitTests; + productName = SessionMessagingKitTests; + productReference = FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -4160,7 +4291,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastSwiftUpdateCheck = 1130; + LastSwiftUpdateCheck = 1320; LastTestingUpgradeCheck = 0600; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Rangeproof Pty Ltd"; @@ -4250,6 +4381,9 @@ }; }; }; + FDC4388D27B9FFC700C60D73 = { + CreatedOnToolsVersion = 13.2.1; + }; }; }; buildConfigurationList = D221A083169C9E5E00537ABF /* Build configuration list for PBXProject "Session" */; @@ -4294,6 +4428,7 @@ C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, C3C2A59E255385C100C340D1 /* SessionSnodeKit */, C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, + FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, ); }; /* End PBXProject section */ @@ -4418,6 +4553,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FDC4388C27B9FFC700C60D73 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -4518,6 +4660,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 7D43E8AB603234C5ADEF2812 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 7E2D14F857C70F98DED3B8E9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -4562,6 +4721,28 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + A067C0B8A52FC6C6FDA49939 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SessionMessagingKitTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; B19B891E99B1507CAC8AAD19 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -4798,18 +4979,22 @@ buildActionMask = 2147483647; files = ( C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */, - FDC4387C27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift in Sources */, C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */, + FDC438B127BB159600C60D73 /* RequestInfo.swift in Sources */, + FDC438B927BB161E00C60D73 /* Version.swift in Sources */, C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */, C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */, C32C5CBF256DD282003C73A2 /* Storage+SnodeAPI.swift in Sources */, C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */, + FDC438B727BB160000C60D73 /* Error.swift in Sources */, + FDC438B527BB15D400C60D73 /* Destination.swift in Sources */, C32C5CBE256DD282003C73A2 /* Storage+OnionRequests.swift in Sources */, C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */, C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */, C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, + FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */, C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */, C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */, @@ -4913,7 +5098,9 @@ FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */, C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, + FDC438AC27BB145200C60D73 /* UserDeleteMessagesRequest.swift in Sources */, 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */, + FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */, C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */, C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */, @@ -4930,6 +5117,7 @@ C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */, C3DB66AC260ACA42001EFC55 /* OpenGroupManagerV2.swift in Sources */, B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */, + FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, B8856D34256F1192001CE70E /* Environment.m in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, @@ -5024,6 +5212,7 @@ C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, C3DB66C3260ACCE6001EFC55 /* OpenGroupPollerV2.swift in Sources */, + FDC438AE27BB148700C60D73 /* UserDeleteMessagesResponse.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, C32C5FBB256E0206003C73A2 /* OWSBackgroundTask.m in Sources */, B8856CA8256F0F42001CE70E /* OWSBackupFragment.m in Sources */, @@ -5039,6 +5228,7 @@ C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */, C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */, C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */, + FDC438A827BB11CD00C60D73 /* UserPermissionsRequest.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */, C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */, @@ -5054,6 +5244,7 @@ FDC4385727B484B700C60D73 /* LegacyFileUploadResponse.swift in Sources */, FDC4385B27B485DE00C60D73 /* LegacyFileDownloadResponse.swift in Sources */, C32C5C01256DC9A0003C73A2 /* OWSIdentityManager.m in Sources */, + FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */, FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */, C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */, @@ -5236,6 +5427,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FDC4388A27B9FFC700C60D73 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FDC4389A27BA002500C60D73 /* OpenGroupAPIV2Tests.swift in Sources */, + FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, + FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -5325,6 +5526,17 @@ target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; targetProxy = FDC4386E27B4E90300C60D73 /* PBXContainerItemProxy */; }; + FDC4389427B9FFC700C60D73 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */; + targetProxy = FDC4389327B9FFC700C60D73 /* PBXContainerItemProxy */; + }; + FDC438BB27BB276F00C60D73 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D221A088169C9E5E00537ABF /* Session */; + targetProxy = FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -6664,6 +6876,112 @@ }; name = "App Store Release"; }; + FDC4389627B9FFC700C60D73 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F4DC483F404B65C59D9C2CF8 /* Pods-SessionMessagingKitTests.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionMessagingKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FDC4389727B9FFC700C60D73 /* App Store Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A0FB43B511403A5FAFAC88B8 /* Pods-SessionMessagingKitTests.app store release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionMessagingKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "App Store Release"; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -6748,6 +7066,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "App Store Release"; }; + FDC4389527B9FFC700C60D73 /* Build configuration list for PBXNativeTarget "SessionMessagingKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FDC4389627B9FFC700C60D73 /* Debug */, + FDC4389727B9FFC700C60D73 /* App Store Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "App Store Release"; + }; /* End XCConfigurationList section */ }; rootObject = D221A080169C9E5E00537ABF /* Project object */; diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index 3426f0ce1..2383ad12e 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -20,20 +20,6 @@ ReferencedContainer = "container:Session.xcodeproj"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme index 6f27e32c8..d32a32659 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme @@ -43,6 +43,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme index 7ab99da50..da97b2ea0 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme @@ -52,6 +52,16 @@ + + + + diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index a1be037f0..13b0ec5ab 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -700,7 +700,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in let publicKey = message.authorId guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) else { return } - OpenGroupAPIV2.ban(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete() + OpenGroupAPIV2.legacyBan(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete() })) alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) present(alert, animated: true, completion: nil) @@ -714,7 +714,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in let publicKey = message.authorId guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) else { return } - OpenGroupAPIV2.banAndDeleteAllMessages(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete() + OpenGroupAPIV2.legacyBanAndDeleteAllMessages(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete() })) alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) present(alert, animated: true, completion: nil) diff --git a/SessionMessagingKit/File Server/FileServerAPIV2.swift b/SessionMessagingKit/File Server/FileServerAPIV2.swift index 7829d9b74..4e18259a7 100644 --- a/SessionMessagingKit/File Server/FileServerAPIV2.swift +++ b/SessionMessagingKit/File Server/FileServerAPIV2.swift @@ -94,8 +94,8 @@ public final class FileServerAPIV2 : NSObject { preconditionFailure("It's currently not allowed to send non onion routed requests.") } - // TODO: Upgrade this to use the V4 onion requests once supported - return LegacyOnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: serverPublicKey) + // TODO: Upgrade this to use the V4 onion requests once supported. + return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: .v3, with: serverPublicKey) .map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) } } diff --git a/SessionMessagingKit/Jobs/NotifyPNServerJob.swift b/SessionMessagingKit/Jobs/NotifyPNServerJob.swift index 227c66202..f13fb8d5f 100644 --- a/SessionMessagingKit/Jobs/NotifyPNServerJob.swift +++ b/SessionMessagingKit/Jobs/NotifyPNServerJob.swift @@ -66,7 +66,8 @@ public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSC request.httpBody = body let promise = attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.global()) { - OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: PushNotificationAPI.serverPublicKey).map { _ in } + OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: PushNotificationAPI.serverPublicKey) + .map { _ in } } let _ = promise.done(on: DispatchQueue.global()) { // Intentionally capture self self.handleSuccess() diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift index 05101987c..e75ceb191 100644 --- a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift @@ -20,7 +20,7 @@ extension OpenGroupAPIV2 { self.path = request.urlPathAndParamsString self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders()) - // TODO: Differentiate between JSON and b64 body + // TODO: Differentiate between JSON and b64 body. if let body: Data = request.body, let bodyString: String = String(data: body, encoding: .utf8) { self.json = bodyString } @@ -56,7 +56,7 @@ extension OpenGroupAPIV2 { typealias BatchRequest = [BatchSubRequest] typealias BatchResponseTypes = [Codable.Type] - typealias BatchResponse = [Codable] + typealias BatchResponse = [(OnionRequestResponseInfoType, Codable)] } // MARK: - Convenience @@ -67,7 +67,7 @@ public extension Decodable { } } -extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) { +extension Promise where T == (OnionRequestResponseInfoType, Data?) { func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise { self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPIV2.BatchResponse in // Need to split the data into an array of data so each item can be Decoded correctly @@ -83,6 +83,7 @@ extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) { do { return try zip(dataArray, types) .map { data, type in try type.decoded(from: data) } + .map { data in (responseInfo, data) } } catch let thrownError { throw (error ?? thrownError) diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift index ab1cd3dab..b87873016 100644 --- a/SessionMessagingKit/Open Groups/Models/Capabilities.swift +++ b/SessionMessagingKit/Open Groups/Models/Capabilities.swift @@ -4,8 +4,8 @@ import Foundation extension OpenGroupAPIV2 { public struct Capabilities: Codable { - enum Capability: CaseIterable, Codable { - static var allCases: [Capability] { + public enum Capability: CaseIterable, Codable { + public static var allCases: [Capability] { [.pysogs] } @@ -16,7 +16,7 @@ extension OpenGroupAPIV2 { // MARK: - Convenience - var rawValue: String { + public var rawValue: String { switch self { case .unsupported(let originalValue): return originalValue default: return "\(self)" @@ -25,7 +25,7 @@ extension OpenGroupAPIV2 { // MARK: - Codable - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container: SingleValueDecodingContainer = try decoder.singleValueContainer() let valueString: String = try container.decode(String.self) let maybeValue: Capability? = Capability.allCases.first { $0.rawValue == valueString } @@ -34,7 +34,7 @@ extension OpenGroupAPIV2 { } } - let capabilities: [Capability] - let missing: [Capability]? + public let capabilities: [Capability] + public let missing: [Capability]? } } diff --git a/SessionMessagingKit/Open Groups/Models/UserBanRequest.swift b/SessionMessagingKit/Open Groups/Models/UserBanRequest.swift new file mode 100644 index 000000000..48144df8d --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserBanRequest.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct UserBanRequest: Codable { + let rooms: [String]? + let global: Bool? + let timeout: TimeInterval? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesRequest.swift b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesRequest.swift new file mode 100644 index 000000000..c4ac800c8 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesRequest.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct UserDeleteMessagesRequest: Codable { + let rooms: [String]? + let global: Bool? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift new file mode 100644 index 000000000..68406a13a --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct UserDeleteMessagesResponse: Codable { + enum CodingKeys: String, CodingKey { + case id + case messagesDeleted = "messages_deleted" + } + + let id: String + let messagesDeleted: Int64 + } +} diff --git a/SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift b/SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift new file mode 100644 index 000000000..468e9e950 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift @@ -0,0 +1,69 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct UserModeratorRequest: Codable { + /// List of room tokens to which the moderator status should be applied. The invoking user must be an admin of all of the given rooms. + /// + /// This may be set to the single-element list ['*'] to add or remove the moderator from all rooms in which the current user has admin + /// permissions (the call will succeed if the calling user is an admin in at least one channel). + /// + /// Exclusive of `global`. (If you want to apply both at once use two calls, e.g. bundled in a batch request). + let rooms: [String]? + + /// If true then appoint this user as a global moderator or admin of the server. The user will receive moderator/admin ability in all rooms + /// on the server (both current and future). + /// + /// The caller must be a global admin to add/remove a global moderator or admin. + let global: Bool? + + /// If `true` then this user will be granted moderator permission to either the listed room(s) or the server globally. + /// + /// If `false` then this user will have their moderator *and admin* permissions removed from the given rooms (or server). Note + /// that removing a global moderator only removes the global permission but does not remove individual room moderator permissions + /// that may also be present. + /// + /// See the `admin` parameter description for information on how `admin` and `moderator` parameters interact. + let moderator: Bool + + /// If `true` then this user will be granted moderator and admin permissions to the given rooms or server. Admin permissions are + /// required to appoint new moderators or administrators and to alter room info such as the image, adding/removing pinned messages, + /// and changing the name/description of the room. + /// + /// If false then this user will have their admin permission removed, but will keep moderator permissions. To remove both moderator and + /// admin permissions specify `moderator: false` (which implies clearing admin permissions as well). + /// + /// Note that removing a global admin only removes the global permission but does not remove individual room admin permissions that + /// may also be present. + /// + /// The `admin`/`moderator` paramters interact as follows: + /// - `admin=true`, `moderator` omitted: this adds admin permissions, which automatically also implies moderator permissions. + /// - `admin=true, moderator=true`: exactly the same as above. + /// - `admin=false, moderator=true`: removes any existing admin permissions from the rooms (or globally), if present, and adds + /// moderator permissions to the rooms/globally (if not already present). + /// - `admin=false`, `moderator` omitted: this removes admin permissions but leaves moderator permissions, if present. (This + /// effectively "downgrades" an admin to a moderator). Unlike the above this does *not* add moderator permissions to matching rooms + /// if not already present. + /// - `moderator=true`, `admin` omitted: adds moderator permissions to the given rooms (or globally), if not already present. If + /// the user already has admin permissions this does nothing (that is, admin permission is *not* removed, unlike the above). + /// - `moderator=false`, `admin` omitted: this removes moderator *and* admin permissions from all given rooms (or globally). + /// - `moderator=false, admin=false`: exactly the same as above. + /// - `moderator=false, admin=true`: this combination is *not* *permitted* (because admin permissions imply moderator + /// permissions) and will result in Bad Request error if given. + let admin: Bool + + /// Whether this user should be a "visible" moderator or admin in the specified rooms (or globally). Visible moderators are identified to all + /// room users (e.g. via a special status badge in Session clients). + /// + /// Invisible moderators/admins have the same permission as as visible ones, but their moderator/admin status is only visible to other + /// moderators, not to ordinary room participants. + /// + /// The default if this field is omitted is true for room-specific moderators/admins and false for server-level global moderators/admins. + /// + /// If an admin or moderator has both global and room-specific moderation permissions then the visibility of the admin/mod for that + /// room's moderator/admin list will use the room-specific visibility value, regardless of the global setting. (This differs from + /// moderator/admin permissions themselves, which are additive). + let visible: Bool + } +} diff --git a/SessionMessagingKit/Open Groups/Models/UserPermissionsRequest.swift b/SessionMessagingKit/Open Groups/Models/UserPermissionsRequest.swift new file mode 100644 index 000000000..66bbe19a7 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserPermissionsRequest.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct UserPermissionsRequest: Codable { + let rooms: [String] + let timeout: TimeInterval + let read: Bool + let write: Bool + let upload: Bool + } +} diff --git a/SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift b/SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift new file mode 100644 index 000000000..5e77485ee --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct UserUnbanRequest: Codable { + let rooms: [String]? + let global: Bool? + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index a4cf3231b..8d9cd0faf 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -29,7 +29,14 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Batching & Polling - public static func poll(_ server: String) -> Promise { + /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open Group + public static func poll( + _ server: String, + through api: OnionRequestAPIType.Type = OnionRequestAPI.self, + using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> { // TODO: Remove comments // Capabilities // Fetch each room @@ -37,7 +44,7 @@ public final class OpenGroupAPIV2: NSObject { // /room//pollInfo/ instead? // Fetch messages for each room // /room/{roomToken}/messages/since/{messageSequence}: - // Fetch deletions for each room (included in messages) + // Fetch deletions for each room (included in messages) // old compact_poll data // public let room: String @@ -46,7 +53,6 @@ public final class OpenGroupAPIV2: NSObject { // public let deletions: [Deletion]? // public let moderators: [String]? - let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage let requestResponseType: [BatchRequestInfo] = [ BatchRequestInfo( request: Request( @@ -59,7 +65,7 @@ public final class OpenGroupAPIV2: NSObject { ] .appending( storage.getAllV2OpenGroups().values - .filter { $0.server == server } + .filter { $0.server == server.lowercased() } // Note: The `OpenGroupV2` converts the server value to lowercase during init .flatMap { openGroup -> [BatchRequestInfo] in let lastSeqNo: Int64? = storage.getLastMessageServerID(for: openGroup.room, on: server) let targetSeqNo: Int64 = (lastSeqNo ?? 0) @@ -88,11 +94,22 @@ public final class OpenGroupAPIV2: NSObject { ) // TODO: Handle response (maybe in the poller or the OpenGroupManagerV2?) - return batch(server, requests: requestResponseType) - .map { _ in () } + return batch(server, requests: requestResponseType, through: api, using: storage, nonceGenerator: nonceGenerator, date: date) } - private static func batch(_ server: String, requests: [BatchRequestInfo]) -> Promise { + /// This is used, for example, to poll multiple rooms on the same server for updates in a single query rather than needing to make multiple requests for each room. + /// + /// No guarantee is made as to the order in which sub-requests are processed; use the `/sequence` instead if you need that. + /// + /// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided with the request body. + private static func batch( + _ server: String, + requests: [BatchRequestInfo], + through api: OnionRequestAPIType.Type = OnionRequestAPI.self, + using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> { let requestBody: BatchRequest = requests.map { BatchSubRequest(request: $0.request) } let responseTypes = requests.map { $0.responseType } @@ -107,14 +124,19 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, through: api, using: storage, nonceGenerator: nonceGenerator, date: date) .decoded(as: responseTypes, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) .map { result in - return "" + result.enumerated() + .reduce(into: [:]) { prev, next in + prev[requests[next.offset].request.endpoint] = next.element + } } } - public static func compactPoll(_ server: String) -> Promise { + // TODO: `/sequence` request + + public static func compactPoll(_ server: String, api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> Promise { let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage let rooms: [String] = storage.getAllV2OpenGroups().values .filter { $0.server == server } @@ -155,7 +177,7 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, through: api) .then(on: OpenGroupAPIV2.workQueue) { _, maybeData -> Promise in guard let data: Data = maybeData else { throw Error.parsingFailed } @@ -173,101 +195,39 @@ public final class OpenGroupAPIV2: NSObject { } } - // MARK: - Authentication - - // TODO: Turn 'Sodium' and 'NonceGenerator16Byte' into protocols for unit testing. - static func sign( - _ request: URLRequest, - with publicKey: String, - sodium: Sodium = Sodium(), - nonceGenerator: NonceGenerator16Byte = NonceGenerator16Byte() - ) -> URLRequest? { - guard let url: URL = request.url else { return nil } - - var updatedRequest: URLRequest = request - let path: String = url.path - .appending(url.query.map { value in "?\(value)" }) - let method: String = (request.httpMethod ?? "GET") - let timestamp: Int = Int(floor(Date().timeIntervalSince1970)) - let nonce: Data = Data(nonceGenerator.nonce()) - - guard let publicKeyData: Data = publicKey.dataFromHex() else { return nil } - guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { - return nil - } -// guard let blindedKeyPair: ECKeyPair = try? userKeyPair.convert(to: .blinded, with: publicKey) else { -// return nil -// } - // TODO: Change this back once you figure out why it's busted - let blindedKeyPair: ECKeyPair = userKeyPair - - // Generate the sharedSecret by "aB || A || B" where - // a, A are the users private and public keys respectively, - // B is the SOGS public key - let maybeSharedSecret: Data? = sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)? - .appending(blindedKeyPair.publicKey) - .appending(publicKeyData.bytes) - - // Generate the hash to be sent along with the request - // intermediateHash = Blake2B(sharedSecret, size=42, salt=noncebytes, person='sogs.shared_keys') - // secretHash = Blake2B( - // Method || Path || Timestamp || Body, - // size=42, - // key=r, - // salt=noncebytes, - // person='sogs.auth_header' - // ) - let secretHashMessage: Bytes = method.bytes - .appending(path.bytes) - .appending("\(timestamp)".bytes) - .appending(request.httpBody?.bytes ?? []) // TODO: Might need to do the 'httpBodyStream' as well??? - - guard let sharedSecret: Data = maybeSharedSecret else { return nil } - guard let intermediateHash: Bytes = sodium.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else { - return nil - } - guard let secretHash: Bytes = sodium.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else { - return nil - } - - updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:]) - .updated(with: [ - Header.sogsPubKey.rawValue: blindedKeyPair.hexEncodedPublicKey, - Header.sogsTimestamp.rawValue: "\(timestamp)", - Header.sogsNonce.rawValue: nonce.base64EncodedString(), - Header.sogsHash.rawValue: secretHash.toBase64() - ]) - - return updatedRequest - } - // MARK: - Capabilities - public static func capabilities(on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Capabilities)> { + public static func capabilities(on server: String) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { let request: Request = Request( server: server, endpoint: .capabilities, - queryParameters: [:] // TODO: Add any requirements '.required' + queryParameters: [:] // TODO: Add any requirements '.required'. ) - // TODO: Handle a `412` response (ie. a required capability isn't supported) + // TODO: Handle a `412` response (ie. a required capability isn't supported). return send(request) .decoded(as: Capabilities.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } // MARK: - Room - public static func rooms(for server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Room])> { + public static func rooms( + for server: String, + through api: OnionRequestAPIType.Type = OnionRequestAPI.self, + using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) -> Promise<(OnionRequestResponseInfoType, [Room])> { let request: Request = Request( server: server, endpoint: .rooms ) - return send(request) + return send(request, through: api, using: storage, nonceGenerator: nonceGenerator, date: date) .decoded(as: [Room].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func room(for roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Room)> { + public static func room(for roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, Room)> { let request: Request = Request( server: server, endpoint: .room(roomToken) @@ -277,7 +237,7 @@ public final class OpenGroupAPIV2: NSObject { .decoded(as: Room.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, RoomPollInfo)> { + public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> { let request: Request = Request( server: server, endpoint: .roomPollInfo(roomToken, lastUpdated) @@ -296,8 +256,8 @@ public final class OpenGroupAPIV2: NSObject { whisperTo: String?, whisperMods: Bool, with serverPublicKey: String - ) -> Promise<(OnionRequestAPI.ResponseInfo, Message)> { - // TODO: Change this to use '.blinded' once it's working + ) -> Promise<(OnionRequestResponseInfoType, Message)> { + // TODO: Change this to use '.blinded' once it's working. guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else { return Promise(error: Error.signingFailed) } @@ -325,7 +285,7 @@ public final class OpenGroupAPIV2: NSObject { .decoded(as: Message.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func recentMessages(in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> { + public static func recentMessages(in roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, [Message])> { // TODO: Recent vs. Since? let request: Request = Request( server: server, @@ -338,13 +298,13 @@ public final class OpenGroupAPIV2: NSObject { return send(request) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in + .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in process(messages: messages, for: roomToken, on: server) .map { processedMessages in (responseInfo, processedMessages) } } } - public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> { + public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, [Message])> { // TODO: Recent vs. Since? let request: Request = Request( server: server, @@ -357,13 +317,13 @@ public final class OpenGroupAPIV2: NSObject { return send(request) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in + .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in process(messages: messages, for: roomToken, on: server) .map { processedMessages in (responseInfo, processedMessages) } } } - public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> { + public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, [Message])> { // TODO: Recent vs. Since? let request: Request = Request( server: server, @@ -376,7 +336,7 @@ public final class OpenGroupAPIV2: NSObject { return send(request) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in + .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in process(messages: messages, for: roomToken, on: server) .map { processedMessages in (responseInfo, processedMessages) } } @@ -384,7 +344,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Pinning - public static func pinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { + public static func pinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { let request: Request = Request( method: .post, server: server, @@ -395,7 +355,7 @@ public final class OpenGroupAPIV2: NSObject { .map { responseInfo, _ in responseInfo } } - public static func unpinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { + public static func unpinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { let request: Request = Request( method: .post, server: server, @@ -406,7 +366,7 @@ public final class OpenGroupAPIV2: NSObject { .map { responseInfo, _ in responseInfo } } - public static func unpinAll(in roomToken: String, on server: String) -> Promise { + public static func unpinAll(in roomToken: String, on server: String) -> Promise { let request: Request = Request( method: .post, server: server, @@ -458,7 +418,7 @@ public final class OpenGroupAPIV2: NSObject { return promise } - public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileUploadResponse)> { + public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { let request: Request = Request( method: .post, server: server, @@ -473,7 +433,7 @@ public final class OpenGroupAPIV2: NSObject { /// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach /// whenever possible - public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileUploadResponse)> { + public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { let request: Request = Request( method: .post, server: server, @@ -486,7 +446,7 @@ public final class OpenGroupAPIV2: NSObject { .decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data)> { + public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, Data)> { let request: Request = Request( server: server, endpoint: .roomFileIndividual(roomToken, fileId) @@ -500,7 +460,7 @@ public final class OpenGroupAPIV2: NSObject { } } - public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileDownloadResponse)> { + public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> { let request: Request = Request( server: server, endpoint: .roomFileIndividualJson(roomToken, fileId) @@ -510,6 +470,116 @@ public final class OpenGroupAPIV2: NSObject { .decoded(as: FileDownloadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } + // MARK: - Users + + public static func userBan(_ sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let requestBody: UserBanRequest = UserBanRequest( + rooms: roomTokens, + global: (roomTokens == nil ? true : nil), + timeout: timeout + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .userBan(sessionId), + body: body + ) + + return send(request) + } + + public static func userUnban(_ sessionId: String, from roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let requestBody: UserUnbanRequest = UserUnbanRequest( + rooms: roomTokens, + global: (roomTokens == nil ? true : nil) + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .userUnban(sessionId), + body: body + ) + + return send(request) + } + + public static func userPermissionUpdate(_ sessionId: String, read: Bool, write: Bool, upload: Bool, for roomTokens: [String], timeout: TimeInterval, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let requestBody: UserPermissionsRequest = UserPermissionsRequest( + rooms: roomTokens, + timeout: timeout, + read: read, + write: write, + upload: upload + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .userPermission(sessionId), + body: body + ) + + return send(request) + } + + public static func userModeratorUpdate(_ sessionId: String, moderator: Bool, admin: Bool, visible: Bool, for roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let requestBody: UserModeratorRequest = UserModeratorRequest( + rooms: roomTokens, + global: (roomTokens == nil ? true : nil), + moderator: moderator, + admin: admin, + visible: visible + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .userModerator(sessionId), + body: body + ) + + return send(request) + } + + public static func userDeleteMessages(_ sessionId: String, for roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> { + let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest( + rooms: roomTokens, + global: (roomTokens == nil ? true : nil) + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .userDeleteMessages(sessionId), + body: body + ) + + return send(request) + .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + // MARK: - Processing // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) @@ -599,9 +669,85 @@ public final class OpenGroupAPIV2: NSObject { ) } + // MARK: - Authentication + + // TODO: Turn 'Sodium' into a protocol for unit testing + static func sign( + _ request: URLRequest, + with publicKey: String, + using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + sodium: Sodium = Sodium(), + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) -> URLRequest? { + guard let url: URL = request.url else { return nil } + + var updatedRequest: URLRequest = request + let path: String = url.path + .appending(url.query.map { value in "?\(value)" }) + let method: String = (request.httpMethod ?? "GET") + let timestamp: Int = Int(floor(date.timeIntervalSince1970)) + let nonce: Data = Data(nonceGenerator.nonce()) + + guard let publicKeyData: Data = publicKey.dataFromHex() else { return nil } + guard let userKeyPair: ECKeyPair = storage.getUserKeyPair() else { + return nil + } +// guard let blindedKeyPair: ECKeyPair = try? userKeyPair.convert(to: .blinded, with: publicKey) else { +// return nil +// } + // TODO: Change this back once you figure out why it's busted + let blindedKeyPair: ECKeyPair = userKeyPair + + /// Generate the sharedSecret by "aB || A || B" where + /// a, A are the users private and public keys respectively, + /// B is the SOGS public key + let maybeSharedSecret: Data? = sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)? + .appending(blindedKeyPair.publicKey) + .appending(publicKeyData.bytes) + + /// Generate the hash to be sent along with the request + /// intermediateHash = Blake2B(sharedSecret, size=42, salt=noncebytes, person='sogs.shared_keys') + /// secretHash = Blake2B( + /// Method || Path || Timestamp || Body, + /// size=42, + /// key=r, + /// salt=noncebytes, + /// person='sogs.auth_header' + /// ) + let secretHashMessage: Bytes = method.bytes + .appending(path.bytes) + .appending("\(timestamp)".bytes) + .appending(request.httpBody?.bytes ?? []) // TODO: Might need to do the 'httpBodyStream' as well??? + + guard let sharedSecret: Data = maybeSharedSecret else { return nil } + guard let intermediateHash: Bytes = sodium.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else { + return nil + } + guard let secretHash: Bytes = sodium.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else { + return nil + } + + updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:]) + .updated(with: [ + Header.sogsPubKey.rawValue: blindedKeyPair.hexEncodedPublicKey, + Header.sogsTimestamp.rawValue: "\(timestamp)", + Header.sogsNonce.rawValue: nonce.base64EncodedString(), + Header.sogsHash.rawValue: secretHash.toBase64() + ]) + + return updatedRequest + } + // MARK: - Convenience - private static func send(_ request: Request, through api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> { + private static func send( + _ request: Request, + through api: OnionRequestAPIType.Type = OnionRequestAPI.self, + using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) -> Promise<(OnionRequestResponseInfoType, Data?)> { guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } var urlRequest: URLRequest = URLRequest(url: url) @@ -612,21 +758,21 @@ public final class OpenGroupAPIV2: NSObject { urlRequest.httpBody = request.body if request.useOnionRouting { - guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { + guard let publicKey = storage.getOpenGroupPublicKey(for: request.server) else { return Promise(error: Error.noPublicKey) } if request.isAuthRequired { // Attempt to sign the request with the new auth - guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else { + guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey, using: storage, nonceGenerator: nonceGenerator, date: date) else { return Promise(error: Error.signingFailed) } // TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`). - return OnionRequestAPI.sendOnionRequest(signedRequest, to: request.server, using: publicKey) + return api.sendOnionRequest(signedRequest, to: request.server, with: publicKey) } - return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + return api.sendOnionRequest(urlRequest, to: request.server, with: publicKey) } preconditionFailure("It's currently not allowed to send non onion routed requests.") @@ -641,6 +787,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: -- Legacy Auth + @available(*, deprecated, message: "Use request signing instead") private static func legacyGetAuthToken(for room: String, on server: String) -> Promise { let storage = SNMessagingKitConfiguration.shared.storage @@ -676,6 +823,7 @@ public final class OpenGroupAPIV2: NSObject { return promise } + @available(*, deprecated, message: "Use request signing instead") public static func legacyRequestNewAuthToken(for room: String, on server: String) -> Promise { SNLog("Requesting auth token for server: \(server).") guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { @@ -705,6 +853,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use request signing instead") public static func legacyClaimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise { let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) @@ -729,6 +878,7 @@ public final class OpenGroupAPIV2: NSObject { } /// Should be called when leaving a group. + @available(*, deprecated, message: "Use request signing instead") public static func legacyDeleteAuthToken(for room: String, on server: String) -> Promise { let request: Request = Request( method: .delete, @@ -748,6 +898,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: -- Legacy Requests + @available(*, deprecated, message: "Use poll or batch instead") public static func legacyCompactPoll(_ server: String) -> Promise { let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage let rooms: [String] = storage.getAllV2OpenGroups().values @@ -841,6 +992,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use getDefaultRoomsIfNeeded instead") public static func legacyGetDefaultRoomsIfNeeded() { Storage.shared.write( with: { transaction in @@ -861,6 +1013,7 @@ public final class OpenGroupAPIV2: NSObject { ) } + @available(*, deprecated, message: "Use rooms(for:) instead") public static func legacyGetAllRooms(from server: String) -> Promise<[LegacyRoomInfo]> { let request: Request = Request( server: server, @@ -877,6 +1030,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use room(for:on:) instead") public static func legacyGetRoomInfo(for room: String, on server: String) -> Promise { let request: Request = Request( server: server, @@ -894,6 +1048,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use roomImage(_:for:on:) instead") public static func legacyGetGroupImage(for room: String, on server: String) -> Promise { // Normally the image for a given group is stored with the group thread, so it's only // fetched once. However, on the join open group screen we show images for groups the @@ -942,6 +1097,7 @@ public final class OpenGroupAPIV2: NSObject { return promise } + @available(*, deprecated, message: "Use room(for:on:) instead") public static func legacyGetMemberCount(for room: String, on server: String) -> Promise { let request: Request = Request( server: server, @@ -965,6 +1121,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Legacy File Storage + @available(*, deprecated, message: "Use uploadFile(_:fileName:to:on:) instead") public static func legacyUpload(_ file: Data, to room: String, on server: String) -> Promise { let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString()) @@ -982,6 +1139,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use downloadFile(_:from:on:) instead") public static func legacyDownload(_ file: UInt64, from room: String, on server: String) -> Promise { let request = Request(server: server, room: room, endpoint: .legacyFile(file)) @@ -995,6 +1153,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Legacy Message Sending & Receiving + @available(*, deprecated, message: "Use send(_:to:on:whisperTo:whisperMods:with:) instead") public static func legacySend(_ message: OpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) } guard let body: Data = try? JSONEncoder().encode(signedMessage) else { @@ -1012,6 +1171,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use recentMessages(in:on:) or messagesSince(seqNo:in:on:) instead") public static func legacyGetMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { let storage = SNMessagingKitConfiguration.shared.storage let request: Request = Request( @@ -1033,6 +1193,8 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Legacy Message Deletion + // TODO: No delete method???? + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyDeleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise { let request: Request = Request( method: .delete, @@ -1044,6 +1206,7 @@ public final class OpenGroupAPIV2: NSObject { return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyGetDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> { let storage = SNMessagingKitConfiguration.shared.storage @@ -1066,6 +1229,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Legacy Moderation + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyGetModerators(for room: String, on server: String) -> Promise<[String]> { let request: Request = Request( server: server, @@ -1090,6 +1254,7 @@ public final class OpenGroupAPIV2: NSObject { } } + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyBan(_ publicKey: String, from room: String, on server: String) -> Promise { let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) @@ -1108,6 +1273,7 @@ public final class OpenGroupAPIV2: NSObject { return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyBanAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise { let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) @@ -1126,6 +1292,7 @@ public final class OpenGroupAPIV2: NSObject { return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } + @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyUnban(_ publicKey: String, from room: String, on server: String) -> Promise { let request: Request = Request( method: .delete, @@ -1140,6 +1307,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Processing // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) + @available(*, deprecated, message: "Use v4 endpoint instead") private static func legacyProcess(messages: [OpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { guard let messages: [OpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } @@ -1165,6 +1333,7 @@ public final class OpenGroupAPIV2: NSObject { return Promise.value(messages) } + @available(*, deprecated, message: "Use v4 endpoint instead") private static func legacyProcess(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { guard let deletions: [Deletion] = deletions else { return Promise.value([]) } @@ -1192,7 +1361,8 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Legacy Convenience - private static func legacySend(_ request: Request, through api: OnionRequestAPIType.Type = LegacyOnionRequestAPI.self) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> { + @available(*, deprecated, message: "Use v4 endpoint instead") + private static func legacySend(_ request: Request, through api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> Promise<(OnionRequestResponseInfoType, Data?)> { guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } var urlRequest: URLRequest = URLRequest(url: url) @@ -1211,14 +1381,14 @@ public final class OpenGroupAPIV2: NSObject { // Because legacy auth happens on a per-room basis, we need to have a room to // make an authenticated request guard let room = request.room else { - return api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey) + return api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey) } return legacyGetAuthToken(for: room, on: request.server) - .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> in + .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise<(OnionRequestResponseInfoType, Data?)> in urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue) - let promise = api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey) + let promise = api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey) promise.catch(on: OpenGroupAPIV2.workQueue) { error in // A 401 means that we didn't provide a (valid) auth token for a route // that required one. We use this as an indication that the token we're @@ -1238,7 +1408,7 @@ public final class OpenGroupAPIV2: NSObject { } } - return api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey) + return api.sendOnionRequest(urlRequest, to: request.server, using: .v3, with: publicKey) } preconditionFailure("It's currently not allowed to send non onion routed requests.") diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/Endpoint.swift index aa4a55ca1..7d8514571 100644 --- a/SessionMessagingKit/Open Groups/Types/Endpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/Endpoint.swift @@ -2,7 +2,7 @@ import Foundation -enum Endpoint { +public enum Endpoint: Hashable { // Utility case onion @@ -47,28 +47,28 @@ enum Endpoint { // Legacy endpoints (to be deprecated and removed) - case legacyFiles - case legacyFile(UInt64) + @available(*, deprecated, message: "Use v4 endpoint") case legacyFiles + @available(*, deprecated, message: "Use v4 endpoint") case legacyFile(UInt64) - case legacyMessages - case legacyMessagesForServer(Int64) - case legacyDeletedMessages + @available(*, deprecated, message: "Use v4 endpoint") case legacyMessages + @available(*, deprecated, message: "Use v4 endpoint") case legacyMessagesForServer(Int64) + @available(*, deprecated, message: "Use v4 endpoint") case legacyDeletedMessages - case legacyModerators + @available(*, deprecated, message: "Use v4 endpoint") case legacyModerators - case legacyBlockList - case legacyBlockListIndividual(String) - case legacyBanAndDeleteAll + @available(*, deprecated, message: "Use v4 endpoint") case legacyBlockList + @available(*, deprecated, message: "Use v4 endpoint") case legacyBlockListIndividual(String) + @available(*, deprecated, message: "Use v4 endpoint") case legacyBanAndDeleteAll - case legacyCompactPoll(legacyAuth: Bool) - case legacyAuthToken(legacyAuth: Bool) - case legacyAuthTokenChallenge(legacyAuth: Bool) - case legacyAuthTokenClaim(legacyAuth: Bool) + @available(*, deprecated, message: "Use v4 endpoint") case legacyCompactPoll(legacyAuth: Bool) + @available(*, deprecated, message: "Use request signing") case legacyAuthToken(legacyAuth: Bool) + @available(*, deprecated, message: "Use request signing") case legacyAuthTokenChallenge(legacyAuth: Bool) + @available(*, deprecated, message: "Use request signing") case legacyAuthTokenClaim(legacyAuth: Bool) - case legacyRooms - case legacyRoomInfo(String) - case legacyRoomImage(String) - case legacyMemberCount(legacyAuth: Bool) + @available(*, deprecated, message: "Use v4 endpoint") case legacyRooms + @available(*, deprecated, message: "Use v4 endpoint") case legacyRoomInfo(String) + @available(*, deprecated, message: "Use v4 endpoint") case legacyRoomImage(String) + @available(*, deprecated, message: "Use v4 endpoint") case legacyMemberCount(legacyAuth: Bool) var path: String { switch self { diff --git a/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift b/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift index e62a1c974..b4c5cb2e1 100644 --- a/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift +++ b/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift @@ -2,8 +2,18 @@ import Sodium +public protocol NonceGenerator16ByteType { + func nonce() -> Array +} + +extension NonceGenerator16ByteType { + +} + extension OpenGroupAPIV2 { - class NonceGenerator16Byte: NonceGenerator { - var NonceBytes: Int { 16 } + public class NonceGenerator16Byte: NonceGenerator, NonceGenerator16ByteType { + public var NonceBytes: Int { 16 } + + public init() {} } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index d434bbd56..2e6a8f3a8 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -52,7 +52,7 @@ public final class PushNotificationAPI : NSObject { let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { // TODO: Update this to use the V4 union requests once supported - LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey) + OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) } .map2 { response in guard let response: UnregisterResponse = try? response.decoded(as: UnregisterResponse.self) else { @@ -103,7 +103,7 @@ public final class PushNotificationAPI : NSObject { let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { // TODO: Update this to use the V4 union requests once supported - LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey) + OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) } .map2 { response in guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else { @@ -151,7 +151,7 @@ public final class PushNotificationAPI : NSObject { let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { // TODO: Update this to use the V4 union requests once supported - LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey) + OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) } .map2 { response in guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else { diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift index b5a53d4fe..23e0331a3 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift @@ -1,4 +1,5 @@ import PromiseKit +import SessionSnodeKit @objc(SNOpenGroupPollerV2) public final class OpenGroupPollerV2 : NSObject { @@ -48,9 +49,9 @@ public final class OpenGroupPollerV2 : NSObject { promise.retainUntilComplete() OpenGroupAPIV2.poll(server) - .done(on: OpenGroupAPIV2.workQueue) { [weak self] _ in + .done(on: OpenGroupAPIV2.workQueue) { [weak self] response in self?.isPolling = false - // TODO: Handle response + self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll) seal.fulfill(()) } .catch(on: OpenGroupAPIV2.workQueue) { [weak self] error in @@ -61,6 +62,94 @@ public final class OpenGroupPollerV2 : NSObject { return promise } + + private func handlePollResponse(_ response: [Endpoint: (info: OnionRequestResponseInfoType, data: Codable)], isBackgroundPoll: Bool) { + let storage = SNMessagingKitConfiguration.shared.storage + + response.forEach { endpoint, response in + switch endpoint { + case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): + guard let responseData: [OpenGroupAPIV2.Message] = response.data as? [OpenGroupAPIV2.Message] else { + //SNLog("Open group polling failed due to error: \(error).") + return // TODO: Throw error? + } + + handleMessages(responseData, roomToken: roomToken, isBackgroundPoll: isBackgroundPoll, using: storage) + + case .roomPollInfo(let roomToken, _): + guard let responseData: OpenGroupAPIV2.RoomPollInfo = response.data as? OpenGroupAPIV2.RoomPollInfo else { + //SNLog("Open group polling failed due to error: \(error).") + return // TODO: Throw error? + } + + handlePollInfo(responseData, roomToken: roomToken, isBackgroundPoll: isBackgroundPoll, using: storage) + + default: break // No custom handling needed + } + } + } + + // MARK: - Custom response handling + // TODO: Shift this logic to the OpenGroupManagerV2? (seems like the place it should belong?) + + private func handleMessages(_ messages: [OpenGroupAPIV2.Message], roomToken: String, isBackgroundPoll: Bool, using storage: SessionMessagingKitStorageProtocol) { + // Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages + let openGroupID = "\(server).\(roomToken)" + let sortedMessages: [OpenGroupAPIV2.Message] = messages + .sorted { lhs, rhs in lhs.seqNo < rhs.seqNo } + + storage.write { transaction in + var messageServerIDsToRemove: [UInt64] = [] + + sortedMessages.forEach { message in + guard let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString), let sender: String = message.sender else { + // A message with no data has been deleted so add it to the list to remove + messageServerIDsToRemove.append(UInt64(message.seqNo)) + return + } + + let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted))) + envelope.setContent(data) + envelope.setSource(sender) + + do { + let data = try envelope.buildSerializedData() + let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.seqNo), isRetry: false, using: transaction) + try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) + } + catch { + SNLog("Couldn't receive open group message due to error: \(error).") + } + } + + // Handle any deletions that are needed + guard !messageServerIDsToRemove.isEmpty else { return } + guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return } + guard let threadID = storage.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + return + } + + var messagesToRemove: [TSMessage] = [] + + thread.enumerateInteractions(with: transaction) { interaction, stop in + guard let message: TSMessage = interaction as? TSMessage, messageServerIDsToRemove.contains(message.openGroupServerMessageID) else { return } + messagesToRemove.append(message) + } + + messagesToRemove.forEach { $0.remove(with: transaction) } + } + } + + private func handlePollInfo(_ pollInfo: OpenGroupAPIV2.RoomPollInfo, roomToken: String, isBackgroundPoll: Bool, using storage: SessionMessagingKitStorageProtocol) { + // TODO: Handle other properties??? + + // - Moderators + OpenGroupAPIV2.moderators[server] = (OpenGroupAPIV2.moderators[server] ?? [:]) + .setting(roomToken, Set(pollInfo.moderators ?? [])) + + } + + // MARK: - Legacy Handling private func handleCompactPollBody(_ body: OpenGroupAPIV2.LegacyCompactPollResponse.Result, isBackgroundPoll: Bool) { let storage = SNMessagingKitConfiguration.shared.storage diff --git a/SessionMessagingKit/Utilities/Promise+Utilities.swift b/SessionMessagingKit/Utilities/Promise+Utilities.swift index e39b21082..84e71cd1c 100644 --- a/SessionMessagingKit/Utilities/Promise+Utilities.swift +++ b/SessionMessagingKit/Utilities/Promise+Utilities.swift @@ -12,9 +12,9 @@ extension Promise where T == Data { } } -extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) { - func decoded(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<(OnionRequestAPI.ResponseInfo, R)> { - self.map(on: queue) { responseInfo, maybeData -> (OnionRequestAPI.ResponseInfo, R) in +extension Promise where T == (OnionRequestResponseInfoType, Data?) { + func decoded(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<(OnionRequestResponseInfoType, R)> { + self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in guard let data: Data = maybeData else { throw OpenGroupAPIV2.Error.parsingFailed } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift new file mode 100644 index 000000000..8923287b6 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift @@ -0,0 +1,220 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import XCTest +import Nimble +import PromiseKit +import SessionSnodeKit + +@testable import SessionMessagingKit + +class OpenGroupAPIV2Tests: XCTestCase { + class TestResponseInfo: OnionRequestResponseInfoType { + let requestData: TestApi.RequestData + let code: Int + let headers: [String: String] + + init(requestData: TestApi.RequestData, code: Int, headers: [String: String]) { + self.requestData = requestData + self.code = code + self.headers = headers + } + } + + struct TestNonceGenerator: NonceGenerator16ByteType { + func nonce() -> Array { return Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes } + } + + class TestApi: OnionRequestAPIType { + struct RequestData: Codable { + let urlString: String? + let httpMethod: String + let headers: [String: String] + let snodeMethod: String? + let body: Data? + + let server: String + let version: OnionRequestAPI.Version + let publicKey: String? + } + + class var mockResponse: Data? { return nil } + + static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let responseInfo: TestResponseInfo = TestResponseInfo( + requestData: RequestData( + urlString: request.url?.absoluteString, + httpMethod: (request.httpMethod ?? "GET"), + headers: (request.allHTTPHeaderFields ?? [:]), + snodeMethod: nil, + body: request.httpBody, + + server: server, + version: version, + publicKey: x25519PublicKey + ), + code: 200, + headers: [:] + ) + + return Promise.value((responseInfo, mockResponse)) + } + + static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise { + // TODO: Test the 'responseInfo' somehow? + return Promise.value(mockResponse!) + } + } + + var testStorage: TestStorage! + + // MARK: - Configuration + + override func setUpWithError() throws { + testStorage = TestStorage() + + testStorage.mockData[.allV2OpenGroups] = [ + "0": OpenGroupV2(server: "testServer", room: "test1", name: "Test", publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", imageID: nil) + ] + testStorage.mockData[.openGroupPublicKeys] = ["testServer": "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"] + + // Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb) + testStorage.mockData[.userKeyPair] = try! ECKeyPair( + publicKeyData: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!, + privateKeyData: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")! + ) + } + + override func tearDownWithError() throws { + testStorage = nil + } + + // MARK: - Batching & Polling + + func testPollGeneratesTheCorrectRequest() throws { + // Define a custom TestApi class so we can override the response + class TestApi1: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.Capabilities(capabilities: [], missing: nil) + ) + ), + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.RoomPollInfo( + token: nil, + created: nil, + name: nil, + description: nil, + imageId: nil, + + infoUpdates: nil, + messageSequence: nil, + activeUsers: nil, + activeUsersCutoff: nil, + pinnedMessages: nil, + + admin: nil, + globalAdmin: nil, + admins: nil, + hiddenAdmins: nil, + + moderator: nil, + globalModerator: nil, + moderators: nil, + hiddenModerators: nil, + read: nil, + defaultRead: nil, + write: nil, + defaultWrite: nil, + upload: nil, + defaultUpload: nil, + details: nil + ) + ) + ), + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPIV2.Message]() + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + + var pollResponse: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + + OpenGroupAPIV2.poll("testServer", through: TestApi1.self, using: testStorage, nonceGenerator: TestNonceGenerator(), date: Date(timeIntervalSince1970: 1234567890)) + .map { result -> [Endpoint: (OnionRequestResponseInfoType, Codable)] in + pollResponse = result + return result + } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(10000) + ) + + // Validate the response data + expect(pollResponse?.values).to(haveCount(3)) + expect(pollResponse?.keys).to(contain(.capabilities)) + expect(pollResponse?.keys).to(contain(.roomPollInfo("test1", 0))) + expect(pollResponse?.keys).to(contain(.roomMessagesRecent("test1"))) + expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + + // Validate request data + let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/batch")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + } + + // MARK: - Authentication + + func testItSignsTheRequestCorrectly() throws { + class TestApi1: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode([OpenGroupAPIV2.Room]()) + } + } + + var response: (OnionRequestResponseInfoType, [OpenGroupAPIV2.Room])? = nil + + OpenGroupAPIV2.rooms(for: "testServer", through: TestApi1.self, using: testStorage, nonceGenerator: TestNonceGenerator(), date: Date(timeIntervalSince1970: 1234567890)) + .map { result -> (OnionRequestResponseInfoType, [OpenGroupAPIV2.Room]) in + response = result + return result + } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(10000) + ) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/rooms")) + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers).to(haveCount(4)) + expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) + expect(requestData?.headers[Header.sogsHash.rawValue]).to(equal("fxqLy5ZDWCsLQpwLw0Dax+4xe7cG2vPRk1NlHORIm0DPd3o9UA24KLZY")) + expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) + } +} diff --git a/SessionMessagingKitTests/_TestUtilities/Mockable.swift b/SessionMessagingKitTests/_TestUtilities/Mockable.swift new file mode 100644 index 000000000..b903f0fa3 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/Mockable.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +protocol Mockable { + associatedtype Key: Hashable + + var mockData: [Key: Any] { get } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift new file mode 100644 index 000000000..82f21d4a5 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -0,0 +1,114 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import PromiseKit +import Sodium + +@testable import SessionMessagingKit + +class TestStorage: SessionMessagingKitStorageProtocol, Mockable { + // MARK: - Mockable + + enum DataKey: Hashable { + case allV2OpenGroups + case openGroupPublicKeys + case userKeyPair + } + + typealias Key = DataKey + + var mockData: [DataKey: Any] = [:] + + // MARK: - Shared + + @discardableResult func write(with block: @escaping (Any) -> Void) -> Promise { + block(()) // TODO: Pass Transaction type to prevent force-cast crashes throughout codebase + return Promise.value(()) + } + + @discardableResult func write(with block: @escaping (Any) -> Void, completion: @escaping () -> Void) -> Promise { + block(()) // TODO: Pass Transaction type to prevent force-cast crashes throughout codebase + return Promise.value(()) + } + + func writeSync(with block: @escaping (Any) -> Void) { + block(()) // TODO: Pass Transaction type to prevent force-cast crashes throughout codebase + } + + // MARK: - General + + func getUserPublicKey() -> String? { return nil } + func getUserKeyPair() -> ECKeyPair? { return (mockData[.userKeyPair] as? ECKeyPair) } + func getUserED25519KeyPair() -> Box.KeyPair? { return nil } + func getUser() -> Contact? { return nil } + func getAllContacts() -> Set { return Set() } + + // MARK: - Closed Groups + + func getUserClosedGroupPublicKeys() -> Set { return Set() } + func getZombieMembers(for groupPublicKey: String) -> Set { return Set() } + func setZombieMembers(for groupPublicKey: String, to zombies: Set, using transaction: Any) {} + func isClosedGroup(_ publicKey: String) -> Bool { return false } + + // MARK: - Jobs + + func persist(_ job: Job, using transaction: Any) {} + func markJobAsSucceeded(_ job: Job, using transaction: Any) {} + func markJobAsFailed(_ job: Job, using transaction: Any) {} + func getAllPendingJobs(of type: Job.Type) -> [Job] { return [] } + func getAttachmentUploadJob(for attachmentID: String) -> AttachmentUploadJob? { return nil } + func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? { return nil } + func resumeMessageSendJobIfNeeded(_ messageSendJobID: String) {} + func isJobCanceled(_ job: Job) -> Bool { return true } + + // MARK: - Authorization + + func getAuthToken(for room: String, on server: String) -> String? { return nil } + func setAuthToken(for room: String, on server: String, to newValue: String, using transaction: Any) {} + func removeAuthToken(for room: String, on server: String, using transaction: Any) {} + + // MARK: - Open Groups + + func getAllV2OpenGroups() -> [String: OpenGroupV2] { return (mockData[.allV2OpenGroups] as! [String: OpenGroupV2]) } + func getV2OpenGroup(for threadID: String) -> OpenGroupV2? { return nil } + func v2GetThreadID(for v2OpenGroupID: String) -> String? { return nil } + func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set, using transaction: Any) {} + + // MARK: - Open Group Public Keys + + func getOpenGroupPublicKey(for server: String) -> String? { + guard let publicKeyMap: [String: String] = mockData[.openGroupPublicKeys] as? [String: String] else { + return (mockData[.openGroupPublicKeys] as? String) + } + + return publicKeyMap[server] + } + + func setOpenGroupPublicKey(for server: String, to newValue: String, using transaction: Any) {} + + // MARK: - Last Message Server ID + + func getLastMessageServerID(for room: String, on server: String) -> Int64? { return nil } + func setLastMessageServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) {} + func removeLastMessageServerID(for room: String, on server: String, using transaction: Any) {} + + // MARK: - Last Deletion Server ID + + func getLastDeletionServerID(for room: String, on server: String) -> Int64? { return nil } + func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) {} + func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) {} + + // MARK: - Open Group Metadata + + func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) {} + + // MARK: - Message Handling + + func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] { return [] } + func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) {} + func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { return nil } + func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { return nil } + func persist(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] { return [] } + func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any) {} + func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any) {} +} diff --git a/SessionSnodeKit/LegacyOnionRequestAPI.swift b/SessionSnodeKit/LegacyOnionRequestAPI.swift deleted file mode 100644 index 9f897e575..000000000 --- a/SessionSnodeKit/LegacyOnionRequestAPI.swift +++ /dev/null @@ -1,455 +0,0 @@ -import CryptoSwift -import PromiseKit -import SessionUtilitiesKit - -/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. -public enum LegacyOnionRequestAPI: OnionRequestAPIType { - private static var buildPathsPromise: Promise<[Path]>? = nil - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - private static var pathFailureCount: [Path:UInt] = [:] - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - private static var snodeFailureCount: [Snode:UInt] = [:] - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var guardSnodes: Set = [] - public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user - // MARK: Settings - public static let maxRequestSize = 10_000_000 // 10 MB - /// The number of snodes (including the guard snode) in a path. - private static let pathSize: UInt = 3 - /// The number of times a path can fail before it's replaced. - private static let pathFailureThreshold: UInt = 3 - /// The number of times a snode can fail before it's replaced. - private static let snodeFailureThreshold: UInt = 3 - /// The number of paths to maintain. - public static let targetPathCount: UInt = 2 - - /// The number of guard snodes required to maintain `targetPathCount` paths. - private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path - - // MARK: Error - public enum Error : LocalizedError { - case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: OnionRequestAPI.Destination) - case insufficientSnodes - case invalidURL - case missingSnodeVersion - case snodePublicKeySetMissing - case unsupportedSnodeVersion(String) - - public var errorDescription: String? { - switch self { - case .httpRequestFailedAtDestination(let statusCode, _, let destination): - if statusCode == 429 { - return "Rate limited." - } else { - 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)." - } - } - } - - // MARK: Path - public typealias Path = [Snode] - - // MARK: Onion Building Result - private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data) - - // 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) -> Promise { - let (promise, seal) = Promise.pending() - DispatchQueue.global(qos: .userInitiated).async { - let url = "\(snode.address):\(snode.port)/get_stats/v1" - let timeout: TimeInterval = 3 // Use a shorter timeout for testing - HTTP.execute(.get, url, timeout: timeout).done2 { json in - guard let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } - if version >= "2.0.7" { - seal.fulfill(()) - } else { - SNLog("Unsupported snode version: \(version).") - seal.reject(Error.unsupportedSnodeVersion(version)) - } - }.catch2 { error in - seal.reject(error) - } - } - return promise - } - - /// 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(reusing reusableGuardSnodes: [Snode]) -> Promise> { - if guardSnodes.count >= targetGuardSnodeCount { - return Promise> { $0.fulfill(guardSnodes) } - } else { - SNLog("Populating guard snode cache.") - var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue - let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) - guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: Error.insufficientSnodes) } - func getGuardSnode() -> Promise { - // randomElement() uses the system's default random generator, which is cryptographically secure - guard let candidate = unusedSnodes.randomElement() else { return Promise { $0.reject(Error.insufficientSnodes) } } - unusedSnodes.remove(candidate) // All used snodes should be unique - SNLog("Testing guard snode: \(candidate).") - // Loop until a reliable guard snode is found - return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in - withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() } - } - } - let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() } - return when(fulfilled: promises).map2 { guardSnodes in - let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes) - OnionRequestAPI.guardSnodes = guardSnodesAsSet - return guardSnodesAsSet - } - } - } - - /// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes` - /// if not enough (reliable) snodes are available. - @discardableResult - private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> { - if let existingBuildPathsPromise = buildPathsPromise { return existingBuildPathsPromise } - SNLog("Building onion request paths.") - DispatchQueue.main.async { - NotificationCenter.default.post(name: .buildingPaths, object: nil) - } - let reusableGuardSnodes = reusablePaths.map { $0[0] } - let promise: Promise<[Path]> = getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in - var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 }) - let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) - let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) - guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } - // Don't test path snodes as this would reveal the user's IP to them - return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in - let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in - // randomElement() uses the system's default random generator, which is cryptographically secure - let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above - unusedSnodes.remove(pathSnode) // All used snodes should be unique - return pathSnode - } - SNLog("Built new onion request path: \(result.prettifiedDescription).") - return result - } - }.map2 { paths in - OnionRequestAPI.paths = paths + reusablePaths - SNSnodeKitConfiguration.shared.storage.writeSync { transaction in - SNLog("Persisting onion request paths to database.") - SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction) - } - DispatchQueue.main.async { - NotificationCenter.default.post(name: .pathsBuilt, object: nil) - } - return paths - } - promise.done2 { _ in buildPathsPromise = nil } - promise.catch2 { _ in buildPathsPromise = nil } - buildPathsPromise = promise - return promise - } - - /// Returns a `Path` to be used for building an onion request. Builds new paths as needed. - private static func getPath(excluding snode: Snode?) -> Promise { - guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } - var paths = OnionRequestAPI.paths - if paths.isEmpty { - paths = SNSnodeKitConfiguration.shared.storage.getOnionRequestPaths() - OnionRequestAPI.paths = paths - if !paths.isEmpty { - guardSnodes.formUnion([ paths[0][0] ]) - if paths.count >= 2 { - guardSnodes.formUnion([ paths[1][0] ]) - } - } - } - // randomElement() uses the system's default random generator, which is cryptographically secure - if paths.count >= targetPathCount { - if let snode = snode { - return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) } - } else { - return Promise { $0.fulfill(paths.randomElement()!) } - } - } else if !paths.isEmpty { - if let snode = snode { - if let path = paths.first(where: { !$0.contains(snode) }) { - buildPaths(reusing: paths) // Re-build paths in the background - return Promise { $0.fulfill(path) } - } else { - return buildPaths(reusing: paths).map2 { paths in - return paths.filter { !$0.contains(snode) }.randomElement()! - } - } - } else { - buildPaths(reusing: paths) // Re-build paths in the background - return Promise { $0.fulfill(paths.randomElement()!) } - } - } else { - return buildPaths(reusing: []).map2 { paths in - if let snode = snode { - return paths.filter { !$0.contains(snode) }.randomElement()! - } else { - return paths.randomElement()! - } - } - } - } - - private static func dropGuardSnode(_ snode: Snode) { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - guardSnodes = guardSnodes.filter { $0 != snode } - } - - private static func drop(_ snode: Snode) throws { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - // 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:) because re-building the path - // in that case is async. - LegacyOnionRequestAPI.snodeFailureCount[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.subtracting(oldPaths.flatMap { $0 }) - guard !unusedSnodes.isEmpty else { throw Error.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 - oldPaths.remove(at: pathIndex) - let newPaths = oldPaths + [ path ] - paths = newPaths - SNSnodeKitConfiguration.shared.storage.writeSync { transaction in - SNLog("Persisting onion request paths to database.") - SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: newPaths, using: transaction) - } - } - - private static func drop(_ path: Path) { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - LegacyOnionRequestAPI.pathFailureCount[path] = 0 - var paths = LegacyOnionRequestAPI.paths - guard let pathIndex = paths.firstIndex(of: path) else { return } - paths.remove(at: pathIndex) - OnionRequestAPI.paths = paths - SNSnodeKitConfiguration.shared.storage.writeSync { transaction in - if !paths.isEmpty { - SNLog("Persisting onion request paths to database.") - SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction) - } else { - SNLog("Clearing onion request paths.") - SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: [], using: transaction) - } - } - } - - /// Builds an onion around `payload` and returns the result. - private static func buildOnion(around payload: JSON, targetedAt destination: OnionRequestAPI.Destination) -> Promise { - var guardSnode: Snode! - var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination - var encryptionResult: AESGCM.EncryptionResult! - var snodeToExclude: Snode? - if case .snode(let snode) = destination { snodeToExclude = snode } - return getPath(excluding: snodeToExclude).then2 { path -> Promise in - guardSnode = path.first! - // Encrypt in reverse order, i.e. the destination first - return OnionRequestAPI.encrypt(payload, for: destination).then2 { r -> Promise 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() -> Promise { - if path.isEmpty { - return Promise { $0.fulfill(encryptionResult) } - } else { - let lhs = OnionRequestAPI.Destination.snode(path.removeLast()) - return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise in - encryptionResult = r - rhs = lhs - return addLayer() - } - } - } - return addLayer() - } - }.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) } - } - - // MARK: Public API - /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { - let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] - return sendOnionRequest(with: payload, to: OnionRequestAPI.Destination.snode(snode)).recover2 { error -> Promise in - guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error - } - } - - /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> { - var rawHeaders = request.allHTTPHeaderFields ?? [:] - rawHeaders.removeValue(forKey: "User-Agent") - var headers: JSON = rawHeaders.mapValues { value in - switch value.lowercased() { - case "true": return true - case "false": return false - default: return value - } - } - guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) } - var endpoint = url.path.removingPrefix("/") - if let query = url.query { endpoint += "?\(query)" } - let scheme = url.scheme - let port = given(url.port) { UInt16($0) } - let bodyAsString: String - - if let body: Data = request.httpBody { - headers["Content-Type"] = "application/json" // Assume data is JSON - bodyAsString = (String(data: body, encoding: .utf8) ?? "null") - } - else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) { - headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] - bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" - } - else { - bodyAsString = "null" - } - - let payload: JSON = [ - "body" : bodyAsString, - "endpoint" : endpoint, - "method" : request.httpMethod!, - "headers" : headers - ] - let destination = OnionRequestAPI.Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port) - let promise = sendOnionRequest(with: payload, to: destination) - .map { (json: JSON) -> (OnionRequestAPI.ResponseInfo, Data?) in - guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else { throw HTTP.Error.invalidJSON } - - return (OnionRequestAPI.ResponseInfo(code: 200, headers: [:]), data) - } - promise.catch2 { error in - SNLog("Couldn't reach server: \(url) due to error: \(error).") - } - return promise - } - - public static func sendOnionRequest(with payload: JSON, to destination: OnionRequestAPI.Destination) -> Promise { - let (promise, seal) = Promise.pending() - var guardSnode: Snode? - Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` - buildOnion(around: payload, targetedAt: destination).done2 { intermediate in - guardSnode = intermediate.guardSnode - let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2" - let finalEncryptionResult = intermediate.finalEncryptionResult - let onion = finalEncryptionResult.ciphertext - if case OnionRequestAPI.Destination.server = destination, Double(onion.count) > 0.75 * Double(maxRequestSize) { - SNLog("Approaching request size limit: ~\(onion.count) bytes.") - } - let parameters: JSON = [ - "ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString() - ] - let body: Data - do { - body = try OnionRequestAPI.encode(ciphertext: onion, json: parameters) - } catch { - return seal.reject(error) - } - let destinationSymmetricKey = intermediate.destinationSymmetricKey - HTTP.execute(.post, url, body: body).done2 { json in - guard let base64EncodedIVAndCiphertext = json["result"] as? String, - let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } - do { - let data = try AESGCM.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 seal.reject(HTTP.Error.invalidJSON) } - if statusCode == 406 { // Clock out of sync - SNLog("The user's clock is out of sync with the service node network.") - seal.reject(SnodeAPI.Error.clockOutOfSync) - } else if let bodyAsString = json["body"] as? String { - guard let bodyAsData = bodyAsString.data(using: .utf8), - let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } - if let timestamp = body["t"] as? Int64 { - let offset = timestamp - Int64(NSDate.millisecondTimestamp()) - SnodeAPI.clockOffset = offset - } - guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) - } - seal.fulfill(body) - } else { - guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) - } - seal.fulfill(json) - } - } catch { - seal.reject(error) - } - }.catch2 { error in - seal.reject(error) - } - }.catch2 { error in - seal.reject(error) - } - } - promise.catch2 { error in // Must be invoked on Threading.workQueue - guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error, let guardSnode = guardSnode else { return } - let path = paths.first { $0.contains(guardSnode) } - func handleUnspecificError() { - guard let path = path else { return } - var pathFailureCount = LegacyOnionRequestAPI.pathFailureCount[path] ?? 0 - pathFailureCount += 1 - if pathFailureCount >= pathFailureThreshold { - dropGuardSnode(guardSnode) - path.forEach { snode in - SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw - } - drop(path) - } else { - LegacyOnionRequestAPI.pathFailureCount[path] = pathFailureCount - } - } - let prefix = "Next node not found: " - if let message = json?["result"] as? String, message.hasPrefix(prefix) { - let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..= snodeFailureThreshold { - SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw - do { - try drop(snode) - } catch { - handleUnspecificError() - } - } else { - LegacyOnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount - } - } else { - // Do nothing - } - } else if let message = json?["result"] as? String, message == "Loki Server error" { - // Do nothing - } else if case .server(let host, _, _, _, _) = destination, host == "116.203.70.33" && statusCode == 0 { - // FIXME: Temporary thing to kick out nodes that can't talk to the V2 OGS yet - handleUnspecificError() - } else if statusCode == 0 { // Timeout - // Do nothing - } else { - handleUnspecificError() - } - } - return promise - } -} diff --git a/SessionSnodeKit/Models/Destination.swift b/SessionSnodeKit/Models/Destination.swift new file mode 100644 index 000000000..f879c1034 --- /dev/null +++ b/SessionSnodeKit/Models/Destination.swift @@ -0,0 +1,17 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OnionRequestAPI { + public enum Destination: CustomStringConvertible { + case snode(Snode) + case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?) + + public var description: String { + switch self { + case .snode(let snode): return "Service node \(snode.ip):\(snode.port)" + case .server(let host, _, _, _, _): return host + } + } + } +} diff --git a/SessionSnodeKit/Models/Error.swift b/SessionSnodeKit/Models/Error.swift new file mode 100644 index 000000000..d12635df8 --- /dev/null +++ b/SessionSnodeKit/Models/Error.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +extension OnionRequestAPI { + public enum Error: LocalizedError { + case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: Destination) + case insufficientSnodes + case invalidURL + case missingSnodeVersion + case snodePublicKeySetMissing + case unsupportedSnodeVersion(String) + case invalidRequestInfo + + public var errorDescription: String? { + switch self { + case .httpRequestFailedAtDestination(let statusCode, _, let destination): + if statusCode == 429 { + return "Rate limited." + } + + return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)." + + case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path." + case .invalidURL: return "Invalid URL" + case .missingSnodeVersion: return "Missing Service Node version." + case .snodePublicKeySetMissing: return "Missing Service Node public key set." + case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)." + case .invalidRequestInfo: return "Invalid Request Info" + } + } + } +} diff --git a/SessionSnodeKit/Models/RequestInfo.swift b/SessionSnodeKit/Models/RequestInfo.swift new file mode 100644 index 000000000..8072364df --- /dev/null +++ b/SessionSnodeKit/Models/RequestInfo.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OnionRequestAPI { + struct RequestInfo: Codable { + let method: String + let endpoint: String + let headers: [String: String] + } +} diff --git a/SessionSnodeKit/Models/ResponseInfo.swift b/SessionSnodeKit/Models/ResponseInfo.swift new file mode 100644 index 000000000..80e9b5f87 --- /dev/null +++ b/SessionSnodeKit/Models/ResponseInfo.swift @@ -0,0 +1,20 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol OnionRequestResponseInfoType: Codable { + var code: Int { get } + var headers: [String: String] { get } +} + +extension OnionRequestAPI { + public struct ResponseInfo: OnionRequestResponseInfoType { + public let code: Int + public let headers: [String: String] + + public init(code: Int, headers: [String: String]) { + self.code = code + self.headers = headers + } + } +} diff --git a/SessionSnodeKit/Models/Version.swift b/SessionSnodeKit/Models/Version.swift new file mode 100644 index 000000000..d45ca87ef --- /dev/null +++ b/SessionSnodeKit/Models/Version.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OnionRequestAPI { + public enum Version: String, Codable { + case v2 = "/loki/v2/lsrpc" + case v3 = "/loki/v3/lsrpc" + case v4 = "/oxen/v4/lsrpc" + } +} diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 84765f969..016a373ca 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -4,12 +4,17 @@ import PromiseKit import SessionUtilitiesKit public protocol OnionRequestAPIType { - static func sendOnionRequest(_ request: URLRequest, to server: String, target: String, using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> + static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise + static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> } public extension OnionRequestAPIType { - static func sendOnionRequest(_ request: URLRequest, to server: String, using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> { - sendOnionRequest(request, to: server, target: "/oxen/v4/lsrpc", using: x25519PublicKey) + static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version = .v3) -> Promise { + return sendOnionRequest(to: snode, invoking: method, with: parameters, using: version, associatedWith: nil) + } + + static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey) } } @@ -38,58 +43,6 @@ public enum OnionRequestAPI: OnionRequestAPIType { /// The number of guard snodes required to maintain `targetPathCount` paths. private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path - // MARK: Destination - public enum Destination : CustomStringConvertible { - case snode(Snode) - case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?) - - public var description: String { - switch self { - case .snode(let snode): return "Service node \(snode.ip):\(snode.port)" - case .server(let host, _, _, _, _): return host - } - } - } - - // MARK: Error - public enum Error : LocalizedError { - case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: Destination) - case insufficientSnodes - case invalidURL - case missingSnodeVersion - case snodePublicKeySetMissing - case unsupportedSnodeVersion(String) - case invalidRequestInfo - - public var errorDescription: String? { - switch self { - case .httpRequestFailedAtDestination(let statusCode, _, let destination): - if statusCode == 429 { - return "Rate limited." - } else { - 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" - } - } - } - - // MARK: RequestInfo - private struct RequestInfo: Codable { - let method: String - let endpoint: String - let headers: [String: String] - } - - public struct ResponseInfo: Codable { - let code: Int - let headers: [String: String] - } // MARK: Path public typealias Path = [Snode] @@ -324,78 +277,55 @@ public enum OnionRequestAPI: OnionRequestAPIType { }.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) } } - // MARK: Public API -// /// Sends an onion request to `snode`. Builds new paths as needed. -// public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { -// let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] -// return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in -// guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error } -// throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error -// } -// } + // MARK: - Public API + + /// Sends an onion request to `snode`. Builds new paths as needed. + public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: Version = .v3, associatedWith publicKey: String? = nil) -> Promise { + let payloadJson: JSON = [ "method": method.rawValue, "params": parameters ] + + guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []), let payload: String = String(data: jsonData, encoding: .utf8) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + return sendOnionRequest(with: payload, to: Destination.snode(snode), version: version) + .map { _, maybeData in + guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse } + + return data + } + .recover2 { error -> Promise in + guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { + throw error + } + + throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error + } + } /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/oxen/v4/lsrpc", using x25519PublicKey: String) -> Promise<(ResponseInfo, Data?)> { - guard server == "https://chat.lokinet.dev" else { // TODO: Remove this - return LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v3/lsrpc", using: x25519PublicKey) + public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: Version = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + guard version != .v4 || server == "https://chat.lokinet.dev" else { // TODO: Remove this + return sendOnionRequest(request, to: server, using: .v3, with: x25519PublicKey) } guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) } - // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy - // endpoint (in which case we need it to ensure the request signing works correctly - // TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints. - let endpoint: String = url.path - .appending(url.query.map { value in "?\(value)" }) let scheme: String? = url.scheme let port: UInt16? = url.port.map { UInt16($0) } - let requestInfo: RequestInfo = RequestInfo( - method: (request.httpMethod ?? "GET"), // Default (if nil) is 'GET' - endpoint: endpoint, - headers: (request.allHTTPHeaderFields ?? [:]) - .setting( - "Content-Type", - // TODO: Determine what 'Content-Type' 'httpBodyStream' should have??? - (request.httpBody == nil && request.httpBodyStream == nil ? nil : - ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") // Default to JSON if not defined - ) - ) - .removingValue(forKey: "User-Agent") - ) - - guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo), let requestInfoString: String = String(data: requestInfoData, encoding: .ascii) else { + guard let payload: String = generatePayload(for: request, with: version) else { return Promise(error: Error.invalidRequestInfo) } - let payload: String - - if let body: Data = request.httpBody { - guard let bodyString: String = String(data: body, encoding: .ascii) else { - return Promise(error: Error.invalidRequestInfo) - } - - payload = "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" - } - else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream), let bodyString: String = String(data: body, encoding: .ascii) { - // TODO: Handle this properly -// headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] -// bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" - payload = "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" - } - else { - payload = "l\(requestInfoString.count):\(requestInfoString)e" - } - - let destination = Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port) - let promise = sendOnionRequest(with: payload, to: destination) + let destination = Destination.server(host: host, target: version.rawValue, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port) + let promise = sendOnionRequest(with: payload, to: destination, version: version) promise.catch2 { error in SNLog("Couldn't reach server: \(url) due to error: \(error).") } return promise } - public static func sendOnionRequest(with payload: String, to destination: Destination) -> Promise<(ResponseInfo, Data?)> { - let (promise, seal) = Promise<(ResponseInfo, Data?)>.pending() + public static func sendOnionRequest(with payload: String, to destination: Destination, version: Version) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending() var guardSnode: Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` buildOnion(around: payload, targetedAt: destination).done2 { intermediate in @@ -419,80 +349,13 @@ public enum OnionRequestAPI: OnionRequestAPIType { HTTP.updatedExecute(.post, url, body: body) .done2 { responseData in - guard responseData.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidResponse) } - - do { - let data: Data = try AESGCM.decrypt(responseData, with: destinationSymmetricKey) - - // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into - // parts to properly process it - guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let stringParts: [String.SubSequence] = responseString.split(separator: ":") - - guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) - let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) - let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { - return seal.fulfill((responseInfo, nil)) - } - - // TODO: Is this going to be done anymore...??? -// if let timestamp = body["t"] as? Int64 { -// let offset = timestamp - Int64(NSDate.millisecondTimestamp()) -// SnodeAPI.clockOffset = offset -// } - - // Extract the response data as well - let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) - let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") - - guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]) else { - return seal.reject(HTTP.Error.invalidResponse) - } - - let finalDataStringStartIndex: String.Index = responseString.index(infoStringEndIndex, offsetBy: "\(finalDataLength):".count) - let finalDataStringEndIndex: String.Index = responseString.index(finalDataStringStartIndex, offsetBy: finalDataLength) - let finalDataString: String = String(responseString[finalDataStringStartIndex.. String? { + guard let url = request.url else { return nil } + + switch version { + // V2 and V3 Onion Requests have the same structure + case .v2, .v3: + var rawHeaders = request.allHTTPHeaderFields ?? [:] + rawHeaders.removeValue(forKey: "User-Agent") + var headers: JSON = rawHeaders.mapValues { value in + switch value.lowercased() { + case "true": return true + case "false": return false + default: return value + } + } + + var endpoint = url.path.removingPrefix("/") + if let query = url.query { endpoint += "?\(query)" } + let bodyAsString: String + + if let body: Data = request.httpBody { + headers["Content-Type"] = "application/json" // Assume data is JSON + bodyAsString = (String(data: body, encoding: .utf8) ?? "null") + } + else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) { + headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] + bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" + } + else { + bodyAsString = "null" + } + + let payload: JSON = [ + "body" : bodyAsString, + "endpoint" : endpoint, + "method" : request.httpMethod!, + "headers" : headers + ] + + guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return nil } + + return String(data: jsonData, encoding: .utf8) + + // V4 Onion Requests have a very different structure + case .v4: + // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy + // endpoint (in which case we need it to ensure the request signing works correctly + // TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints + let endpoint: String = url.path + .appending(url.query.map { value in "?\(value)" }) + + let requestInfo: RequestInfo = RequestInfo( + method: (request.httpMethod ?? "GET"), // Default (if nil) is 'GET' + endpoint: endpoint, + headers: (request.allHTTPHeaderFields ?? [:]) + .setting( + "Content-Type", + // TODO: Determine what 'Content-Type' 'httpBodyStream' should have???. + (request.httpBody == nil && request.httpBodyStream == nil ? nil : + ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") // Default to JSON if not defined + ) + ) + .removingValue(forKey: "User-Agent") + ) + + guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo), let requestInfoString: String = String(data: requestInfoData, encoding: .ascii) else { + return nil + } + + if let body: Data = request.httpBody { + guard let bodyString: String = String(data: body, encoding: .ascii) else { + return nil + } + + return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" + } + else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream), let bodyString: String = String(data: body, encoding: .ascii) { + // TODO: Handle this properly + // headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] + // bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" + return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" + } + else { + return "l\(requestInfoString.count):\(requestInfoString)e" + } + } + } + + private static func handleResponse( + responseData: Data, + destinationSymmetricKey: Data, + version: Version, + destination: Destination, + seal: Resolver<(OnionRequestResponseInfoType, Data?)> + ) { + 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 seal.reject(HTTP.Error.invalidJSON) + } + + guard let base64EncodedIVAndCiphertext = json["result"] as? String, let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { + return seal.reject(HTTP.Error.invalidJSON) + } + + do { + let data = try AESGCM.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 seal.reject(HTTP.Error.invalidJSON) + } + + if statusCode == 406 { // Clock out of sync + SNLog("The user's clock is out of sync with the service node network.") + return seal.reject(SnodeAPI.Error.clockOutOfSync) + } + + if let bodyAsString = json["body"] as? String { + guard let bodyAsData = bodyAsString.data(using: .utf8), let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { + return seal.reject(HTTP.Error.invalidJSON) + } + + if let timestamp = body["t"] as? Int64 { + let offset = timestamp - Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset = offset + } + + guard 200...299 ~= statusCode else { + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) + } + + return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), bodyAsData)) + } + + guard 200...299 ~= statusCode else { + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) + } + + return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), data)) + + } + catch { + return seal.reject(error) + } + + // V4 Onion Requests have a very different structure for responses + case .v4: + guard responseData.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidResponse) } + + do { + let data: Data = try AESGCM.decrypt(responseData, with: destinationSymmetricKey) + + // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into + // parts to properly process it + guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { + return seal.reject(HTTP.Error.invalidResponse) + } + + let stringParts: [String.SubSequence] = responseString.split(separator: ":") + + guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { + return seal.reject(HTTP.Error.invalidResponse) + } + + let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) + let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) + let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { + return seal.fulfill((responseInfo, nil)) + } + + // TODO: Is this going to be done anymore...??? +// if let timestamp = body["t"] as? Int64 { +// let offset = timestamp - Int64(NSDate.millisecondTimestamp()) +// SnodeAPI.clockOffset = offset +// } + + // Extract the response data as well + let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) + let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") + + guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]) else { + return seal.reject(HTTP.Error.invalidResponse) + } + + let finalDataStringStartIndex: String.Index = responseString.index(infoStringEndIndex, offsetBy: "\(finalDataLength):".count) + let finalDataStringEndIndex: String.Index = responseString.index(finalDataStringStartIndex, offsetBy: finalDataLength) + let finalDataString: String = String(responseString[finalDataStringStartIndex.. RawResponsePromise { if Features.useOnionRequests { - // TODO: Ensure this should use the Legact request? - return LegacyOnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any } + // TODO: Ensure this should use the v3 request? + return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey).map2 { $0 as Any } } else { let url = "\(snode.address):\(snode.port)/storage_rpc/v1" return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise in