RCNConfigRealtime.m 32 KB

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