Started cleaning up some of the SOGS and Onion Requests structure

Cleaned up the OnionRequestAPI so we don't need the LegacyOnionRequestAPI
Added requests for the user endpoints
Added deprecated flags to the legacy endpoints and functions
Added some logic to start handling the new poll (batch) response
Started adding unit tests for the OpenGroupAPI functions
This commit is contained in:
Morgan Pretty 2022-02-15 13:55:59 +11:00
parent c90f346d6a
commit eb927c36a9
35 changed files with 1718 additions and 975 deletions

View File

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

View File

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

View File

@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
C3F0A607255C98A6007BE2A3 /* Storage+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+SnodeAPI.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
D24B5BD4169F568C00681372 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = ../../../../../../System/Library/Frameworks/AudioToolbox.framework; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Permissions.m"; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = "<group>"; };
FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyOnionRequestAPI.swift; sourceTree = "<group>"; };
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 = "<group>"; };
FDC4389C27BA01F000C60D73 /* TestStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStorage.swift; sourceTree = "<group>"; };
FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = "<group>"; };
FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = "<group>"; };
FDC438A727BB11CD00C60D73 /* UserPermissionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissionsRequest.swift; sourceTree = "<group>"; };
FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = "<group>"; };
FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesRequest.swift; sourceTree = "<group>"; };
FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteMessagesResponse.swift; sourceTree = "<group>"; };
FDC438B027BB159600C60D73 /* RequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInfo.swift; sourceTree = "<group>"; };
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = "<group>"; };
FDC438B427BB15D400C60D73 /* Destination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Destination.swift; sourceTree = "<group>"; };
FDC438B627BB160000C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
FDC438B827BB161E00C60D73 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
@ -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 = "<group>";
@ -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 = "<group>";
@ -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 = "<group>";
};
FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */ = {
isa = PBXGroup;
children = (
FDC4389B27BA01E300C60D73 /* _TestUtilities */,
FDC4389827BA001800C60D73 /* Open Groups */,
);
path = SessionMessagingKitTests;
sourceTree = "<group>";
};
FDC4389827BA001800C60D73 /* Open Groups */ = {
isa = PBXGroup;
children = (
FDC4389927BA002500C60D73 /* OpenGroupAPIV2Tests.swift */,
);
path = "Open Groups";
sourceTree = "<group>";
};
FDC4389B27BA01E300C60D73 /* _TestUtilities */ = {
isa = PBXGroup;
children = (
FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
FDC4389C27BA01F000C60D73 /* TestStorage.swift */,
);
path = _TestUtilities;
sourceTree = "<group>";
};
FDC438AF27BB158500C60D73 /* Models */ = {
isa = PBXGroup;
children = (
FDC438B827BB161E00C60D73 /* Version.swift */,
FDC438B627BB160000C60D73 /* Error.swift */,
FDC438B427BB15D400C60D73 /* Destination.swift */,
FDC438B027BB159600C60D73 /* RequestInfo.swift */,
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */,
);
path = Models;
sourceTree = "<group>";
};
/* 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 */;

View File

@ -20,20 +20,6 @@
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "11319FE11E0F163FEF714A606CCC265F"
BuildableName = "SignalServiceKit.framework"
BlueprintName = "SignalServiceKit"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@ -62,170 +48,12 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D221A0A9169C9E5F00537ABF"
BuildableName = "SignalTests.xctest"
BlueprintName = "SignalTests"
BlueprintIdentifier = "FDC4388D27B9FFC700C60D73"
BuildableName = "SessionMessagingKitTests.xctest"
BlueprintName = "SessionMessagingKitTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B772E882F193AA2F25932C514BBF0805"
BuildableName = "SignalServiceKit-Unit-Tests.xctest"
BlueprintName = "SignalServiceKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "ContactSortingTest">
</Test>
<Test
Identifier = "DeviceNamesTest">
</Test>
<Test
Identifier = "JobQueueTest">
</Test>
<Test
Identifier = "MessageSenderJobQueueTest">
</Test>
<Test
Identifier = "OWSAnalyticsTests">
</Test>
<Test
Identifier = "OWSDeviceProvisionerTest">
</Test>
<Test
Identifier = "OWSDisappearingMessageFinderTest">
</Test>
<Test
Identifier = "OWSDisappearingMessagesConfigurationTest">
</Test>
<Test
Identifier = "OWSDisappearingMessagesJobTest">
</Test>
<Test
Identifier = "OWSFingerprintTest">
</Test>
<Test
Identifier = "OWSIncomingMessageFinderTest">
</Test>
<Test
Identifier = "OWSLinkPreviewTest">
</Test>
<Test
Identifier = "OWSMessageManagerTest">
</Test>
<Test
Identifier = "OWSMessageSenderTest">
</Test>
<Test
Identifier = "OWSProvisioningCipherTest">
</Test>
<Test
Identifier = "OWSSignalAddressTest">
</Test>
<Test
Identifier = "OWSUDManagerTest">
</Test>
<Test
Identifier = "PhoneNumberTest">
</Test>
<Test
Identifier = "PhoneNumberUtilTest">
</Test>
<Test
Identifier = "SSKBaseTestObjC">
</Test>
<Test
Identifier = "SSKBaseTestSwift">
</Test>
<Test
Identifier = "SSKMessageSenderJobRecordTest">
</Test>
<Test
Identifier = "SignalRecipientTest">
</Test>
<Test
Identifier = "SignedPreKeyDeletionTests">
</Test>
<Test
Identifier = "TSContactThreadTest">
</Test>
<Test
Identifier = "TSGroupThreadTest">
</Test>
<Test
Identifier = "TSMessageStorageTests">
</Test>
<Test
Identifier = "TSMessageTest">
</Test>
<Test
Identifier = "TSOutgoingMessageTest">
</Test>
<Test
Identifier = "TSStorageIdentityKeyStoreTests">
</Test>
<Test
Identifier = "TSStoragePreKeyStoreTests">
</Test>
<Test
Identifier = "TSThreadTest">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5C9F6BA9ADC4724B2612C9F20FBE2076"
BuildableName = "SignalCoreKit-Unit-Tests.xctest"
BlueprintName = "SignalCoreKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF2BCB29C9D47F15FB156F1EC64E5CC2"
BuildableName = "AxolotlKit-Unit-Tests.xctest"
BlueprintName = "AxolotlKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "78DE33AED82B26B4B8D899CC403003AF"
BuildableName = "Curve25519Kit-Unit-Tests.xctest"
BlueprintName = "Curve25519Kit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AF7FC2C93AA68E33600807F168BD483A"
BuildableName = "HKDFKit-Unit-Tests.xctest"
BlueprintName = "HKDFKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B086B0C72F8A5814FF48795531F21635"
BuildableName = "SignalMetadataKit-Unit-Tests.xctest"
BlueprintName = "SignalMetadataKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1"
BuildableName = "SessionMessagingKit.framework"
BlueprintName = "SessionMessagingKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDC4388D27B9FFC700C60D73"
BuildableName = "SessionMessagingKitTests.xctest"
BlueprintName = "SessionMessagingKitTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "App Store Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1"
BuildableName = "SessionMessagingKit.framework"
BlueprintName = "SessionMessagingKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "App Store Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -43,6 +43,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDC4388027B9FF1E00C60D73"
BuildableName = "SessionTests.xctest"
BlueprintName = "SessionTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
@ -73,6 +83,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -52,6 +52,16 @@
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDC4388027B9FF1E00C60D73"
BuildableName = "SessionTests.xctest"
BlueprintName = "SessionTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
@ -83,6 +93,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

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

View File

@ -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: []) }
}

View File

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

View File

@ -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<OpenGroupAPIV2.BatchResponse> {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,14 @@ public final class OpenGroupAPIV2: NSObject {
// MARK: - Batching & Polling
public static func poll(_ server: String) -> Promise<Void> {
/// 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/<token>/pollInfo/<id> 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<String> {
/// 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<LegacyCompactPollResponse> {
// TODO: `/sequence` request
public static func compactPoll(_ server: String, api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> Promise<LegacyCompactPollResponse> {
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<LegacyCompactPollResponse> 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<OnionRequestAPI.ResponseInfo> {
public static func pinMessage(id: Int64, in roomToken: String, on server: String) -> Promise<OnionRequestResponseInfoType> {
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<OnionRequestAPI.ResponseInfo> {
public static func unpinMessage(id: Int64, in roomToken: String, on server: String) -> Promise<OnionRequestResponseInfoType> {
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<OnionRequestAPI.ResponseInfo> {
public static func unpinAll(in roomToken: String, on server: String) -> Promise<OnionRequestResponseInfoType> {
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<String> {
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<String> {
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<String> {
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<Void> {
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<LegacyCompactPollResponse> {
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<LegacyRoomInfo> {
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<Data> {
// 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<UInt64> {
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<UInt64> {
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<Data> {
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<OpenGroupMessageV2> {
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<Void> {
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<Void> {
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<Void> {
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<Void> {
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.")

View File

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

View File

@ -2,8 +2,18 @@
import Sodium
public protocol NonceGenerator16ByteType {
func nonce() -> Array<UInt8>
}
extension NonceGenerator16ByteType {
}
extension OpenGroupAPIV2 {
class NonceGenerator16Byte: NonceGenerator {
var NonceBytes: Int { 16 }
public class NonceGenerator16Byte: NonceGenerator, NonceGenerator16ByteType {
public var NonceBytes: Int { 16 }
public init() {}
}
}

View File

@ -52,7 +52,7 @@ public final class PushNotificationAPI : NSObject {
let promise: Promise<Void> = 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<Void> = 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<Void> = 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 {

View File

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

View File

@ -12,9 +12,9 @@ extension Promise where T == Data {
}
}
extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) {
func decoded<R: Decodable>(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<R: Decodable>(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
}

View File

@ -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<UInt8> { 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<Data> {
// 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"))
}
}

View File

@ -0,0 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
protocol Mockable {
associatedtype Key: Hashable
var mockData: [Key: Any] { get }
}

View File

@ -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<Void> {
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<Void> {
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<Contact> { return Set() }
// MARK: - Closed Groups
func getUserClosedGroupPublicKeys() -> Set<String> { return Set() }
func getZombieMembers(for groupPublicKey: String) -> Set<String> { return Set() }
func setZombieMembers(for groupPublicKey: String, to zombies: Set<String>, 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<String>, 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) {}
}

View File

@ -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<Snode> = []
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<Void> {
let (promise, seal) = Promise<Void>.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<Set<Snode>> {
if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Snode>> { $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<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $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<Path> {
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<OnionBuildingResult> {
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<AESGCM.EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return OnionRequestAPI.encrypt(payload, for: destination).then2 { r -> Promise<AESGCM.EncryptionResult> 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<AESGCM.EncryptionResult> {
if path.isEmpty {
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = OnionRequestAPI.Destination.snode(path.removeLast())
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<AESGCM.EncryptionResult> 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<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
return sendOnionRequest(with: payload, to: OnionRequestAPI.Destination.snode(snode)).recover2 { error -> Promise<JSON> 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<JSON> {
let (promise, seal) = Promise<JSON>.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)..<message.endIndex]
if let path = path, let snode = path.first(where: { $0.publicKeySet.ed25519Key == ed25519PublicKey }) {
var snodeFailureCount = LegacyOnionRequestAPI.snodeFailureCount[snode] ?? 0
snodeFailureCount += 1
if snodeFailureCount >= 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Data>
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<Data> {
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<Data> {
// let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
// return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<Data> 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<Data> {
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<Data> 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..<infoStringEndIndex])
guard let infoStringData: Data = infoString.data(using: .utf8), let responseInfo: ResponseInfo = try? JSONDecoder().decode(ResponseInfo.self, from: infoStringData) else {
return seal.reject(HTTP.Error.invalidResponse)
}
// Custom handle a clock out of sync error
guard responseInfo.code != 406 else {
SNLog("The user's clock is out of sync with the service node network.")
return seal.reject(SnodeAPI.Error.clockOutOfSync)
}
// Handle error status codes
guard 200...299 ~= responseInfo.code else {
return seal.reject(
Error.httpRequestFailedAtDestination(
statusCode: UInt(responseInfo.code),
json: [:], // TODO: Remove the 'json' value??
destination: destination
)
)
}
// If there is no data in the response then just return the ResponseInfo
guard responseString.count > "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..<finalDataStringEndIndex])
guard let finalData: Data = finalDataString.data(using: .ascii) else {
return seal.reject(HTTP.Error.invalidResponse)
}
return seal.fulfill((responseInfo, finalData))
}
catch {
seal.reject(error)
}
handleResponse(
responseData: responseData,
destinationSymmetricKey: destinationSymmetricKey,
version: version,
destination: destination,
seal: seal
)
}
.catch2 { error in
seal.reject(error)
@ -570,4 +433,239 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return promise
}
// MARK: - Version Handling
private static func generatePayload(for request: URLRequest, with version: Version) -> 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..<infoStringEndIndex])
guard let infoStringData: Data = infoString.data(using: .utf8), let responseInfo: ResponseInfo = try? JSONDecoder().decode(ResponseInfo.self, from: infoStringData) else {
return seal.reject(HTTP.Error.invalidResponse)
}
// Custom handle a clock out of sync error
guard responseInfo.code != 406 else {
SNLog("The user's clock is out of sync with the service node network.")
return seal.reject(SnodeAPI.Error.clockOutOfSync)
}
// Handle error status codes
guard 200...299 ~= responseInfo.code else {
return seal.reject(
Error.httpRequestFailedAtDestination(
statusCode: UInt(responseInfo.code),
json: [:], // TODO: Remove the 'json' value??
destination: destination
)
)
}
// If there is no data in the response then just return the ResponseInfo
guard responseString.count > "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..<finalDataStringEndIndex])
guard let finalData: Data = finalDataString.data(using: .ascii) else {
return seal.reject(HTTP.Error.invalidResponse)
}
return seal.fulfill((responseInfo, finalData))
}
catch {
return seal.reject(error)
}
}
}
}

View File

@ -131,8 +131,8 @@ public final class SnodeAPI : NSObject {
// MARK: Internal API
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> 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<Any> in