diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -1074,11 +1074,10 @@ native: NEXT_CODE_VERSION, }); - let blobHash, encryptionKey, blobUploadError; + let blobHash, blobHolder, encryptionKey, blobUploadError; if (canQueryBlobService) { - ({ blobHash, encryptionKey, blobUploadError } = await blobServiceUpload( - copyWithMessageInfos.compile(), - )); + ({ blobHash, blobHolder, encryptionKey, blobUploadError } = + await blobServiceUpload(copyWithMessageInfos.compile())); } if (blobUploadError) { @@ -1091,6 +1090,7 @@ if (blobHash && encryptionKey) { notification.payload = { blobHash, + blobHolder, encryptionKey, ...notification.payload, }; diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -399,6 +399,7 @@ async function blobServiceUpload(payload: string): Promise< | { + +blobHolder: string, +blobHash: string, +encryptionKey: string, } @@ -414,6 +415,7 @@ }); return { blobHash: encryptedPayloadHash, + blobHolder, encryptionKey, }; } catch (e) { diff --git a/native/ios/Comm/AppDelegate.mm b/native/ios/Comm/AppDelegate.mm --- a/native/ios/Comm/AppDelegate.mm +++ b/native/ios/Comm/AppDelegate.mm @@ -40,6 +40,7 @@ #import "CommConstants.h" #import "CommCoreModule.h" +#import "CommIOSBlobClient.h" #import "CommMMKV.h" #import "CommRustModule.h" #import "CommUtilsModule.h" @@ -98,6 +99,7 @@ RCTAppSetupPrepareApp(application); [self moveMessagesToDatabase:NO]; + [self scheduleNSEBlobsDeletion]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; @@ -386,6 +388,7 @@ - (void)didReceiveNewMessageInfosNSNotification:(NSNotification *)notification { [self moveMessagesToDatabase:YES]; + [self scheduleNSEBlobsDeletion]; } - (void)registerForNewMessageInfosNotifications { @@ -404,6 +407,22 @@ CFNotificationSuspensionBehaviorDeliverImmediately); } +// NSE has limited time to process notifications. Therefore +// deferable and low priority networking such as fetched +// blob deletion from blob service should be handled by the +// main app on a low priority background thread. + +- (void)scheduleNSEBlobsDeletion { + dispatch_async( + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [CommIOSBlobClient.sharedInstance deleteStoredBlobs]; + }); +} + +- (void)applicationWillResignActive:(UIApplication *)application { + [[CommIOSBlobClient sharedInstance] cancelOngoingRequests]; +} + // Copied from // ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/OnLoad.cpp static ::hermes::vm::RuntimeConfig diff --git a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h --- a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h +++ b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.h @@ -5,4 +5,12 @@ - (void)getBlobSync:(NSString *)blobHash withSuccessHandler:(void (^)(NSData *))successHandler andFailureHandler:(void (^)(NSError *))failureHandler; +- (void)deleteBlobAsyncWithHash:(NSString *)blobHash + andHolder:(NSString *)blobHolder + withSuccessHandler:(void (^)())successHandler + andFailureHandler:(void (^)(NSError *))failureHandler; +- (void)storeBlobForDeletionWithHash:(NSString *)blobHash + andHolder:(NSString *)blobHolder; +- (void)deleteStoredBlobs; +- (void)cancelOngoingRequests; @end diff --git a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm --- a/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm +++ b/native/ios/Comm/CommIOSServices/CommIOSBlobClient.mm @@ -1,4 +1,5 @@ #import "CommIOSBlobClient.h" +#import "CommMMKV.h" #import "CommSecureStore.h" #import "Logger.h" @@ -10,6 +11,11 @@ #endif int const blobServiceQueryTimeLimit = 15; +const std::string mmkvBlobHolderPrefix = "BLOB_HOLDER."; +// The blob service expects slightly different keys in +// delete reuqest payload than we use in notif payload +NSString *const blobServiceHashKey = @"blob_hash"; +NSString *const blobServiceHolderKey = @"holder"; @interface CommIOSBlobClient () @property(nonatomic, strong) NSURLSession *sharedBlobServiceSession; @@ -102,6 +108,129 @@ (int64_t)(blobServiceQueryTimeLimit * NSEC_PER_SEC))); } +- (void)deleteBlobAsyncWithHash:(NSString *)blobHash + andHolder:(NSString *)blobHolder + withSuccessHandler:(void (^)())successHandler + andFailureHandler:(void (^)(NSError *))failureHandler { + NSError *authTokenError = nil; + NSString *authToken = + [CommIOSBlobClient _getAuthTokenOrSetError:&authTokenError]; + + if (authTokenError) { + comm::Logger::log( + "Failed to create blob service auth token. Reason: " + + std::string([authTokenError.localizedDescription UTF8String])); + return; + } + + NSString *blobUrlStr = [blobServiceAddress stringByAppendingString:@"/blob"]; + NSURL *blobUrl = [NSURL URLWithString:blobUrlStr]; + + NSMutableURLRequest *deleteRequest = + [NSMutableURLRequest requestWithURL:blobUrl]; + + [deleteRequest setValue:authToken forHTTPHeaderField:@"Authorization"]; + [deleteRequest setValue:@"application/json" + forHTTPHeaderField:@"content-type"]; + + deleteRequest.HTTPMethod = @"DELETE"; + deleteRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{ + blobServiceHolderKey : blobHolder, + blobServiceHashKey : blobHash + } + options:0 + error:nil]; + + NSURLSessionDataTask *task = [self.sharedBlobServiceSession + dataTaskWithRequest:deleteRequest + completionHandler:^( + NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + + if (httpResponse.statusCode > 299) { + NSString *errorMessage = + [@"Deleting blob failed with the following reason: " + stringByAppendingString:[NSHTTPURLResponse + localizedStringForStatusCode: + httpResponse.statusCode]]; + failureHandler([NSError + errorWithDomain:@"app.comm" + code:httpResponse.statusCode + userInfo:@{NSLocalizedDescriptionKey : errorMessage}]); + return; + } + + if (error) { + failureHandler(error); + return; + } + + successHandler(); + }]; + + [task resume]; +} + +- (void)storeBlobForDeletionWithHash:(NSString *)blobHash + andHolder:(NSString *)blobHolder { + std::string blobHashCpp = std::string([blobHash UTF8String]); + std::string blobHolderCpp = std::string([blobHolder UTF8String]); + + std::string mmkvBlobHolderKey = mmkvBlobHolderPrefix + blobHolderCpp; + comm::CommMMKV::setString(mmkvBlobHolderKey, blobHashCpp); +} + +- (void)deleteStoredBlobs { + std::vector allKeys = comm::CommMMKV::getAllKeys(); + NSMutableArray *blobsDataForDeletion = + [[NSMutableArray alloc] init]; + + for (const auto &key : allKeys) { + if (key.size() <= mmkvBlobHolderPrefix.size() || + key.compare(0, mmkvBlobHolderPrefix.size(), mmkvBlobHolderPrefix)) { + continue; + } + + std::optional blobHash = comm::CommMMKV::getString(key); + if (!blobHash.has_value()) { + continue; + } + std::string blobHolder = key.substr(mmkvBlobHolderPrefix.size()); + + NSString *blobHolderObjC = + [NSString stringWithCString:blobHolder.c_str() + encoding:NSUTF8StringEncoding]; + NSString *blobHashObjC = + [NSString stringWithCString:blobHash.value().c_str() + encoding:NSUTF8StringEncoding]; + + [self deleteBlobAsyncWithHash:blobHashObjC + andHolder:blobHolderObjC + withSuccessHandler:^{ + std::string mmkvBlobHolderKey = mmkvBlobHolderPrefix + blobHolder; + comm::CommMMKV::removeKeys({mmkvBlobHolderKey}); + } + andFailureHandler:^(NSError *error) { + comm::Logger::log( + "Failed to delete blob hash " + blobHash.value() + + " from blob service. Details: " + + std::string([error.localizedDescription UTF8String])); + }]; + } +} + +- (void)cancelOngoingRequests { + [self.sharedBlobServiceSession + getAllTasksWithCompletionHandler:^( + NSArray<__kindof NSURLSessionTask *> *_Nonnull tasks) { + for (NSURLSessionTask *task in tasks) { + [task cancel]; + } + }]; +} + + (NSString *)_getAuthTokenOrSetError:(NSError **)error { // Authentication data are retrieved on every request // since they might change while NSE process is running diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -17,6 +17,7 @@ NSString *const collapseIDKey = @"collapseID"; NSString *const keyserverIDKey = @"keyserverID"; NSString *const blobHashKey = @"blobHash"; +NSString *const blobHolderKey = @"blobHolder"; NSString *const encryptionKeyLabel = @"encryptionKey"; // Those and future MMKV-related constants should match @@ -557,6 +558,9 @@ [NotificationService aesDecryptAndParse:largePayloadBinary withKey:encryptionKey]; [self persistMessagePayload:largePayload]; + [CommIOSBlobClient.sharedInstance + storeBlobForDeletionWithHash:blobHash + andHolder:content.userInfo[blobHolderKey]]; } - (BOOL)needsSilentBadgeUpdate:(NSDictionary *)payload { @@ -579,7 +583,8 @@ } - (BOOL)isLargeNotification:(NSDictionary *)payload { - return payload[blobHashKey] && payload[encryptionKeyLabel]; + return payload[blobHashKey] && payload[encryptionKeyLabel] && + payload[blobHolderKey]; } - (UNNotificationContent *)getBadgeOnlyContentFor: