RCNConfigRealtime.m 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. /*
  2. * Copyright 2022 Google LLC
  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. #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
  17. #import <Foundation/Foundation.h>
  18. #import <GoogleUtilities/GULNSData+zlib.h>
  19. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  20. #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
  21. #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
  22. #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
  23. #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
  24. #import "FirebaseRemoteConfig/Sources/RCNDevice.h"
  25. /// URL params
  26. static NSString *const kServerURLDomain = @"https://firebaseremoteconfigrealtime.googleapis.com";
  27. static NSString *const kServerURLVersion = @"/v1";
  28. static NSString *const kServerURLProjects = @"/projects/";
  29. static NSString *const kServerURLNamespaces = @"/namespaces/";
  30. static NSString *const kServerURLQuery = @":streamFetchInvalidations?";
  31. static NSString *const kServerURLKey = @"key=";
  32. /// Realtime API enablement
  33. static NSString *const kServerForbiddenStatusCode = @"\"code\": 403";
  34. /// Header names
  35. static NSString *const kHTTPMethodPost = @"POST"; ///< HTTP request method config fetch using
  36. static NSString *const kContentTypeHeaderName = @"Content-Type"; ///< HTTP Header Field Name
  37. static NSString *const kContentEncodingHeaderName =
  38. @"Content-Encoding"; ///< HTTP Header Field Name
  39. static NSString *const kAcceptEncodingHeaderName = @"Accept"; ///< HTTP Header Field Name
  40. static NSString *const kETagHeaderName = @"etag"; ///< HTTP Header Field Name
  41. static NSString *const kIfNoneMatchETagHeaderName = @"if-none-match"; ///< HTTP Header Field Name
  42. static NSString *const kInstallationsAuthTokenHeaderName = @"x-goog-firebase-installations-auth";
  43. // Sends the bundle ID. Refer to b/130301479 for details.
  44. static NSString *const kiOSBundleIdentifierHeaderName =
  45. @"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name
  46. /// Retryable HTTP status code.
  47. static NSInteger const kRCNFetchResponseHTTPStatusOk = 200;
  48. static NSInteger const kRCNFetchResponseHTTPStatusClientTimeout = 429;
  49. static NSInteger const kRCNFetchResponseHTTPStatusTooManyRequests = 429;
  50. static NSInteger const kRCNFetchResponseHTTPStatusCodeBadGateway = 502;
  51. static NSInteger const kRCNFetchResponseHTTPStatusCodeServiceUnavailable = 503;
  52. static NSInteger const kRCNFetchResponseHTTPStatusCodeGatewayTimeout = 504;
  53. /// Invalidation message field names.
  54. static NSString *const kTemplateVersionNumberKey = @"latestTemplateVersionNumber";
  55. static NSString *const kIsFeatureDisabled = @"featureDisabled";
  56. static NSTimeInterval gTimeoutSeconds = 330;
  57. static NSInteger const gFetchAttempts = 3;
  58. // Retry parameters
  59. static NSInteger const gMaxRetries = 7;
  60. @interface FIRConfigUpdateListenerRegistration ()
  61. @property(strong, atomic, nonnull) RCNConfigUpdateCompletion completionHandler;
  62. @end
  63. @implementation FIRConfigUpdateListenerRegistration {
  64. RCNConfigRealtime *_realtimeClient;
  65. }
  66. - (instancetype)initWithClient:(RCNConfigRealtime *)realtimeClient
  67. completionHandler:(RCNConfigUpdateCompletion)completionHandler {
  68. self = [super init];
  69. if (self) {
  70. _realtimeClient = realtimeClient;
  71. _completionHandler = completionHandler;
  72. }
  73. return self;
  74. }
  75. - (void)remove {
  76. [self->_realtimeClient removeConfigUpdateListener:_completionHandler];
  77. }
  78. @end
  79. @interface RCNConfigRealtime ()
  80. @property(strong, atomic, nonnull) NSMutableSet<RCNConfigUpdateCompletion> *listeners;
  81. @property(strong, atomic, nonnull) dispatch_queue_t realtimeLockQueue;
  82. @property(strong, atomic, nonnull) NSNotificationCenter *notificationCenter;
  83. @property(strong, atomic) NSURLSession *session;
  84. @property(strong, atomic) NSURLSessionDataTask *dataTask;
  85. @property(strong, atomic) NSMutableURLRequest *request;
  86. @end
  87. @implementation RCNConfigRealtime {
  88. RCNConfigFetch *_configFetch;
  89. RCNConfigSettings *_settings;
  90. FIROptions *_options;
  91. NSString *_namespace;
  92. NSInteger _remainingRetryCount;
  93. bool _isRequestInProgress;
  94. bool _isInBackground;
  95. bool _isRealtimeDisabled;
  96. }
  97. - (instancetype)init:(RCNConfigFetch *)configFetch
  98. settings:(RCNConfigSettings *)settings
  99. namespace:(NSString *)namespace
  100. options:(FIROptions *)options {
  101. self = [super init];
  102. if (self) {
  103. _listeners = [[NSMutableSet alloc] init];
  104. _realtimeLockQueue = [RCNConfigRealtime realtimeRemoteConfigSerialQueue];
  105. _notificationCenter = [NSNotificationCenter defaultCenter];
  106. _configFetch = configFetch;
  107. _settings = settings;
  108. _options = options;
  109. _namespace = namespace;
  110. _remainingRetryCount = MAX(gMaxRetries - [_settings realtimeRetryCount], 1);
  111. _isRequestInProgress = false;
  112. _isRealtimeDisabled = false;
  113. _isInBackground = false;
  114. [self setUpHttpRequest];
  115. [self setUpHttpSession];
  116. [self backgroundChangeListener];
  117. }
  118. return self;
  119. }
  120. /// Singleton instance of serial queue for queuing all incoming RC calls.
  121. + (dispatch_queue_t)realtimeRemoteConfigSerialQueue {
  122. static dispatch_once_t onceToken;
  123. static dispatch_queue_t realtimeRemoteConfigQueue;
  124. dispatch_once(&onceToken, ^{
  125. realtimeRemoteConfigQueue =
  126. dispatch_queue_create(RCNRemoteConfigQueueLabel, DISPATCH_QUEUE_SERIAL);
  127. });
  128. return realtimeRemoteConfigQueue;
  129. }
  130. - (void)propagateErrors:(NSError *)error {
  131. __weak RCNConfigRealtime *weakSelf = self;
  132. dispatch_async(_realtimeLockQueue, ^{
  133. __strong RCNConfigRealtime *strongSelf = weakSelf;
  134. for (RCNConfigUpdateCompletion listener in strongSelf->_listeners) {
  135. listener(nil, error);
  136. }
  137. });
  138. }
  139. #pragma mark - Test Only Helpers
  140. // TESTING ONLY
  141. - (void)triggerListenerForTesting:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
  142. NSError *_Nullable error))listener {
  143. listener([[FIRRemoteConfigUpdate alloc] init], nil);
  144. }
  145. #pragma mark - Http Helpers
  146. - (NSString *)constructServerURL {
  147. NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain];
  148. serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion];
  149. serverURLStr = [serverURLStr stringByAppendingString:kServerURLProjects];
  150. serverURLStr = [serverURLStr stringByAppendingString:_options.GCMSenderID];
  151. serverURLStr = [serverURLStr stringByAppendingString:kServerURLNamespaces];
  152. /// Get the namespace from the fully qualified namespace string of "namespace:FIRAppName".
  153. NSString *namespace = [_namespace substringToIndex:[_namespace rangeOfString:@":"].location];
  154. serverURLStr = [serverURLStr stringByAppendingString:namespace];
  155. serverURLStr = [serverURLStr stringByAppendingString:kServerURLQuery];
  156. if (_options.APIKey) {
  157. serverURLStr = [serverURLStr stringByAppendingString:kServerURLKey];
  158. serverURLStr = [serverURLStr stringByAppendingString:_options.APIKey];
  159. } else {
  160. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000071",
  161. @"Missing `APIKey` from `FirebaseOptions`, please ensure the configured "
  162. @"`FirebaseApp` is configured with `FirebaseOptions` that contains an `APIKey`.");
  163. }
  164. return serverURLStr;
  165. }
  166. - (NSString *)FIRAppNameFromFullyQualifiedNamespace {
  167. return [[_namespace componentsSeparatedByString:@":"] lastObject];
  168. }
  169. - (void)reportCompletionOnHandler:(FIRRemoteConfigFetchCompletion)completionHandler
  170. withStatus:(FIRRemoteConfigFetchStatus)status
  171. withError:(NSError *)error {
  172. if (completionHandler) {
  173. dispatch_async(_realtimeLockQueue, ^{
  174. completionHandler(status, error);
  175. });
  176. }
  177. }
  178. /// Refresh installation ID token before fetching config. installation ID is now mandatory for fetch
  179. /// requests to work.(b/14751422).
  180. - (void)refreshInstallationsTokenWithCompletionHandler:
  181. (FIRRemoteConfigFetchCompletion)completionHandler {
  182. FIRInstallations *installations = [FIRInstallations
  183. installationsWithApp:[FIRApp appNamed:[self FIRAppNameFromFullyQualifiedNamespace]]];
  184. if (!installations || !_options.GCMSenderID) {
  185. NSString *errorDescription = @"Failed to get GCMSenderID";
  186. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000074", @"%@",
  187. [NSString stringWithFormat:@"%@", errorDescription]);
  188. return [self
  189. reportCompletionOnHandler:completionHandler
  190. withStatus:FIRRemoteConfigFetchStatusFailure
  191. withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
  192. code:FIRRemoteConfigErrorInternalError
  193. userInfo:@{
  194. NSLocalizedDescriptionKey : errorDescription
  195. }]];
  196. }
  197. __weak RCNConfigRealtime *weakSelf = self;
  198. FIRInstallationsTokenHandler installationsTokenHandler = ^(
  199. FIRInstallationsAuthTokenResult *tokenResult, NSError *error) {
  200. RCNConfigRealtime *strongSelf = weakSelf;
  201. if (strongSelf == nil) {
  202. return;
  203. }
  204. if (!tokenResult || !tokenResult.authToken || error) {
  205. NSString *errorDescription =
  206. [NSString stringWithFormat:@"Failed to get installations token. Error : %@.", error];
  207. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000073", @"%@",
  208. [NSString stringWithFormat:@"%@", errorDescription]);
  209. return [strongSelf
  210. reportCompletionOnHandler:completionHandler
  211. withStatus:FIRRemoteConfigFetchStatusFailure
  212. withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
  213. code:FIRRemoteConfigErrorInternalError
  214. userInfo:@{
  215. NSLocalizedDescriptionKey : errorDescription
  216. }]];
  217. }
  218. /// We have a valid token. Get the backing installationID.
  219. [installations installationIDWithCompletion:^(NSString *_Nullable identifier,
  220. NSError *_Nullable error) {
  221. RCNConfigRealtime *strongSelf = weakSelf;
  222. if (strongSelf == nil) {
  223. return;
  224. }
  225. // Dispatch to the RC serial queue to update settings on the queue.
  226. dispatch_async(strongSelf->_realtimeLockQueue, ^{
  227. RCNConfigRealtime *strongSelfQueue = weakSelf;
  228. if (strongSelfQueue == nil) {
  229. return;
  230. }
  231. /// Update config settings with the IID and token.
  232. strongSelfQueue->_settings.configInstallationsToken = tokenResult.authToken;
  233. strongSelfQueue->_settings.configInstallationsIdentifier = identifier;
  234. if (!identifier || error) {
  235. NSString *errorDescription =
  236. [NSString stringWithFormat:@"Error getting iid : %@.", error];
  237. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000055", @"%@",
  238. [NSString stringWithFormat:@"%@", errorDescription]);
  239. strongSelfQueue->_settings.isFetchInProgress = NO;
  240. return [strongSelfQueue
  241. reportCompletionOnHandler:completionHandler
  242. withStatus:FIRRemoteConfigFetchStatusFailure
  243. withError:[NSError
  244. errorWithDomain:FIRRemoteConfigErrorDomain
  245. code:FIRRemoteConfigErrorInternalError
  246. userInfo:@{
  247. NSLocalizedDescriptionKey : errorDescription
  248. }]];
  249. }
  250. FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.",
  251. strongSelfQueue->_settings.configInstallationsIdentifier);
  252. return [strongSelfQueue reportCompletionOnHandler:completionHandler
  253. withStatus:FIRRemoteConfigFetchStatusNoFetchYet
  254. withError:nil];
  255. });
  256. }];
  257. };
  258. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", @"Starting requesting token.");
  259. [installations authTokenWithCompletion:installationsTokenHandler];
  260. }
  261. - (void)createRequestBodyWithCompletion:(void (^)(NSData *_Nonnull requestBody))completion {
  262. __weak __typeof(self) weakSelf = self;
  263. [self refreshInstallationsTokenWithCompletionHandler:^(FIRRemoteConfigFetchStatus status,
  264. NSError *_Nullable error) {
  265. __strong __typeof(self) strongSelf = weakSelf;
  266. if (!strongSelf) return;
  267. if (![strongSelf->_settings.configInstallationsIdentifier length]) {
  268. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000013",
  269. @"Installation token retrieval failed. Realtime connection will not include "
  270. @"valid installations token.");
  271. }
  272. [strongSelf.request setValue:strongSelf->_settings.configInstallationsToken
  273. forHTTPHeaderField:kInstallationsAuthTokenHeaderName];
  274. if (strongSelf->_settings.lastETag) {
  275. [strongSelf.request setValue:strongSelf->_settings.lastETag
  276. forHTTPHeaderField:kIfNoneMatchETagHeaderName];
  277. }
  278. NSString *namespace = [strongSelf->_namespace
  279. substringToIndex:[strongSelf->_namespace rangeOfString:@":"].location];
  280. NSString *postBody = [NSString
  281. stringWithFormat:@"{project:'%@', namespace:'%@', lastKnownVersionNumber:'%@', appId:'%@', "
  282. @"sdkVersion:'%@', appInstanceId:'%@'}",
  283. [strongSelf->_options GCMSenderID], namespace,
  284. strongSelf->_configFetch.templateVersionNumber,
  285. strongSelf->_options.googleAppID, FIRRemoteConfigPodVersion(),
  286. strongSelf->_settings.configInstallationsIdentifier];
  287. NSData *postData = [postBody dataUsingEncoding:NSUTF8StringEncoding];
  288. NSError *compressionError;
  289. completion([NSData gul_dataByGzippingData:postData error:&compressionError]);
  290. }];
  291. }
  292. /// Creates request.
  293. - (void)setUpHttpRequest {
  294. NSString *address = [self constructServerURL];
  295. _request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:address]
  296. cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
  297. timeoutInterval:gTimeoutSeconds];
  298. [_request setHTTPMethod:kHTTPMethodPost];
  299. [_request setValue:@"application/json" forHTTPHeaderField:kContentTypeHeaderName];
  300. [_request setValue:@"application/json" forHTTPHeaderField:kAcceptEncodingHeaderName];
  301. [_request setValue:@"gzip" forHTTPHeaderField:kContentEncodingHeaderName];
  302. [_request setValue:@"true" forHTTPHeaderField:@"X-Google-GFE-Can-Retry"];
  303. [_request setValue:[_options APIKey] forHTTPHeaderField:@"X-Goog-Api-Key"];
  304. [_request setValue:[[NSBundle mainBundle] bundleIdentifier]
  305. forHTTPHeaderField:kiOSBundleIdentifierHeaderName];
  306. }
  307. /// Makes call to create session.
  308. - (void)setUpHttpSession {
  309. NSURLSessionConfiguration *sessionConfig =
  310. [[NSURLSessionConfiguration defaultSessionConfiguration] copy];
  311. [sessionConfig setTimeoutIntervalForResource:gTimeoutSeconds];
  312. [sessionConfig setTimeoutIntervalForRequest:gTimeoutSeconds];
  313. _session = [NSURLSession sessionWithConfiguration:sessionConfig
  314. delegate:self
  315. delegateQueue:[NSOperationQueue mainQueue]];
  316. }
  317. #pragma mark - Retry Helpers
  318. - (BOOL)canMakeConnection {
  319. BOOL noRunningConnection =
  320. self->_dataTask == nil || self->_dataTask.state != NSURLSessionTaskStateRunning;
  321. BOOL canMakeConnection = noRunningConnection && [self->_listeners count] > 0 &&
  322. !self->_isInBackground && !self->_isRealtimeDisabled;
  323. return canMakeConnection;
  324. }
  325. // Retry mechanism for HTTP connections
  326. - (void)retryHTTPConnection {
  327. __weak RCNConfigRealtime *weakSelf = self;
  328. dispatch_async(_realtimeLockQueue, ^{
  329. __strong RCNConfigRealtime *strongSelf = weakSelf;
  330. if (!strongSelf || strongSelf->_isInBackground) {
  331. return;
  332. }
  333. if ([strongSelf canMakeConnection] && strongSelf->_remainingRetryCount > 0) {
  334. NSTimeInterval backOffInterval = self->_settings.getRealtimeBackoffInterval;
  335. strongSelf->_remainingRetryCount--;
  336. [strongSelf->_settings setRealtimeRetryCount:[strongSelf->_settings realtimeRetryCount] + 1];
  337. dispatch_time_t executionDelay =
  338. dispatch_time(DISPATCH_TIME_NOW, (backOffInterval * NSEC_PER_SEC));
  339. dispatch_after(executionDelay, strongSelf->_realtimeLockQueue, ^{
  340. [strongSelf beginRealtimeStream];
  341. });
  342. } else {
  343. NSError *error = [NSError
  344. errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  345. code:FIRRemoteConfigUpdateErrorStreamError
  346. userInfo:@{
  347. NSLocalizedDescriptionKey :
  348. @"Unable to connect to the server. Check your connection and try again."
  349. }];
  350. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014", @"Cannot establish connection. Error: %@",
  351. error);
  352. [self propagateErrors:error];
  353. }
  354. });
  355. }
  356. - (void)backgroundChangeListener {
  357. [_notificationCenter addObserver:self
  358. selector:@selector(isInForeground)
  359. name:@"UIApplicationWillEnterForegroundNotification"
  360. object:nil];
  361. [_notificationCenter addObserver:self
  362. selector:@selector(isInBackground)
  363. name:@"UIApplicationDidEnterBackgroundNotification"
  364. object:nil];
  365. }
  366. - (void)isInForeground {
  367. __weak RCNConfigRealtime *weakSelf = self;
  368. dispatch_async(_realtimeLockQueue, ^{
  369. __strong RCNConfigRealtime *strongSelf = weakSelf;
  370. strongSelf->_isInBackground = false;
  371. [strongSelf beginRealtimeStream];
  372. });
  373. }
  374. - (void)isInBackground {
  375. __weak RCNConfigRealtime *weakSelf = self;
  376. dispatch_async(_realtimeLockQueue, ^{
  377. __strong RCNConfigRealtime *strongSelf = weakSelf;
  378. [strongSelf pauseRealtimeStream];
  379. strongSelf->_isInBackground = true;
  380. });
  381. }
  382. #pragma mark - Autofetch Helpers
  383. - (void)fetchLatestConfig:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
  384. __weak RCNConfigRealtime *weakSelf = self;
  385. dispatch_async(_realtimeLockQueue, ^{
  386. __strong RCNConfigRealtime *strongSelf = weakSelf;
  387. NSInteger attempts = remainingAttempts - 1;
  388. [strongSelf->_configFetch
  389. realtimeFetchConfigWithNoExpirationDuration:gFetchAttempts - attempts
  390. completionHandler:^(FIRRemoteConfigFetchStatus status,
  391. FIRRemoteConfigUpdate *update,
  392. NSError *error) {
  393. if (error != nil) {
  394. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010",
  395. @"Failed to retrieve config due to fetch error. "
  396. @"Error: %@",
  397. error);
  398. return [self propagateErrors:error];
  399. }
  400. if (status == FIRRemoteConfigFetchStatusSuccess) {
  401. if ([strongSelf->_configFetch.templateVersionNumber
  402. integerValue] >= targetVersion) {
  403. // only notify listeners if there is a change
  404. if ([update updatedKeys].count > 0) {
  405. dispatch_async(strongSelf->_realtimeLockQueue, ^{
  406. for (RCNConfigUpdateCompletion listener in strongSelf
  407. ->_listeners) {
  408. listener(update, nil);
  409. }
  410. });
  411. }
  412. } else {
  413. FIRLogDebug(
  414. kFIRLoggerRemoteConfig, @"I-RCN000016",
  415. @"Fetched config's template version is outdated, "
  416. @"re-fetching");
  417. [strongSelf autoFetch:attempts targetVersion:targetVersion];
  418. }
  419. } else {
  420. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000016",
  421. @"Fetched config's template version is "
  422. @"outdated, re-fetching");
  423. [strongSelf autoFetch:attempts targetVersion:targetVersion];
  424. }
  425. }];
  426. });
  427. }
  428. - (void)scheduleFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
  429. /// Needs fetch to occur between 0 - 3 seconds. Randomize to not cause DDoS alerts in backend
  430. dispatch_time_t executionDelay =
  431. dispatch_time(DISPATCH_TIME_NOW, arc4random_uniform(4) * NSEC_PER_SEC);
  432. dispatch_after(executionDelay, _realtimeLockQueue, ^{
  433. [self fetchLatestConfig:remainingAttempts targetVersion:targetVersion];
  434. });
  435. }
  436. /// Perform fetch and handle developers callbacks
  437. - (void)autoFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
  438. __weak RCNConfigRealtime *weakSelf = self;
  439. dispatch_async(_realtimeLockQueue, ^{
  440. __strong RCNConfigRealtime *strongSelf = weakSelf;
  441. if (remainingAttempts == 0) {
  442. NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  443. code:FIRRemoteConfigUpdateErrorNotFetched
  444. userInfo:@{
  445. NSLocalizedDescriptionKey :
  446. @"Unable to fetch the latest version of the template."
  447. }];
  448. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
  449. @"Ran out of fetch attempts, cannot find target config version.");
  450. [self propagateErrors:error];
  451. return;
  452. }
  453. [strongSelf scheduleFetch:remainingAttempts targetVersion:targetVersion];
  454. });
  455. }
  456. #pragma mark - NSURLSession Delegates
  457. - (void)evaluateStreamResponse:(NSDictionary *)response error:(NSError *)dataError {
  458. NSInteger updateTemplateVersion = 1;
  459. if (dataError == nil) {
  460. if ([response objectForKey:kTemplateVersionNumberKey]) {
  461. updateTemplateVersion = [[response objectForKey:kTemplateVersionNumberKey] integerValue];
  462. }
  463. if ([response objectForKey:kIsFeatureDisabled]) {
  464. self->_isRealtimeDisabled = [response objectForKey:kIsFeatureDisabled];
  465. }
  466. if (self->_isRealtimeDisabled) {
  467. [self pauseRealtimeStream];
  468. NSError *error = [NSError
  469. errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  470. code:FIRRemoteConfigUpdateErrorUnavailable
  471. userInfo:@{
  472. NSLocalizedDescriptionKey :
  473. @"The server is temporarily unavailable. Try again in a few minutes."
  474. }];
  475. [self propagateErrors:error];
  476. } else {
  477. NSInteger clientTemplateVersion = [_configFetch.templateVersionNumber integerValue];
  478. if (updateTemplateVersion > clientTemplateVersion) {
  479. [self autoFetch:gFetchAttempts targetVersion:updateTemplateVersion];
  480. }
  481. }
  482. } else {
  483. NSError *error =
  484. [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  485. code:FIRRemoteConfigUpdateErrorMessageInvalid
  486. userInfo:@{NSLocalizedDescriptionKey : @"Unable to parse ConfigUpdate."}];
  487. [self propagateErrors:error];
  488. }
  489. }
  490. /// Delegate to asynchronously handle every new notification that comes over the wire. Auto-fetches
  491. /// and runs callback for each new notification
  492. - (void)URLSession:(NSURLSession *)session
  493. dataTask:(NSURLSessionDataTask *)dataTask
  494. didReceiveData:(NSData *)data {
  495. NSError *dataError;
  496. NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  497. /// If response data contains the API enablement link, return the entire message to the user in
  498. /// the form of a error.
  499. if ([strData containsString:kServerForbiddenStatusCode]) {
  500. NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  501. code:FIRRemoteConfigUpdateErrorStreamError
  502. userInfo:@{NSLocalizedDescriptionKey : strData}];
  503. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. %@", error);
  504. [self propagateErrors:error];
  505. return;
  506. }
  507. NSRange endRange = [strData rangeOfString:@"}"];
  508. NSRange beginRange = [strData rangeOfString:@"{"];
  509. if (beginRange.location != NSNotFound && endRange.location != NSNotFound) {
  510. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000015",
  511. @"Received config update message on stream.");
  512. NSRange msgRange =
  513. NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1);
  514. strData = [strData substringWithRange:msgRange];
  515. data = [strData dataUsingEncoding:NSUTF8StringEncoding];
  516. NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data
  517. options:NSJSONReadingMutableContainers
  518. error:&dataError];
  519. [self evaluateStreamResponse:response error:dataError];
  520. }
  521. }
  522. /// Check if response code is retryable
  523. - (bool)isStatusCodeRetryable:(NSInteger)statusCode {
  524. return statusCode == kRCNFetchResponseHTTPStatusClientTimeout ||
  525. statusCode == kRCNFetchResponseHTTPStatusTooManyRequests ||
  526. statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable ||
  527. statusCode == kRCNFetchResponseHTTPStatusCodeBadGateway ||
  528. statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout;
  529. }
  530. /// Delegate to handle initial reply from the server
  531. - (void)URLSession:(NSURLSession *)session
  532. dataTask:(NSURLSessionDataTask *)dataTask
  533. didReceiveResponse:(NSURLResponse *)response
  534. completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  535. _isRequestInProgress = false;
  536. NSHTTPURLResponse *_httpURLResponse = (NSHTTPURLResponse *)response;
  537. NSInteger statusCode = [_httpURLResponse statusCode];
  538. if (statusCode == 403) {
  539. completionHandler(NSURLSessionResponseAllow);
  540. return;
  541. }
  542. if (statusCode != kRCNFetchResponseHTTPStatusOk) {
  543. [self->_settings updateRealtimeExponentialBackoffTime];
  544. [self pauseRealtimeStream];
  545. if ([self isStatusCodeRetryable:statusCode]) {
  546. [self retryHTTPConnection];
  547. } else {
  548. NSError *error = [NSError
  549. errorWithDomain:FIRRemoteConfigUpdateErrorDomain
  550. code:FIRRemoteConfigUpdateErrorStreamError
  551. userInfo:@{
  552. NSLocalizedDescriptionKey :
  553. [NSString stringWithFormat:@"Unable to connect to the server. Try again in "
  554. @"a few minutes. Http Status code: %@",
  555. [@(statusCode) stringValue]]
  556. }];
  557. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. Error: %@",
  558. error);
  559. }
  560. } else {
  561. /// on success reset retry parameters
  562. _remainingRetryCount = gMaxRetries;
  563. [self->_settings setRealtimeRetryCount:0];
  564. }
  565. completionHandler(NSURLSessionResponseAllow);
  566. }
  567. /// Delegate to handle data task completion
  568. - (void)URLSession:(NSURLSession *)session
  569. task:(NSURLSessionTask *)task
  570. didCompleteWithError:(NSError *)error {
  571. _isRequestInProgress = false;
  572. if (error != nil && [error code] != NSURLErrorCancelled) {
  573. [self->_settings updateRealtimeExponentialBackoffTime];
  574. }
  575. [self pauseRealtimeStream];
  576. [self retryHTTPConnection];
  577. }
  578. /// Delegate to handle session invalidation
  579. - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
  580. if (!_isRequestInProgress) {
  581. if (error != nil) {
  582. [self->_settings updateRealtimeExponentialBackoffTime];
  583. }
  584. [self pauseRealtimeStream];
  585. [self retryHTTPConnection];
  586. }
  587. }
  588. #pragma mark - Top level methods
  589. - (void)beginRealtimeStream {
  590. __weak __typeof(self) weakSelf = self;
  591. dispatch_async(_realtimeLockQueue, ^{
  592. __strong __typeof(self) strongSelf = weakSelf;
  593. if (strongSelf->_settings.getRealtimeBackoffInterval > 0) {
  594. [strongSelf retryHTTPConnection];
  595. return;
  596. }
  597. if ([strongSelf canMakeConnection]) {
  598. __weak __typeof(self) weakSelf = strongSelf;
  599. [strongSelf createRequestBodyWithCompletion:^(NSData *_Nonnull requestBody) {
  600. __strong __typeof(self) strongSelf = weakSelf;
  601. if (!strongSelf) return;
  602. strongSelf->_isRequestInProgress = true;
  603. [strongSelf->_request setHTTPBody:requestBody];
  604. strongSelf->_dataTask = [strongSelf->_session dataTaskWithRequest:strongSelf->_request];
  605. [strongSelf->_dataTask resume];
  606. }];
  607. }
  608. });
  609. }
  610. - (void)pauseRealtimeStream {
  611. __weak RCNConfigRealtime *weakSelf = self;
  612. dispatch_async(_realtimeLockQueue, ^{
  613. __strong RCNConfigRealtime *strongSelf = weakSelf;
  614. if (strongSelf->_dataTask != nil) {
  615. [strongSelf->_dataTask cancel];
  616. strongSelf->_dataTask = nil;
  617. }
  618. });
  619. }
  620. - (FIRConfigUpdateListenerRegistration *)addConfigUpdateListener:
  621. (void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate, NSError *_Nullable error))listener {
  622. if (listener == nil) {
  623. return nil;
  624. }
  625. __block id listenerCopy = listener;
  626. __weak RCNConfigRealtime *weakSelf = self;
  627. dispatch_async(_realtimeLockQueue, ^{
  628. __strong RCNConfigRealtime *strongSelf = weakSelf;
  629. [strongSelf->_listeners addObject:listenerCopy];
  630. [strongSelf beginRealtimeStream];
  631. });
  632. return [[FIRConfigUpdateListenerRegistration alloc] initWithClient:self
  633. completionHandler:listenerCopy];
  634. }
  635. - (void)removeConfigUpdateListener:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
  636. NSError *_Nullable error))listener {
  637. __weak RCNConfigRealtime *weakSelf = self;
  638. dispatch_async(_realtimeLockQueue, ^{
  639. __strong RCNConfigRealtime *strongSelf = weakSelf;
  640. [strongSelf->_listeners removeObject:listener];
  641. if (strongSelf->_listeners.count == 0) {
  642. [strongSelf pauseRealtimeStream];
  643. }
  644. });
  645. }
  646. @end