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 "FIRMessagingTopicOperation.h"
19 #import "FIRMessagingCheckinService.h"
20 #import "FIRMessagingDefines.h"
21 #import "FIRMessagingLogger.h"
22 #import "FIRMessagingUtilities.h"
23 #import "NSError+FIRMessaging.h"
25 #define DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS 0
27 static NSString *const kFIRMessagingSubscribeServerHost =
28 @"https://iid.googleapis.com/iid/register";
30 NSString *FIRMessagingSubscriptionsServer() {
31 static NSString *serverHost = nil;
32 static dispatch_once_t onceToken;
33 dispatch_once(&onceToken, ^{
34 NSDictionary *environment = [[NSProcessInfo processInfo] environment];
35 NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
36 if (customServerHost.length) {
37 serverHost = customServerHost;
39 serverHost = kFIRMessagingSubscribeServerHost;
45 @interface FIRMessagingTopicOperation () {
50 @property(nonatomic, readwrite, copy) NSString *topic;
51 @property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
52 @property(nonatomic, readwrite, copy) NSString *token;
53 @property(nonatomic, readwrite, copy) NSDictionary *options;
54 @property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
55 @property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
57 @property(atomic, strong) NSURLSessionDataTask *dataTask;
61 @implementation FIRMessagingTopicOperation
63 + (NSURLSession *)sharedSession {
64 static NSURLSession *subscriptionOperationSharedSession;
65 static dispatch_once_t onceToken;
66 dispatch_once(&onceToken, ^{
67 NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
68 config.timeoutIntervalForResource = 60.0f; // 1 minute
69 subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
70 subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
72 return subscriptionOperationSharedSession;
75 - (instancetype)initWithTopic:(NSString *)topic
76 action:(FIRMessagingTopicAction)action
77 token:(NSString *)token
78 options:(NSDictionary *)options
79 checkinService:(FIRMessagingCheckinService *)checkinService
80 completion:(FIRMessagingTopicOperationCompletion)completion {
81 if (self = [super init]) {
86 _checkinService = checkinService;
87 _completion = completion;
98 _checkinService = nil;
102 - (BOOL)isAsynchronous {
106 - (BOOL)isExecuting {
110 - (void)setExecuting:(BOOL)executing {
111 [self willChangeValueForKey:@"isExecuting"];
112 _isExecuting = executing;
113 [self didChangeValueForKey:@"isExecuting"];
120 - (void)setFinished:(BOOL)finished {
121 [self willChangeValueForKey:@"isFinished"];
122 _isFinished = finished;
123 [self didChangeValueForKey:@"isFinished"];
127 if (self.isCancelled) {
129 [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
130 [self finishWithError:error];
134 [self setExecuting:YES];
136 [self performSubscriptionChange];
139 - (void)finishWithError:(NSError *)error {
140 // Add a check to prevent this finish from being called more than once.
141 if (self.isFinished) {
145 if (self.completion) {
146 self.completion(error);
149 [self setExecuting:NO];
150 [self setFinished:YES];
155 [self.dataTask cancel];
156 NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
157 [self finishWithError:error];
160 - (void)performSubscriptionChange {
162 NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
163 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
164 NSString *appIdentifier = FIRMessagingAppIdentifier();
165 NSString *deviceAuthID = self.checkinService.deviceAuthID;
166 NSString *secretToken = self.checkinService.secretToken;
167 NSString *authString = [NSString stringWithFormat:@"AidLogin %@:%@", deviceAuthID, secretToken];
168 [request setValue:authString forHTTPHeaderField:@"Authorization"];
169 [request setValue:appIdentifier forHTTPHeaderField:@"app"];
170 [request setValue:self.checkinService.versionInfo forHTTPHeaderField:@"info"];
172 // Topic can contain special characters (like `%`) so encode the value.
173 NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
174 NSString *encodedTopic =
175 [self.topic stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
176 if (encodedTopic == nil) {
177 // The transformation was somehow not possible, so use the original topic.
178 FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed,
179 @"Unable to encode the topic '%@' during topic subscription change. "
180 @"Please ensure that the topic name contains only valid characters.",
182 encodedTopic = self.topic;
185 NSMutableString *content = [NSMutableString stringWithFormat:
186 @"sender=%@&app=%@&device=%@&"
187 @"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
191 FIRMessagingCurrentAppVersion(),
195 if (self.action == FIRMessagingTopicActionUnsubscribe) {
196 [content appendString:@"&delete=true"];
199 FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
202 request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
203 [request setHTTPMethod:@"POST"];
205 #if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
206 NSDate *start = [NSDate date];
209 FIRMessaging_WEAKIFY(self)
210 void(^requestHandler)(NSData *, NSURLResponse *, NSError *) =
211 ^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
212 FIRMessaging_STRONGIFY(self)
214 // Our operation could have been cancelled, which would result in our data task's error being
215 // NSURLErrorCancelled
216 if (error.code == NSURLErrorCancelled) {
217 // We would only have been cancelled in the -cancel method, which will call finish for us
218 // so just return and do nothing.
221 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
222 @"Device registration HTTP fetch error. Error Code: %ld",
223 _FIRMessaging_L(error.code));
224 [self finishWithError:error];
227 NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
228 if (response.length == 0) {
229 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOperationEmptyResponse,
230 @"Invalid registration response - zero length.");
231 [self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
234 NSArray *parts = [response componentsSeparatedByString:@"="];
235 _FIRMessagingDevAssert(parts.count, @"Invalid registration response");
236 if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
237 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002,
238 @"Invalid registration response %@", response);
239 [self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
242 #if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
243 NSTimeInterval duration = -[start timeIntervalSinceNow];
244 FIRMessagingLoggerDebug(@"%@ change took %.2fs", self.topic, duration);
246 [self finishWithError:nil];
250 NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
252 self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
253 NSString *description;
254 if (_action == FIRMessagingTopicActionSubscribe) {
255 description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
257 description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
259 self.dataTask.taskDescription = description;
260 [self.dataTask resume];