Page MenuHomePhabricator

D8795.id30841.diff
No OneTemporary

D8795.id30841.diff

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
@@ -36,9 +36,8 @@
@interface NotificationService ()
-@property(nonatomic, strong) void (^contentHandler)
- (UNNotificationContent *contentToDeliver);
-@property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
+@property(strong) NSMutableDictionary *contentHandlers;
+@property(strong) NSMutableDictionary *contents;
@end
@@ -48,19 +47,22 @@
withContentHandler:
(void (^)(UNNotificationContent *_Nonnull))
contentHandler {
+ // Set-up methods are idempotent
[NotificationService setUpNSEProcess];
+ [self setUpNSEInstance];
- self.contentHandler = contentHandler;
- self.bestAttemptContent = [request.content mutableCopy];
+ NSString *contentHandlerKey = [request.identifier copy];
+ UNMutableNotificationContent *content = [request.content mutableCopy];
+ [self putContent:content withHandler:contentHandler forKey:contentHandlerKey];
- UNNotificationContent *publicUserContent = self.bestAttemptContent;
+ UNNotificationContent *publicUserContent = content;
// Step 1: notification decryption.
- if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo]) {
+ if ([self shouldBeDecrypted:content.userInfo]) {
std::string decryptErrorMessage;
try {
@try {
- [self decryptBestAttemptContent];
+ [self decryptContentInPlace:content];
} @catch (NSException *e) {
decryptErrorMessage = "NSE: Received Obj-C exception: " +
std::string([e.name UTF8String]) +
@@ -75,13 +77,12 @@
if (decryptErrorMessage.size()) {
NSString *errorMessage =
[NSString stringWithUTF8String:decryptErrorMessage.c_str()];
- [self callContentHandlerOnErrorMessage:errorMessage
- withPublicUserContent:[[UNNotificationContent alloc]
- init]];
+ [self callContentHandlerForKey:contentHandlerKey
+ onErrorMessage:errorMessage
+ withPublicUserContent:[[UNNotificationContent alloc] init]];
return;
}
- } else if ([self shouldAlertUnencryptedNotification:self.bestAttemptContent
- .userInfo]) {
+ } else if ([self shouldAlertUnencryptedNotification:content.userInfo]) {
// In future this will be replaced by notification content
// modification for DEV environment and staff members
comm::Logger::log("NSE: Received erroneously unencrypted notitication.");
@@ -93,7 +94,7 @@
std::string persistErrorMessage;
try {
@try {
- [self persistMessagePayload:self.bestAttemptContent.userInfo];
+ [self persistMessagePayload:content.userInfo];
} @catch (NSException *e) {
persistErrorMessage =
"Obj-C exception: " + std::string([e.name UTF8String]) +
@@ -113,12 +114,12 @@
// Message payload persistence is a higher priority task, so it has
// to happen prior to potential notification center clearing.
- if ([self isRescind:self.bestAttemptContent.userInfo]) {
+ if ([self isRescind:content.userInfo]) {
std::string rescindErrorMessage;
try {
@try {
[self
- removeNotificationWithIdentifier:self.bestAttemptContent
+ removeNotificationWithIdentifier:content
.userInfo[@"notificationId"]];
} @catch (NSException *e) {
rescindErrorMessage =
@@ -139,14 +140,16 @@
publicUserContent = [[UNNotificationContent alloc] init];
}
- if ([self isBadgeOnly:self.bestAttemptContent.userInfo]) {
+ if ([self isBadgeOnly:content.userInfo]) {
UNMutableNotificationContent *badgeOnlyContent =
[[UNMutableNotificationContent alloc] init];
- badgeOnlyContent.badge = self.bestAttemptContent.badge;
- self.bestAttemptContent = badgeOnlyContent;
+ badgeOnlyContent.badge = content.badge;
+ content = badgeOnlyContent;
publicUserContent = badgeOnlyContent;
}
+ // Step 5: notify main app that there is data
+ // to transfer to SQLite and redux.
[self sendNewMessageInfosNotification];
if (NSString *currentMemoryEventMessage =
@@ -157,43 +160,86 @@
if (errorMessages.count) {
NSString *cumulatedErrorMessage = [@"NSE: Received "
stringByAppendingString:[errorMessages componentsJoinedByString:@" "]];
- [self callContentHandlerOnErrorMessage:cumulatedErrorMessage
- withPublicUserContent:publicUserContent];
+ [self callContentHandlerForKey:contentHandlerKey
+ onErrorMessage:cumulatedErrorMessage
+ withPublicUserContent:publicUserContent];
return;
}
-
- self.contentHandler(self.bestAttemptContent);
+ [self callContentHandlerForKey:contentHandlerKey
+ withContent:publicUserContent];
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified
// content, otherwise the original push payload will be used.
- if ([self isRescind:self.bestAttemptContent.userInfo]) {
- // If we get to this place it means we were unable to
- // remove relevant notification from notification center in
- // in time given to NSE to process notification.
- // It is an extremely unlikely to happen.
- NSString *errorMessage =
- @"NSE: Exceeded time limit to rescind a notification.";
- [self
- callContentHandlerOnErrorMessage:errorMessage
- withPublicUserContent:[[UNNotificationContent alloc] init]];
- return;
+ NSMutableArray<void (^)(UNNotificationContent *_Nonnull)> *allHandlers =
+ [[NSMutableArray alloc] init];
+ NSMutableArray<UNNotificationContent *> *allContents =
+ [[NSMutableArray alloc] init];
+
+ @synchronized(self.contentHandlers) {
+ for (NSString *key in self.contentHandlers) {
+ [allHandlers addObject:self.contentHandlers[key]];
+ [allContents addObject:self.contents[key]];
+ }
+
+ [self.contentHandlers removeAllObjects];
+ [self.contents removeAllObjects];
}
- if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo] &&
- !self.bestAttemptContent.userInfo[@"succesfullyDecrypted"]) {
- // If we get to this place it means we were unable to
- // decrypt encrypted notification content in time
- // given to NSE to process notification.
- NSString *errorMessage =
- @"NSE: Exceeded time limit to decrypt a notification.";
- [self
- callContentHandlerOnErrorMessage:errorMessage
- withPublicUserContent:[[UNNotificationContent alloc] init]];
- return;
+
+ for (int i = 0; i < allContents.count; i++) {
+ UNNotificationContent *content = allContents[i];
+ void (^handler)(UNNotificationContent *_Nonnull) = allHandlers[i];
+
+ if ([self isRescind:content.userInfo]) {
+ // If we get to this place it means we were unable to
+ // remove relevant notification from notification center in
+ // in time given to NSE to process notification.
+ // It is an extremely unlikely to happen.
+ if (!comm::StaffUtils::isStaffRelease()) {
+ handler([[UNNotificationContent alloc] init]);
+ continue;
+ }
+
+ NSString *errorMessage =
+ @"NSE: Exceeded time limit to rescind a notification.";
+ UNNotificationContent *errorContent =
+ [self buildContentForError:errorMessage];
+ handler(errorContent);
+ continue;
+ }
+
+ if ([self shouldBeDecrypted:content.userInfo] &&
+ !content.userInfo[@"succesfullyDecrypted"]) {
+ // If we get to this place it means we were unable to
+ // decrypt encrypted notification content in time
+ // given to NSE to process notification.
+ if (!comm::StaffUtils::isStaffRelease()) {
+ handler([[UNNotificationContent alloc] init]);
+ continue;
+ }
+
+ NSString *errorMessage =
+ @"NSE: Exceeded time limit to decrypt a notification.";
+ UNNotificationContent *errorContent =
+ [self buildContentForError:errorMessage];
+ handler(errorContent);
+ continue;
+ }
+
+ // At this point we know that the content is at least
+ // correctly decrypted so we can display it to the user.
+ // Another operation, like persistence, had failed.
+ if ([self isBadgeOnly:content.userInfo]) {
+ UNNotificationContent *badgeOnlyContent =
+ [self getBadgeOnlyContentFor:content];
+ handler(badgeOnlyContent);
+ continue;
+ }
+
+ handler(content);
}
- self.contentHandler(self.bestAttemptContent);
}
- (void)removeNotificationWithIdentifier:(NSString *)identifier {
@@ -268,6 +314,14 @@
return !payload[@"threadID"];
}
+- (UNNotificationContent *)getBadgeOnlyContentFor:
+ (UNNotificationContent *)content {
+ UNMutableNotificationContent *badgeOnlyContent =
+ [[UNMutableNotificationContent alloc] init];
+ badgeOnlyContent.badge = content.badge;
+ return badgeOnlyContent;
+}
+
- (void)sendNewMessageInfosNotification {
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
@@ -297,17 +351,16 @@
.c_str()];
}
-- (void)decryptBestAttemptContent {
- NSString *decryptedSerializedPayload = [self
- singleDecrypt:self.bestAttemptContent.userInfo[encryptedPayloadKey]];
+- (void)decryptContentInPlace:(UNMutableNotificationContent *)content {
+ NSString *decryptedSerializedPayload =
+ [self singleDecrypt:content.userInfo[encryptedPayloadKey]];
NSDictionary *decryptedPayload = [NSJSONSerialization
JSONObjectWithData:[decryptedSerializedPayload
dataUsingEncoding:NSUTF8StringEncoding]
options:0
error:nil];
- NSMutableDictionary *mutableUserInfo =
- [self.bestAttemptContent.userInfo mutableCopy];
+ NSMutableDictionary *mutableUserInfo = [content.userInfo mutableCopy];
NSMutableDictionary *mutableAps = nil;
if (mutableUserInfo[@"aps"]) {
@@ -316,7 +369,7 @@
NSString *body = decryptedPayload[@"merged"];
if (body) {
- self.bestAttemptContent.body = body;
+ content.body = body;
if (mutableAps && mutableAps[@"alert"]) {
mutableAps[@"alert"] = body;
}
@@ -324,7 +377,7 @@
NSString *threadID = decryptedPayload[@"threadID"];
if (threadID) {
- self.bestAttemptContent.threadIdentifier = threadID;
+ content.threadIdentifier = threadID;
mutableUserInfo[@"threadID"] = threadID;
if (mutableAps) {
mutableAps[@"thread-id"] = threadID;
@@ -334,7 +387,7 @@
NSString *badgeStr = decryptedPayload[@"badge"];
if (badgeStr) {
NSNumber *badge = @([badgeStr intValue]);
- self.bestAttemptContent.badge = badge;
+ content.badge = badge;
if (mutableAps) {
mutableAps[@"badge"] = badge;
}
@@ -355,7 +408,54 @@
mutableUserInfo[@"aps"] = mutableAps;
}
[mutableUserInfo removeObjectForKey:encryptedPayloadKey];
- self.bestAttemptContent.userInfo = mutableUserInfo;
+ content.userInfo = mutableUserInfo;
+}
+
+// Apple documentation for NSE does not explicitly state
+// that single NSE instance will be used by only one thread
+// at a time. Even though UNNotificationServiceExtension API
+// suggests that it could be the case we don't trust it
+// and keep a synchronized collection of handlers and contents.
+// We keep reports of events that strongly suggest there is
+// parallelism in notifications processing. In particular we
+// have see notifications not being decrypted when access
+// to encryption keys had not been correctly implemented.
+// Similar behaviour is adopted by other apps such as Signal,
+// Telegram or Element.
+
+- (void)setUpNSEInstance {
+ @synchronized(self) {
+ if (self.contentHandlers) {
+ return;
+ }
+ self.contentHandlers = [[NSMutableDictionary alloc] init];
+ self.contents = [[NSMutableDictionary alloc] init];
+ }
+}
+
+- (void)putContent:(UNNotificationContent *)content
+ withHandler:(void (^)(UNNotificationContent *_Nonnull))handler
+ forKey:(NSString *)key {
+ @synchronized(self.contentHandlers) {
+ [self.contentHandlers setObject:handler forKey:key];
+ [self.contents setObject:content forKey:key];
+ }
+}
+
+- (void)callContentHandlerForKey:(NSString *)key
+ withContent:(UNNotificationContent *)content {
+ void (^handler)(UNNotificationContent *_Nonnull);
+
+ @synchronized(self.contentHandlers) {
+ handler = [self.contentHandlers objectForKey:key];
+ [self.contentHandlers removeObjectForKey:key];
+ [self.contents removeObjectForKey:key];
+ }
+
+ if (!handler) {
+ return;
+ }
+ handler(content);
}
- (UNNotificationContent *)buildContentForError:(NSString *)error {
@@ -365,18 +465,18 @@
return content;
}
-- (void)callContentHandlerOnErrorMessage:(NSString *)errorMessage
- withPublicUserContent:
- (UNNotificationContent *)publicUserContent {
+- (void)callContentHandlerForKey:(NSString *)key
+ onErrorMessage:(NSString *)errorMessage
+ withPublicUserContent:(UNNotificationContent *)publicUserContent {
comm::Logger::log(std::string([errorMessage UTF8String]));
if (!comm::StaffUtils::isStaffRelease()) {
- self.contentHandler(publicUserContent);
+ [self callContentHandlerForKey:key withContent:publicUserContent];
return;
}
UNNotificationContent *content = [self buildContentForError:errorMessage];
- self.contentHandler(content);
+ [self callContentHandlerForKey:key withContent:content];
}
// Monitor memory usage

File Metadata

Mime Type
text/plain
Expires
Fri, Nov 15, 5:40 PM (21 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2494946
Default Alt Text
D8795.id30841.diff (13 KB)

Event Timeline