added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingDataMessageManager.m
1 /*
2  * Copyright 2017 Google
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 #import "FIRMessagingDataMessageManager.h"
18
19 #import "Protos/GtalkCore.pbobjc.h"
20
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"
33
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
37
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.";
43
44 static NSString *const kFCMMessageSpecialMessage = @"message_type";
45
46 // special messages sent by the server
47 static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
48
49 static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
50 static NSString *const kDataMessageNotificationKey = @"notification";
51
52
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,
60 };
61
62 @interface FIRMessagingDataMessageManager ()
63
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;
69
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;
75
76 @end
77
78 @implementation FIRMessagingDataMessageManager
79
80 - (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
81                           client:(FIRMessagingClient *)client
82                      rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
83               syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
84   self = [super init];
85   if (self) {
86     _delegate = delegate;
87     _client = client;
88     _rmq2Manager = rmq2Manager;
89     _syncMessageManager = syncMessageManager;
90     _ttl = kFIRMessagingSendTtlDefault;
91     _maxAppDataSize = kMaxAppDataSizeDefault;
92     // on by default
93     _upstreamForceReconnect = kUpstreamForceReconnectAll;
94   }
95   return self;
96 }
97
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;
103 }
104
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];
112                               }];
113 }
114
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];
121     return nil;
122   } else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
123     [self handleMCSDataMessage:dataMessage];
124     return nil;
125   }
126
127   return [self parseDataMessage:dataMessage];
128 }
129
130 - (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
131   FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
132                           @"Received message for FIRMessaging from downstream %@", dataMessage);
133 }
134
135 - (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
136   NSMutableDictionary *message = [NSMutableDictionary dictionary];
137   NSString *from = [dataMessage from];
138   if ([from length]) {
139     message[kFIRMessagingFromKey] = from;
140   }
141
142   // raw data
143   NSData *rawData = [dataMessage rawData];
144   if ([rawData length]) {
145     message[kFIRMessagingRawDataKey] = rawData;
146   }
147
148   NSString *token = [dataMessage token];
149   if ([token length]) {
150     message[kFIRMessagingCollapseKey] = token;
151   }
152
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;
158   }
159
160   // third-party data
161   for (GtalkAppData *item in dataMessage.appDataArray) {
162     _FIRMessagingDevAssert(item.hasKey && item.hasValue, @"Invalid AppData");
163
164     // do not process the "from" key -- is not useful
165     if ([kFIRMessagingFromKey isEqualToString:item.key]) {
166       continue;
167     }
168
169     // Filter the "gcm.notification." keys in the message
170     if ([item.key hasPrefix:kMCSNotificationPrefix]) {
171       NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
172       if ([key length]) {
173         if (!message[kDataMessageNotificationKey]) {
174           message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
175         }
176         message[kDataMessageNotificationKey][key] = item.value;
177       } else {
178         _FIRMessagingDevAssert([key length], @"Invalid key in MCS message: %@", key);
179         FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
180                                 @"Invalid key in MCS message: %@", key);
181       }
182       continue;
183     }
184
185     // Filter the "gcm.duplex" key
186     if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
187       BOOL value = [item.value boolValue];
188       message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
189       continue;
190     }
191
192     // do not allow keys with "reserved" keyword
193     if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
194       continue;
195     }
196
197     [message setObject:item.value forKey:item.key];
198   }
199   // TODO: Add support for encrypting raw data later
200   return [NSDictionary dictionaryWithDictionary:message];
201 }
202
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];
210       return;
211     }
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];
217     if (isDuplicate) {
218       return;
219     }
220   }
221   NSString *messageId = message[kFIRMessagingMessageIDKey];
222   NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
223   [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
224 }
225
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];
231     }
232   }
233   return [newMessage copy];
234 }
235
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];
242
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) {
246     ttl = self.ttl;
247   }
248   [stanza setTtl:ttl];
249   [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
250
251   int delay = [self delayForMessage:dataMessage];
252   if (delay > 0) {
253     [stanza setMaxDelay:delay];
254   }
255
256   if (msgId) {
257     [stanza setId_p:msgId];
258   }
259
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];
266   }
267
268   if (!self.secretToken) {
269     FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
270                             @"Trying to send data message without a secret token. "
271                             @"Authentication failed.");
272     [self willSendDataMessageFail:stanza
273                     withMessageId:msgId
274                             error:kFIRMessagingErrorCodeMissingDeviceID];
275     return;
276   }
277
278   if (![to length]) {
279     [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
280     return;
281   }
282   [stanza setTo:to];
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:@""];
287
288   // MCS itself would set the registration ID
289   // [stanza setRegId:nil];
290
291   int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
292   if (size > kMaxAppDataSizeDefault) {
293     [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
294     return;
295   }
296
297   BOOL useRmq = (ttl != 0) && (msgId != nil);
298   if (useRmq) {
299     if (!self.client.isConnected) {
300       // do nothing assuming rmq save is enabled
301     }
302
303     NSError *error;
304     if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
305       FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
306       [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
307       return;
308     }
309
310     [self willSendDataMessageSuccess:stanza withMessageId:msgId];
311   }
312
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 %@",
317                             dataMessage);
318     return;
319   }
320   // send delayed messages
321   [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
322
323   BOOL sending = [self tryToSendDataMessageStanza:stanza];
324   if (!sending) {
325     if (useRmq) {
326       NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
327       FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
328     } else {
329       [self willSendDataMessageFail:stanza
330                       withMessageId:msgId
331                               error:kFIRMessagingErrorCodeNetwork];
332       return;
333     }
334   }
335 }
336
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];
343   }
344 }
345
346 - (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
347   NSString *msgId = [message id_p] ?: @"";
348   [self.delegate didSendDataMessageWithID:msgId];
349 }
350
351 - (void)addParamWithKey:(NSString *)key
352                   value:(NSString *)val
353                toStanza:(GtalkDataMessageStanza *)stanza {
354   if (!key || !val) {
355     return;
356   }
357   GtalkAppData *appData = [[GtalkAppData alloc] init];
358   [appData setKey:key];
359   [appData setValue:val];
360   [[stanza appDataArray] addObject:appData];
361 }
362
363 /**
364  @return The size of the data being added to stanza.
365  */
366 - (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
367   int size = 0;
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];
383     } else {
384       FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
385                               key);
386     }
387   }
388   return size;
389 }
390
391 /**
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()
394  * method.
395  */
396 - (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
397                      withMessageId:(NSString *)messageId {
398   FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
399                           @"send message success: %@", messageId);
400   [self.delegate willSendDataMessageWithID:messageId error:nil];
401 }
402
403 /**
404  * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
405  * extra - same as 'message deleted'.
406  *
407  * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
408  * we throw IOExceptions
409  */
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);
415
416   NSError *error = [NSError errorWithFCMErrorCode:errorCode];
417   if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
418     [self.delegate willSendDataMessageWithID:messageId error:error];
419   }
420 }
421
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);
430     GPBMessage *proto =
431         [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
432     if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
433       GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
434
435       if (![self handleExpirationForDataMessage:stanza]) {
436         // time expired let's delete from RMQ
437         [toRemoveRmqIds addObject:stanza.persistentId];
438         return;
439       }
440       [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
441     }
442
443     [connection sendProto:proto];
444   };
445   [self.rmq2Manager scanWithRmqMessageHandler:messageHandler
446                            dataMessageHandler:nil];
447
448   if ([rmqIdsResent length]) {
449     FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
450                             rmqIdsResent);
451   }
452
453   if ([toRemoveRmqIds count]) {
454     [self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
455   }
456 }
457
458 /**
459  *  Check the TTL and generate an error if needed.
460  *
461  *  @return false if the message needs to be deleted
462  */
463 - (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
464   if (message.ttl == 0) {
465     return NO;
466   }
467
468   int64_t now = FIRMessagingCurrentTimestampInSeconds();
469   if (now > message.sent + message.ttl) {
470     [self willSendDataMessageFail:message
471                     withMessageId:message.id_p
472                             error:kFIRMessagingErrorServiceNotAvailable];
473     return NO;
474   }
475   return YES;
476 }
477
478 #pragma mark - Private
479
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) {
486       delay = 0;
487     } else if (delay > kMaxDelaySeconds) {
488       delay = kMaxDelaySeconds;
489     }
490   }
491   return delay;
492 }
493
494 // return True if successfully delayed else False
495 - (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
496   return [self.delayedMessagesQueue queueMessage:message];
497 }
498
499 - (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
500   if (self.client.isConnectionActive) {
501     [self.client sendMessage:stanza];
502     return YES;
503   }
504
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];
515       }
516
517       [self.client retryConnectionImmediately:YES];
518       return YES;
519     }
520   }
521   return NO;
522 }
523
524 - (NSString *)categoryForUpstreamMessages {
525   return FIRMessagingAppIdentifier();
526 }
527
528 @end