Updated the code to decode and use updated notifications

Made the JobQueue execution type explicit
Fixed a bug where legacy group's might not be unsubscribed from
This commit is contained in:
Morgan Pretty 2023-05-26 14:27:14 +10:00
parent 61ad85b97b
commit 09ab977861
12 changed files with 537 additions and 57 deletions

View File

@ -654,6 +654,7 @@
FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; };
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; };
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; };
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; };
FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; };
@ -817,6 +818,7 @@
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; };
FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; };
FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; };
FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; };
@ -910,6 +912,9 @@
FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; };
FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; };
FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; };
FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */; };
FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; };
FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */; };
FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; };
FDFC4E1929F1F9A600992FB6 /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */; };
FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; };
@ -1802,6 +1807,7 @@
FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = "<group>"; };
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = "<group>"; };
FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = "<group>"; };
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = "<group>"; };
FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = "<group>"; };
@ -1958,6 +1964,7 @@
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = "<group>"; };
FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = "<group>"; };
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = "<group>"; };
@ -2054,6 +2061,9 @@
FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = "<group>"; };
FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = "<group>"; };
FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserPoller.swift; sourceTree = "<group>"; };
FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = "<group>"; };
FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMetadata.swift; sourceTree = "<group>"; };
FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeSpec.swift; sourceTree = "<group>"; };
FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */ = {isa = PBXFileReference; explicitFileType = wrapper.xcframework; includeInIndex = 0; path = "libsession-util.xcframework"; sourceTree = BUILD_DIR; };
FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGeneralCache.swift; sourceTree = "<group>"; };
@ -3612,6 +3622,7 @@
FD09796527F6B0A800936362 /* Utilities */ = {
isa = PBXGroup;
children = (
FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */,
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
FD09796A27F6C67500936362 /* Failable.swift */,
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
@ -4045,6 +4056,7 @@
FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */,
FD9B30F1293EA0AF008DEE3E /* Networking */,
FDFBB7522A2023DE00CA7350 /* Utilities */,
);
path = SessionUtilitiesKitTests;
sourceTree = "<group>";
@ -4163,6 +4175,7 @@
isa = PBXGroup;
children = (
FDC13D482A16EC20007267C7 /* Service.swift */,
FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */,
FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */,
);
path = Types;
@ -4225,9 +4238,11 @@
FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */,
FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */,
FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */,
FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */,
FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */,
FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */,
FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */,
FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */,
);
path = Models;
sourceTree = "<group>";
@ -4425,6 +4440,14 @@
path = Models;
sourceTree = "<group>";
};
FDFBB7522A2023DE00CA7350 /* Utilities */ = {
isa = PBXGroup;
children = (
FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FDFDE122282D04E30098B17F /* Transitions */ = {
isa = PBXGroup;
children = (
@ -5644,6 +5667,7 @@
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */,
FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */,
FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */,
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */,
FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */,
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
@ -5758,6 +5782,7 @@
FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */,
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */,
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */,
FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */,
FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */,
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */,
7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */,
@ -5771,6 +5796,7 @@
FD245C57285065F100B966DD /* Poller.swift in Sources */,
FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */,
FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */,
FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */,
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */,
FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */,
@ -5808,6 +5834,7 @@
FDC6D6F32860607300B04575 /* Environment.swift in Sources */,
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */,
FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */,
B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */,
FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */,
FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */,
@ -6155,6 +6182,7 @@
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,

View File

@ -24,6 +24,7 @@ public protocol SodiumType {
public protocol AeadXChaCha20Poly1305IetfType {
var KeyBytes: Int { get }
var ABytes: Int { get }
var NonceBytes: Int { get }
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?

View File

@ -0,0 +1,47 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension PushNotificationAPI {
struct NotificationMetadata: Codable {
private enum CodingKeys: String, CodingKey {
case accountId = "@"
case hash = "#"
case namespace = "n"
case dataLength = "l"
case dataTooLong = "B"
}
/// Account ID (such as Session ID or closed group ID) where the message arrived.
let accountId: String
/// The hash of the message in the swarm.
let hash: String
/// The swarm namespace in which this message arrived.
let namespace: Int
/// The length of the message data. This is always included, even if the message content
/// itself was too large to fit into the push notification.
let dataLength: Int
/// This will be `true` if the data was omitted because it was too long to fit in a push
/// notification (around 2.5kB of raw data), in which case the push notification includes
/// only this metadata but not the message content itself.
let dataTooLong: Bool
}
}
extension PushNotificationAPI.NotificationMetadata {
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self = PushNotificationAPI.NotificationMetadata(
accountId: try container.decode(String.self, forKey: .accountId),
hash: try container.decode(String.self, forKey: .hash),
namespace: try container.decode(Int.self, forKey: .namespace),
dataLength: try container.decode(Int.self, forKey: .dataLength),
dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false)
)
}
}

View File

@ -54,13 +54,13 @@ public enum PushNotificationAPI {
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
.defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType)
let request: SubscribeRequest = SubscribeRequest(
pubkey: currentUserPublicKey,
namespaces: [.default],
includeMessageData: (previewType == .nameAndPreview), // TODO: Test resubscribing when changing the type
// Note: Unfortunately we always need the message content because without the content
// control messages can't be distinguished from visible messages which results in the
// 'generic' notification being shown when receiving things like typing indicator updates
includeMessageData: true,
serviceInfo: SubscribeRequest.ServiceInfo(
token: hexEncodedToken
),
@ -349,14 +349,6 @@ public enum PushNotificationAPI {
) -> AnyPublisher<Void, Error> {
let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs]
// TODO: Need to validate if this is actually desired behaviour - would this check prevent the app from unsubscribing if the user switches off fast mode??? (this is what the app is currently doing)
// TODO: This flag seems like it might actually be buggy... should double check it
guard isUsingFullAPNs else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
return PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
@ -385,11 +377,70 @@ public enum PushNotificationAPI {
.map { _ in () }
.eraseToAnyPublisher()
}
// MARK: - Notification Handling
public static func processNotification(
notificationContent: UNNotificationContent,
dependencies: SMKDependencies = SMKDependencies()
) -> (envelope: SNProtoEnvelope?, result: ProcessResult) {
// Make sure the notification is from the updated push server
guard notificationContent.userInfo["spns"] != nil else {
guard
let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String,
let data: Data = Data(base64Encoded: base64EncodedData),
let envelope: SNProtoEnvelope = try? MessageWrapper.unwrap(data: data)
else { return (nil, .legacyFailure) }
// We only support legacy notifications for legacy group conversations
guard envelope.type == .closedGroupMessage else { return (envelope, .legacyForceSilent) }
return (envelope, .legacySuccess)
}
guard
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
let encData: Data = Data(base64Encoded: base64EncodedEncString),
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(),
encData.count > dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes
else { return (nil, .failure) }
let nonce: Data = encData[0..<dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes]
let payload: Data = encData[dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes...]
guard
let paddedData: [UInt8] = dependencies.aeadXChaCha20Poly1305Ietf.decrypt(
authenticatedCipherText: payload.bytes,
secretKey: notificationsEncryptionKey.bytes,
nonce: nonce.bytes
)
else { return (nil, .failure) }
let decryptedData: Data = Data(paddedData.reversed().drop(while: { $0 == 0 }).reversed())
// Decode the decrypted data
guard let notification: BencodeResponse<NotificationMetadata> = try? Bencode.decodeResponse(from: decryptedData) else {
return (nil, .failure)
}
// If the metadata says that the message was too large then we should show the generic
// notification (this is a valid case)
guard !notification.info.dataTooLong else { return (nil, .success) }
// Check that the body we were given is valid
guard
let notificationData: Data = notification.data,
notification.info.dataLength == notificationData.count,
let envelope = try? MessageWrapper.unwrap(data: notificationData)
else { return (nil, .failure) }
// Success, we have the notification content
return (envelope, .success)
}
// MARK: - Security
@discardableResult private static func getOrGenerateEncryptionKey() throws -> Data {
// TODO: May want to work this differently (will break after a phone restart if the device hasn't been unlocked yet)
do {
var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data(
forService: keychainService,

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension PushNotificationAPI {
enum ProcessResult {
case success
case failure
case legacySuccess
case legacyFailure
case legacyForceSilent
}
}

View File

@ -5,5 +5,6 @@ import Foundation
extension PushNotificationAPI {
enum Service: String, Codable {
case apns
case sandbox = "apns-sandbox" // Use for push notifications in Testnet
}
}

View File

@ -8,6 +8,7 @@ import Sodium
class MockAeadXChaCha20Poly1305Ietf: Mock<AeadXChaCha20Poly1305IetfType>, AeadXChaCha20Poly1305IetfType {
var KeyBytes: Int = 32
var ABytes: Int = 16
var NonceBytes: Int = 24
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? {
return accept(args: [message, secretKey, nonce, additionalData]) as? Bytes

View File

@ -57,12 +57,22 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
)
}
let (maybeEnvelope, result) = PushNotificationAPI.processNotification(
notificationContent: notificationContent
)
guard
let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String,
let data: Data = Data(base64Encoded: base64EncodedData),
let envelope = try? MessageWrapper.unwrap(data: data)
(result == .success || result == .legacySuccess),
let envelope: SNProtoEnvelope = maybeEnvelope
else {
return self.handleFailure(for: notificationContent)
switch result {
// If we got an explicit failure, or we got a success but no content then show
// the fallback notification
case .success, .legacySuccess, .failure, .legacyFailure:
return self.handleFailure(for: notificationContent)
case .legacyForceSilent: return
}
}
// HACK: It is important to use write synchronously here to avoid a race condition

View File

@ -784,50 +784,15 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
public static func process(bencodedData data: Data) -> (info: ResponseInfoType, body: Data?)? {
// 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 {
guard let response: BencodeResponse<HTTP.ResponseInfo> = try? Bencode.decodeResponse(from: data) else {
return nil
}
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 nil
}
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: HTTP.ResponseInfo = try? JSONDecoder().decode(HTTP.ResponseInfo.self, from: infoStringData) else {
return nil
}
// Custom handle a clock out of sync error (v4 returns '425' but included the '406' just
// in case)
guard responseInfo.code != 406 && responseInfo.code != 425 else { return nil }
guard responseInfo.code != 401 else { return nil }
guard response.info.code != 406 && response.info.code != 425 else { return nil }
guard response.info.code != 401 else { return nil }
// If there is no data in the response then just return the ResponseInfo
guard responseString.count > "l\(infoLength)\(infoString)e".count else {
return (responseInfo, nil)
}
// 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]), let suffixData: Data = "e".data(using: .utf8) else {
return nil
}
let dataBytes: Array<UInt8> = Array(data)
let dataEndIndex: Int = (dataBytes.count - suffixData.count)
let dataStartIndex: Int = (dataEndIndex - finalDataLength)
let finalDataBytes: ArraySlice<UInt8> = dataBytes[dataStartIndex..<dataEndIndex]
let finalData: Data = Data(finalDataBytes)
return (responseInfo, finalData)
return (response.info, response.data)
}
}

View File

@ -45,6 +45,7 @@ public final class JobRunner {
private static let blockingQueue: Atomic<JobQueue?> = Atomic(
JobQueue(
type: .blocking,
executionType: .serial,
qos: .default,
jobVariants: [],
onQueueDrained: {
@ -85,6 +86,7 @@ public final class JobRunner {
)
let attachmentDownloadQueue: JobQueue = JobQueue(
type: .attachmentDownload,
executionType: .serial,
qos: .utility,
jobVariants: [
jobVariants.remove(.attachmentDownload)
@ -92,6 +94,7 @@ public final class JobRunner {
)
let generalQueue: JobQueue = JobQueue(
type: .general(number: 0),
executionType: .serial,
qos: .utility,
jobVariants: Array(jobVariants)
)
@ -509,7 +512,7 @@ private final class JobQueue {
init(
type: QueueType,
executionType: ExecutionType = .serial,
executionType: ExecutionType,
qos: DispatchQoS,
jobVariants: [Job.Variant],
onQueueDrained: (() -> ())? = nil

View File

@ -0,0 +1,263 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public protocol BencodableType {
associatedtype ValueType: BencodableType
static var isCollection: Bool { get }
static var isDictionary: Bool { get }
}
public struct BencodeResponse<T: Codable> {
public let info: T
public let data: Data?
}
extension BencodeResponse: Equatable where T: Equatable {}
public enum Bencode {
private enum Element: Character {
case number0 = "0"
case number1 = "1"
case number2 = "2"
case number3 = "3"
case number4 = "4"
case number5 = "5"
case number6 = "6"
case number7 = "7"
case number8 = "8"
case number9 = "9"
case intIndicator = "i"
case listIndicator = "l"
case dictIndicator = "d"
case endIndicator = "e"
case separator = ":"
init?(_ byte: UInt8?) {
guard
let byte: UInt8 = byte,
let byteString: String = String(data: Data([byte]), encoding: .utf8),
let character: Character = byteString.first,
let result: Element = Element(rawValue: character)
else { return nil }
self = result
}
}
private struct BencodeString {
let value: String?
let rawValue: Data
}
// MARK: - Functions
public static func decodeResponse<T>(
from data: Data,
using dependencies: Dependencies = Dependencies()
) throws -> BencodeResponse<T> where T: Decodable {
guard
let result: [Data] = try? decode([Data].self, from: data),
let responseData: Data = result.first
else { throw HTTPError.parsingFailed }
return BencodeResponse(
info: try responseData.decoded(as: T.self, using: dependencies),
data: (result.count > 1 ? result.last : nil)
)
}
public static func decode<T: BencodableType>(_ type: T.Type, from data: Data) throws -> T {
guard
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
decodedData.remainingData.isEmpty == true // Ensure there is no left over data
else { throw HTTPError.parsingFailed }
return try recursiveCast(type, from: decodedData.value)
}
// MARK: - Logic
private static func decodeData(_ data: Data) -> (value: Any, remainingData: Data)? {
switch Element(data.first) {
case .number0, .number1, .number2, .number3, .number4,
.number5, .number6, .number7, .number8, .number9:
return decodeString(data)
case .intIndicator: return decodeInt(data)
case .listIndicator: return decodeList(data)
case .dictIndicator: return decodeDict(data)
default: return nil
}
}
/// Decode a string element from iterator assumed to have structure `{length}:{data}`
private static func decodeString(_ data: Data) -> (value: BencodeString, remainingData: Data)? {
var mutableData: Data = data
var lengthData: [UInt8] = []
// Remove bytes until we hit the separator
while let next: UInt8 = mutableData.popFirst(), Element(next) != .separator {
lengthData.append(next)
}
// Need to reset the index of the data (it maintains the index after popping/slicing)
// See https://forums.swift.org/t/data-subscript/57195 for more info
mutableData = Data(mutableData)
guard
let lengthString: String = String(data: Data(lengthData), encoding: .ascii),
let length: Int = Int(lengthString, radix: 10),
mutableData.count >= length
else { return nil }
// Need to reset the index of the data (it maintains the index after popping/slicing)
// See https://forums.swift.org/t/data-subscript/57195 for more info
return (
BencodeString(
value: String(data: mutableData[0..<length], encoding: .ascii),
rawValue: mutableData[0..<length]
),
Data(mutableData.dropFirst(length))
)
}
/// Decode an int element from iterator assumed to have structure `i{int}e`
private static func decodeInt(_ data: Data) -> (value: Int, remainingData: Data)? {
var mutableData: Data = data
var intData: [UInt8] = []
_ = mutableData.popFirst() // drop `i`
// Pop until after `e`
while let next: UInt8 = mutableData.popFirst(), Element(next) != .endIndicator {
intData.append(next)
}
guard
let intString: String = String(data: Data(intData), encoding: .ascii),
let result: Int = Int(intString, radix: 10)
else { return nil }
// Need to reset the index of the data (it maintains the index after popping/slicing)
// See https://forums.swift.org/t/data-subscript/57195 for more info
return (result, Data(mutableData))
}
/// Decode a list element from iterator assumed to have structure `l{data}e`
private static func decodeList(_ data: Data) -> ([Any], Data)? {
var mutableData: Data = data
var listElements: [Any] = []
_ = mutableData.popFirst() // drop `l`
while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator {
guard let result = decodeData(mutableData) else { break }
listElements.append(result.value)
mutableData = result.remainingData
}
_ = mutableData.popFirst() // drop `e`
// Need to reset the index of the data (it maintains the index after popping/slicing)
// See https://forums.swift.org/t/data-subscript/57195 for more info
return (listElements, Data(mutableData))
}
/// Decode a dict element from iterator assumed to have structure `d{data}e`
private static func decodeDict(_ data: Data) -> ([String: Any], Data)? {
var mutableData: Data = data
var dictElements: [String: Any] = [:]
_ = mutableData.popFirst() // drop `d`
while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator {
guard
let keyResult = decodeString(mutableData),
let key: String = keyResult.value.value,
let valueResult = decodeData(keyResult.remainingData)
else { return nil }
dictElements[key] = valueResult.value
mutableData = valueResult.remainingData
}
_ = mutableData.popFirst() // drop `e`
// Need to reset the index of the data (it maintains the index after popping/slicing)
// See https://forums.swift.org/t/data-subscript/57195 for more info
return (dictElements, Data(mutableData))
}
// MARK: - Internal Functions
private static func recursiveCast<T: BencodableType>(_ type: T.Type, from value: Any) throws -> T {
switch (type.isCollection, type.isDictionary) {
case (_, true):
guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed }
return try (
dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
case (true, _):
guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed }
return try (
arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
default:
switch (value, type) {
case (let bencodeString as BencodeString, is String.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, is Optional<String>.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, _):
return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }())
default: return try (value as? T ?? { throw HTTPError.parsingFailed }())
}
}
}
}
// MARK: - BencodableType Extensions
extension Data: BencodableType {
public typealias ValueType = Data
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Int: BencodableType {
public typealias ValueType = Int
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension String: BencodableType {
public typealias ValueType = String
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Array: BencodableType where Element: BencodableType {
public typealias ValueType = Element
public static var isCollection: Bool { true }
public static var isDictionary: Bool { false }
}
extension Dictionary: BencodableType where Key == String, Value: BencodableType {
public typealias ValueType = Value
public static var isCollection: Bool { false }
public static var isDictionary: Bool { true }
}

View File

@ -0,0 +1,97 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionUtilitiesKit
class BencodeSpec: QuickSpec {
struct TestType: Codable, Equatable {
let intValue: Int
let stringValue: String
}
// MARK: - Spec
override func spec() {
describe("Bencode") {
context("when decoding") {
it("should decode a basic string") {
let basicStringData: Data = "5:howdy".data(using: .utf8)!
let result = try? Bencode.decode(String.self, from: basicStringData)
expect(result).to(equal("howdy"))
}
it("should decode a basic integer") {
let basicIntegerData: Data = "i3e".data(using: .utf8)!
let result = try? Bencode.decode(Int.self, from: basicIntegerData)
expect(result).to(equal(3))
}
it("should decode a list of integers") {
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
let result = try? Bencode.decode([Int].self, from: basicIntListData)
expect(result).to(equal([1, 2]))
}
it("should decode a basic dict") {
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
expect(result).to(equal(["spam": ["a", "b"]]))
}
}
context("when decoding a response") {
it("decodes successfully") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
}
it("decodes successfully with no body") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
}
it("throws a parsing error when invalid") {
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
}
}
}