|
|
/* * Copyright 2017 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
#import "Firebase/Messaging/FIRMessagingDataMessageManager.h"
#import "Firebase/Messaging/Protos/GtalkCore.pbobjc.h"
#import "Firebase/Messaging/FIRMessagingClient.h" #import "Firebase/Messaging/FIRMessagingConnection.h" #import "Firebase/Messaging/FIRMessagingConstants.h" #import "Firebase/Messaging/FIRMessagingDefines.h" #import "Firebase/Messaging/FIRMessagingDelayedMessageQueue.h" #import "Firebase/Messaging/FIRMessagingLogger.h" #import "Firebase/Messaging/FIRMessagingReceiver.h" #import "Firebase/Messaging/FIRMessagingRmqManager.h" #import "Firebase/Messaging/FIRMessaging_Private.h" #import "Firebase/Messaging/FIRMessagingSyncMessageManager.h" #import "Firebase/Messaging/FIRMessagingUtilities.h" #import "Firebase/Messaging/NSError+FIRMessaging.h"
static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k static const int kMinDelaySeconds = 1; // 1 second static const int kMaxDelaySeconds = 60 * 60; // 1 hour
static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com"; static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice"; // TODO: Update Gcm to FIRMessaging in the constants below static NSString *const kFCMMessageCategory = @"com.google.gcm"; static NSString *const kMessageReservedPrefix = @"google.";
static NSString *const kFCMMessageSpecialMessage = @"message_type";
// special messages sent by the server static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
static NSString *const kMCSNotificationPrefix = @"gcm.notification."; static NSString *const kDataMessageNotificationKey = @"notification";
typedef NS_ENUM(int8_t, UpstreamForceReconnect) { // Never force reconnect on upstream messages kUpstreamForceReconnectOff = 0, // Force reconnect for TTL=0 upstream messages kUpstreamForceReconnectTTL0 = 1, // Force reconnect for all upstream messages kUpstreamForceReconnectAll = 2, };
@interface FIRMessagingDataMessageManager ()
@property(nonatomic, readwrite, weak) FIRMessagingClient *client; @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager; @property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager; @property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate; @property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
@property(nonatomic, readwrite, assign) int ttl; @property(nonatomic, readwrite, copy) NSString *deviceAuthID; @property(nonatomic, readwrite, copy) NSString *secretToken; @property(nonatomic, readwrite, assign) int maxAppDataSize; @property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
@end
@implementation FIRMessagingDataMessageManager
- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate client:(FIRMessagingClient *)client rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager { self = [super init]; if (self) { _delegate = delegate; _client = client; _rmq2Manager = rmq2Manager; _syncMessageManager = syncMessageManager; _ttl = kFIRMessagingSendTtlDefault; _maxAppDataSize = kMaxAppDataSizeDefault; // on by default _upstreamForceReconnect = kUpstreamForceReconnectAll; } return self; }
- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken { if (deviceAuthID.length == 0 || secretToken.length == 0) { FIRMessagingLoggerWarn(kFIRMessagingMessageCodeDataMessageManager013, @"Invalid credentials: deviceAuthID: %@, secrectToken: %@", deviceAuthID, secretToken); } self.deviceAuthID = deviceAuthID; self.secretToken = secretToken; }
- (void)refreshDelayedMessages { FIRMessaging_WEAKIFY(self); self.delayedMessagesQueue = [[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager sendDelayedMessagesHandler:^(NSArray *messages) { FIRMessaging_STRONGIFY(self); [self sendDelayedMessages:messages]; }]; }
- (nullable NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage { NSString *category = dataMessage.category; NSString *from = dataMessage.from; if ([kFCMMessageCategory isEqualToString:category] || [kGSFMessageCategory isEqualToString:category]) { [self handleMCSDataMessage:dataMessage]; return nil; } else if ([kFromForFIRMessagingMessages isEqualToString:from]) { [self handleMCSDataMessage:dataMessage]; return nil; }
return [self parseDataMessage:dataMessage]; }
- (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000, @"Received message for FIRMessaging from downstream %@", dataMessage); }
- (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage { NSMutableDictionary *message = [NSMutableDictionary dictionary]; NSString *from = [dataMessage from]; if (from.length) { message[kFIRMessagingFromKey] = from; }
// raw data NSData *rawData = [dataMessage rawData]; if (rawData.length) { message[kFIRMessagingRawDataKey] = rawData; }
NSString *token = [dataMessage token]; if (token.length) { message[kFIRMessagingCollapseKey] = token; }
// Add the persistent_id. This would be removed later before sending the message to the device. NSString *persistentID = [dataMessage persistentId]; if (persistentID.length) { message[kFIRMessagingMessageIDKey] = persistentID; }
// third-party data for (GtalkAppData *item in dataMessage.appDataArray) {
// do not process the "from" key -- is not useful if ([kFIRMessagingFromKey isEqualToString:item.key]) { continue; }
// Filter the "gcm.notification." keys in the message if ([item.key hasPrefix:kMCSNotificationPrefix]) { NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]]; if ([key length]) { if (!message[kDataMessageNotificationKey]) { message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary]; } message[kDataMessageNotificationKey][key] = item.value; } else { FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001, @"Invalid key in MCS message: %@", key); } continue; }
// Filter the "gcm.duplex" key if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) { BOOL value = [item.value boolValue]; message[kFIRMessagingMessageSyncViaMCSKey] = @(value); continue; }
// do not allow keys with "reserved" keyword if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) { continue; }
[message setObject:item.value forKey:item.key]; } // TODO: Add support for encrypting raw data later return [NSDictionary dictionaryWithDictionary:message]; }
- (void)didReceiveParsedMessage:(NSDictionary *)message { if ([message[kFCMMessageSpecialMessage] length]) { NSString *messageType = message[kFCMMessageSpecialMessage]; if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) { // TODO: Maybe trim down message to remove some unnecessary fields. // tell the FCM receiver of deleted messages [self.delegate didDeleteMessagesOnServer]; return; } FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002, @"Invalid message type received: %@", messageType); } else if (message[kFIRMessagingMessageSyncViaMCSKey]) { // Update SYNC_RMQ with the message BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message]; if (isDuplicate) { return; } } NSString *messageId = message[kFIRMessagingMessageIDKey]; NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message]; [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId]; }
- (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message { NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message]; for (NSString *key in message) { if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) { [newMessage removeObjectForKey:key]; } } return [newMessage copy]; }
- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage { NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL]; NSString *to = dataMessage[kFIRMessagingSendTo]; NSString *msgId = dataMessage[kFIRMessagingSendMessageID]; NSString *appPackage = [self categoryForUpstreamMessages]; GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
// TODO: enforce TTL (right now only ttl=0 is special, means no storage) int ttl = [ttlNumber intValue]; if (ttl < 0 || ttl > self.ttl) { ttl = self.ttl; } [stanza setTtl:ttl]; [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
int delay = [self delayForMessage:dataMessage]; if (delay > 0) { [stanza setMaxDelay:delay]; }
if (msgId) { [stanza setId_p:msgId]; }
// collapse key as given by the sender NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey]; if ([token length]) { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003, @"FIRMessaging using %@ as collapse key", token); [stanza setToken:token]; }
if (!self.secretToken) { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004, @"Trying to send data message without a secret token. " @"Authentication failed."); [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorCodeMissingDeviceID]; return; }
if (![to length]) { [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo]; return; } [stanza setTo:to]; [stanza setCategory:appPackage]; // required field in the proto this is set by the server // set it to a sentinel so the runtime doesn't throw an exception [stanza setFrom:@""];
// MCS itself would set the registration ID // [stanza setRegId:nil];
int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza]; if (size > kMaxAppDataSizeDefault) { [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded]; return; }
BOOL useRmq = (ttl != 0) && (msgId != nil); if (useRmq) { [self.rmq2Manager saveRmqMessage:stanza withCompletionHandler:^(BOOL success) { if (!success) { [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave]; return; } [self willSendDataMessageSuccess:stanza withMessageId:msgId]; }]; }
// if delay > 0 we don't really care about sending the message right now // so we piggy-back on any other urgent(delay = 0) message that we are sending if (delay > 0 && [self delayMessage:stanza]) { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@", dataMessage); return; } // send delayed messages [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
BOOL sending = [self tryToSendDataMessageStanza:stanza]; if (!sending) { if (useRmq) { NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]]; FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event); } else { [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorCodeNetwork]; return; } } }
- (void)sendDelayedMessages:(NSArray *)delayedMessages { for (GtalkDataMessageStanza *message in delayedMessages) { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008, @"%@ Sending delayed message %@", @"DMM", message); [message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)]; [self tryToSendDataMessageStanza:message]; } }
- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message { NSString *msgId = [message id_p] ?: @""; [self.delegate didSendDataMessageWithID:msgId]; }
- (void)addParamWithKey:(NSString *)key value:(NSString *)val toStanza:(GtalkDataMessageStanza *)stanza { if (!key || !val) { return; } GtalkAppData *appData = [[GtalkAppData alloc] init]; [appData setKey:key]; [appData setValue:val]; [[stanza appDataArray] addObject:appData]; }
/** @return The size of the data being added to stanza. */ - (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza { int size = 0; for (NSString *key in data) { NSObject *val = data[key]; if ([val isKindOfClass:[NSString class]]) { NSString *strVal = (NSString *)val; [self addParamWithKey:key value:strVal toStanza:stanza]; size += [key length] + [strVal length]; } else if ([val isKindOfClass:[NSNumber class]]) { NSString *strVal = [(NSNumber *)val stringValue]; [self addParamWithKey:key value:strVal toStanza:stanza]; size += [key length] + [strVal length]; } else if ([kFIRMessagingRawDataKey isEqualToString:key] && [val isKindOfClass:[NSData class]]) { NSData *rawData = (NSData *)val; [stanza setRawData:[rawData copy]]; size += [rawData length]; } else { FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@", key); } } return size; }
/** * Notify the messenger that send data message completed with success. This is called for * TTL=0, after the message has been sent, or when message is saved, to unlock the send() * method. */ - (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza withMessageId:(NSString *)messageId { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010, @"send message success: %@", messageId); [self.delegate willSendDataMessageWithID:messageId error:nil]; }
/** * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type' * extra - same as 'message deleted'. * * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc) * we throw IOExceptions */ - (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza withMessageId:(NSString *)messageId error:(FIRMessagingInternalErrorCode)errorCode { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011, @"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
NSError *error = [NSError errorWithFCMErrorCode:errorCode]; if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) { [self.delegate willSendDataMessageWithID:messageId error:error]; } }
- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection { NSMutableString *rmqIdsResent = [NSMutableString string]; NSMutableArray *toRemoveRmqIds = [NSMutableArray array]; FIRMessaging_WEAKIFY(self); FIRMessaging_WEAKIFY(connection);
[self.rmq2Manager scanWithRmqMessageHandler:^(NSDictionary *messages) { FIRMessaging_STRONGIFY(self); FIRMessaging_STRONGIFY(connection); for (NSString *rmqID in messages) { GPBMessage *proto = messages[rmqID]; if ([proto isKindOfClass:GtalkDataMessageStanza.class]) { GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto; if (![self handleExpirationForDataMessage:stanza]) { // time expired let's delete from RMQ [toRemoveRmqIds addObject:stanza.persistentId]; continue; } [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]]; } [connection sendProto:proto]; } if ([rmqIdsResent length]) { FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@", rmqIdsResent); } if ([toRemoveRmqIds count]) { [self.rmq2Manager removeRmqMessagesWithRmqIds:[toRemoveRmqIds copy]]; } }]; }
/** * Check the TTL and generate an error if needed. * * @return false if the message needs to be deleted */ - (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message { if (message.ttl == 0) { return NO; }
int64_t now = FIRMessagingCurrentTimestampInSeconds(); if (now > message.sent + message.ttl) { [self willSendDataMessageFail:message withMessageId:message.id_p error:kFIRMessagingErrorServiceNotAvailable]; return NO; } return YES; }
#pragma mark - Private
- (int)delayForMessage:(NSMutableDictionary *)message { int delay = 0; // default if (message[kFIRMessagingSendDelay]) { delay = [message[kFIRMessagingSendDelay] intValue]; [message removeObjectForKey:kFIRMessagingSendDelay]; if (delay < kMinDelaySeconds) { delay = 0; } else if (delay > kMaxDelaySeconds) { delay = kMaxDelaySeconds; } } return delay; }
// return True if successfully delayed else False - (BOOL)delayMessage:(GtalkDataMessageStanza *)message { return [self.delayedMessagesQueue queueMessage:message]; }
- (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza { if (self.client.isConnectionActive) { [self.client sendMessage:stanza]; return YES; }
// if we only reconnect for TTL = 0 messages check if we ttl = 0 or // if we reconnect for all messages try to reconnect if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) || self.upstreamForceReconnect == kUpstreamForceReconnectAll) { BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable]; if (isNetworkAvailable) { if (stanza.ttl == 0) { // Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are // persisted, and will be sent from the RMQ. [self.client sendOnConnectOrDrop:stanza]; }
[self.client retryConnectionImmediately:YES]; return YES; } } return NO; }
- (NSString *)categoryForUpstreamMessages { return FIRMessagingAppIdentifier(); }
@end
|