2 * Copyright 2017 Google
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "FIRMessagingDataMessageManager.h"
19 #import "Protos/GtalkCore.pbobjc.h"
21 #import "FIRMessagingClient.h"
22 #import "FIRMessagingConnection.h"
23 #import "FIRMessagingConstants.h"
24 #import "FIRMessagingDefines.h"
25 #import "FIRMessagingDelayedMessageQueue.h"
26 #import "FIRMessagingLogger.h"
27 #import "FIRMessagingReceiver.h"
28 #import "FIRMessagingRmqManager.h"
29 #import "FIRMessaging_Private.h"
30 #import "FIRMessagingSyncMessageManager.h"
31 #import "FIRMessagingUtilities.h"
32 #import "NSError+FIRMessaging.h"
34 static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k
35 static const int kMinDelaySeconds = 1; // 1 second
36 static const int kMaxDelaySeconds = 60 * 60; // 1 hour
38 static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com";
39 static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice";
40 // TODO: Update Gcm to FIRMessaging in the constants below
41 static NSString *const kFCMMessageCategory = @"com.google.gcm";
42 static NSString *const kMessageReservedPrefix = @"google.";
44 static NSString *const kFCMMessageSpecialMessage = @"message_type";
46 // special messages sent by the server
47 static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
49 static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
50 static NSString *const kDataMessageNotificationKey = @"notification";
53 typedef NS_ENUM(int8_t, UpstreamForceReconnect) {
54 // Never force reconnect on upstream messages
55 kUpstreamForceReconnectOff = 0,
56 // Force reconnect for TTL=0 upstream messages
57 kUpstreamForceReconnectTTL0 = 1,
58 // Force reconnect for all upstream messages
59 kUpstreamForceReconnectAll = 2,
62 @interface FIRMessagingDataMessageManager ()
64 @property(nonatomic, readwrite, weak) FIRMessagingClient *client;
65 @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
66 @property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager;
67 @property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate;
68 @property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
70 @property(nonatomic, readwrite, assign) int ttl;
71 @property(nonatomic, readwrite, copy) NSString *deviceAuthID;
72 @property(nonatomic, readwrite, copy) NSString *secretToken;
73 @property(nonatomic, readwrite, assign) int maxAppDataSize;
74 @property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
78 @implementation FIRMessagingDataMessageManager
80 - (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
81 client:(FIRMessagingClient *)client
82 rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
83 syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
88 _rmq2Manager = rmq2Manager;
89 _syncMessageManager = syncMessageManager;
90 _ttl = kFIRMessagingSendTtlDefault;
91 _maxAppDataSize = kMaxAppDataSizeDefault;
93 _upstreamForceReconnect = kUpstreamForceReconnectAll;
98 - (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken {
99 _FIRMessagingDevAssert([deviceAuthID length] && [secretToken length],
100 @"Invalid credentials for FIRMessaging");
101 self.deviceAuthID = deviceAuthID;
102 self.secretToken = secretToken;
105 - (void)refreshDelayedMessages {
106 FIRMessaging_WEAKIFY(self);
107 self.delayedMessagesQueue =
108 [[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager
109 sendDelayedMessagesHandler:^(NSArray *messages) {
110 FIRMessaging_STRONGIFY(self);
111 [self sendDelayedMessages:messages];
115 - (nullable NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage {
116 NSString *category = dataMessage.category;
117 NSString *from = dataMessage.from;
118 if ([kFCMMessageCategory isEqualToString:category] ||
119 [kGSFMessageCategory isEqualToString:category]) {
120 [self handleMCSDataMessage:dataMessage];
122 } else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
123 [self handleMCSDataMessage:dataMessage];
127 return [self parseDataMessage:dataMessage];
130 - (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
131 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
132 @"Received message for FIRMessaging from downstream %@", dataMessage);
135 - (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
136 NSMutableDictionary *message = [NSMutableDictionary dictionary];
137 NSString *from = [dataMessage from];
139 message[kFIRMessagingFromKey] = from;
143 NSData *rawData = [dataMessage rawData];
144 if ([rawData length]) {
145 message[kFIRMessagingRawDataKey] = rawData;
148 NSString *token = [dataMessage token];
149 if ([token length]) {
150 message[kFIRMessagingCollapseKey] = token;
153 // Add the persistent_id. This would be removed later before sending the message to the device.
154 NSString *persistentID = [dataMessage persistentId];
155 _FIRMessagingDevAssert([persistentID length], @"Invalid MCS message without persistentID");
156 if ([persistentID length]) {
157 message[kFIRMessagingMessageIDKey] = persistentID;
161 for (GtalkAppData *item in dataMessage.appDataArray) {
162 _FIRMessagingDevAssert(item.hasKey && item.hasValue, @"Invalid AppData");
164 // do not process the "from" key -- is not useful
165 if ([kFIRMessagingFromKey isEqualToString:item.key]) {
169 // Filter the "gcm.notification." keys in the message
170 if ([item.key hasPrefix:kMCSNotificationPrefix]) {
171 NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
173 if (!message[kDataMessageNotificationKey]) {
174 message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
176 message[kDataMessageNotificationKey][key] = item.value;
178 _FIRMessagingDevAssert([key length], @"Invalid key in MCS message: %@", key);
179 FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
180 @"Invalid key in MCS message: %@", key);
185 // Filter the "gcm.duplex" key
186 if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
187 BOOL value = [item.value boolValue];
188 message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
192 // do not allow keys with "reserved" keyword
193 if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
197 [message setObject:item.value forKey:item.key];
199 // TODO: Add support for encrypting raw data later
200 return [NSDictionary dictionaryWithDictionary:message];
203 - (void)didReceiveParsedMessage:(NSDictionary *)message {
204 if ([message[kFCMMessageSpecialMessage] length]) {
205 NSString *messageType = message[kFCMMessageSpecialMessage];
206 if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) {
207 // TODO: Maybe trim down message to remove some unnecessary fields.
208 // tell the FCM receiver of deleted messages
209 [self.delegate didDeleteMessagesOnServer];
212 FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002,
213 @"Invalid message type received: %@", messageType);
214 } else if (message[kFIRMessagingMessageSyncViaMCSKey]) {
215 // Update SYNC_RMQ with the message
216 BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message];
221 NSString *messageId = message[kFIRMessagingMessageIDKey];
222 NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
223 [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
226 - (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message {
227 NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message];
228 for (NSString *key in message) {
229 if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) {
230 [newMessage removeObjectForKey:key];
233 return [newMessage copy];
236 - (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage {
237 NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL];
238 NSString *to = dataMessage[kFIRMessagingSendTo];
239 NSString *msgId = dataMessage[kFIRMessagingSendMessageID];
240 NSString *appPackage = [self categoryForUpstreamMessages];
241 GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
243 // TODO: enforce TTL (right now only ttl=0 is special, means no storage)
244 int ttl = [ttlNumber intValue];
245 if (ttl < 0 || ttl > self.ttl) {
249 [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
251 int delay = [self delayForMessage:dataMessage];
253 [stanza setMaxDelay:delay];
257 [stanza setId_p:msgId];
260 // collapse key as given by the sender
261 NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey];
262 if ([token length]) {
263 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003,
264 @"FIRMessaging using %@ as collapse key", token);
265 [stanza setToken:token];
268 if (!self.secretToken) {
269 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
270 @"Trying to send data message without a secret token. "
271 @"Authentication failed.");
272 [self willSendDataMessageFail:stanza
274 error:kFIRMessagingErrorCodeMissingDeviceID];
279 [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
283 [stanza setCategory:appPackage];
284 // required field in the proto this is set by the server
285 // set it to a sentinel so the runtime doesn't throw an exception
286 [stanza setFrom:@""];
288 // MCS itself would set the registration ID
289 // [stanza setRegId:nil];
291 int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
292 if (size > kMaxAppDataSizeDefault) {
293 [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
297 BOOL useRmq = (ttl != 0) && (msgId != nil);
299 if (!self.client.isConnected) {
300 // do nothing assuming rmq save is enabled
304 if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
305 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
306 [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
310 [self willSendDataMessageSuccess:stanza withMessageId:msgId];
313 // if delay > 0 we don't really care about sending the message right now
314 // so we piggy-back on any other urgent(delay = 0) message that we are sending
315 if (delay > 0 && [self delayMessage:stanza]) {
316 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@",
320 // send delayed messages
321 [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
323 BOOL sending = [self tryToSendDataMessageStanza:stanza];
326 NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
327 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
329 [self willSendDataMessageFail:stanza
331 error:kFIRMessagingErrorCodeNetwork];
337 - (void)sendDelayedMessages:(NSArray *)delayedMessages {
338 for (GtalkDataMessageStanza *message in delayedMessages) {
339 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008,
340 @"%@ Sending delayed message %@", @"DMM", message);
341 [message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)];
342 [self tryToSendDataMessageStanza:message];
346 - (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
347 NSString *msgId = [message id_p] ?: @"";
348 [self.delegate didSendDataMessageWithID:msgId];
351 - (void)addParamWithKey:(NSString *)key
352 value:(NSString *)val
353 toStanza:(GtalkDataMessageStanza *)stanza {
357 GtalkAppData *appData = [[GtalkAppData alloc] init];
358 [appData setKey:key];
359 [appData setValue:val];
360 [[stanza appDataArray] addObject:appData];
364 @return The size of the data being added to stanza.
366 - (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
368 for (NSString *key in data) {
369 NSObject *val = data[key];
370 if ([val isKindOfClass:[NSString class]]) {
371 NSString *strVal = (NSString *)val;
372 [self addParamWithKey:key value:strVal toStanza:stanza];
373 size += [key length] + [strVal length];
374 } else if ([val isKindOfClass:[NSNumber class]]) {
375 NSString *strVal = [(NSNumber *)val stringValue];
376 [self addParamWithKey:key value:strVal toStanza:stanza];
377 size += [key length] + [strVal length];
378 } else if ([kFIRMessagingRawDataKey isEqualToString:key] &&
379 [val isKindOfClass:[NSData class]]) {
380 NSData *rawData = (NSData *)val;
381 [stanza setRawData:[rawData copy]];
382 size += [rawData length];
384 FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
392 * Notify the messenger that send data message completed with success. This is called for
393 * TTL=0, after the message has been sent, or when message is saved, to unlock the send()
396 - (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
397 withMessageId:(NSString *)messageId {
398 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
399 @"send message success: %@", messageId);
400 [self.delegate willSendDataMessageWithID:messageId error:nil];
404 * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
405 * extra - same as 'message deleted'.
407 * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
408 * we throw IOExceptions
410 - (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza
411 withMessageId:(NSString *)messageId
412 error:(FIRMessagingInternalErrorCode)errorCode {
413 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011,
414 @"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
416 NSError *error = [NSError errorWithFCMErrorCode:errorCode];
417 if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
418 [self.delegate willSendDataMessageWithID:messageId error:error];
422 - (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection {
423 NSMutableString *rmqIdsResent = [NSMutableString string];
424 NSMutableArray *toRemoveRmqIds = [NSMutableArray array];
425 FIRMessaging_WEAKIFY(self);
426 FIRMessaging_WEAKIFY(connection);
427 FIRMessagingRmqMessageHandler messageHandler = ^(int64_t rmqId, int8_t tag, NSData *data) {
428 FIRMessaging_STRONGIFY(self);
429 FIRMessaging_STRONGIFY(connection);
431 [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
432 if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
433 GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
435 if (![self handleExpirationForDataMessage:stanza]) {
436 // time expired let's delete from RMQ
437 [toRemoveRmqIds addObject:stanza.persistentId];
440 [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
443 [connection sendProto:proto];
445 [self.rmq2Manager scanWithRmqMessageHandler:messageHandler
446 dataMessageHandler:nil];
448 if ([rmqIdsResent length]) {
449 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
453 if ([toRemoveRmqIds count]) {
454 [self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
459 * Check the TTL and generate an error if needed.
461 * @return false if the message needs to be deleted
463 - (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
464 if (message.ttl == 0) {
468 int64_t now = FIRMessagingCurrentTimestampInSeconds();
469 if (now > message.sent + message.ttl) {
470 [self willSendDataMessageFail:message
471 withMessageId:message.id_p
472 error:kFIRMessagingErrorServiceNotAvailable];
478 #pragma mark - Private
480 - (int)delayForMessage:(NSMutableDictionary *)message {
481 int delay = 0; // default
482 if (message[kFIRMessagingSendDelay]) {
483 delay = [message[kFIRMessagingSendDelay] intValue];
484 [message removeObjectForKey:kFIRMessagingSendDelay];
485 if (delay < kMinDelaySeconds) {
487 } else if (delay > kMaxDelaySeconds) {
488 delay = kMaxDelaySeconds;
494 // return True if successfully delayed else False
495 - (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
496 return [self.delayedMessagesQueue queueMessage:message];
499 - (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
500 if (self.client.isConnectionActive) {
501 [self.client sendMessage:stanza];
505 // if we only reconnect for TTL = 0 messages check if we ttl = 0 or
506 // if we reconnect for all messages try to reconnect
507 if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) ||
508 self.upstreamForceReconnect == kUpstreamForceReconnectAll) {
509 BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable];
510 if (isNetworkAvailable) {
511 if (stanza.ttl == 0) {
512 // Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are
513 // persisted, and will be sent from the RMQ.
514 [self.client sendOnConnectOrDrop:stanza];
517 [self.client retryConnectionImmediately:YES];
524 - (NSString *)categoryForUpstreamMessages {
525 return FIRMessagingAppIdentifier();