RLMSyncSessionRefreshHandle.mm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. ////////////////////////////////////////////////////////////////////////////
  2. //
  3. // Copyright 2016 Realm Inc.
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License");
  6. // you may not use this file except in compliance with the License.
  7. // You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. //
  17. ////////////////////////////////////////////////////////////////////////////
  18. #import "RLMSyncSessionRefreshHandle.hpp"
  19. #import "RLMJSONModels.h"
  20. #import "RLMNetworkClient.h"
  21. #import "RLMSyncManager_Private.h"
  22. #import "RLMSyncUser_Private.hpp"
  23. #import "RLMSyncUtil_Private.hpp"
  24. #import "RLMUtil.hpp"
  25. #import "sync/sync_session.hpp"
  26. using namespace realm;
  27. namespace {
  28. void unregisterRefreshHandle(const std::weak_ptr<SyncUser>& user, const std::string& path) {
  29. if (auto strong_user = user.lock()) {
  30. context_for(strong_user).unregister_refresh_handle(path);
  31. }
  32. }
  33. void reportInvalidAccessToken(const std::weak_ptr<SyncUser>& user, NSError *error) {
  34. if (auto strong_user = user.lock()) {
  35. if (RLMUserErrorReportingBlock block = context_for(strong_user).error_handler()) {
  36. RLMSyncUser *theUser = [[RLMSyncUser alloc] initWithSyncUser:std::move(strong_user)];
  37. [theUser logOut];
  38. block(theUser, error);
  39. }
  40. }
  41. }
  42. }
  43. static const NSTimeInterval RLMRefreshBuffer = 10;
  44. @interface RLMSyncSessionRefreshHandle () {
  45. std::weak_ptr<SyncUser> _user;
  46. std::string _path;
  47. std::weak_ptr<SyncSession> _session;
  48. std::shared_ptr<SyncSession> _strongSession;
  49. }
  50. @property (nonatomic) NSTimer *timer;
  51. @property (nonatomic) NSURL *realmURL;
  52. @property (nonatomic) NSURL *authServerURL;
  53. @property (nonatomic, copy) RLMSyncBasicErrorReportingBlock completionBlock;
  54. @end
  55. @implementation RLMSyncSessionRefreshHandle
  56. - (instancetype)initWithRealmURL:(NSURL *)realmURL
  57. user:(std::shared_ptr<realm::SyncUser>)user
  58. session:(std::shared_ptr<realm::SyncSession>)session
  59. completionBlock:(RLMSyncBasicErrorReportingBlock)completionBlock {
  60. if (self = [super init]) {
  61. NSString *path = [realmURL path];
  62. _path = [path UTF8String];
  63. self.authServerURL = [NSURL URLWithString:@(user->server_url().c_str())];
  64. if (!self.authServerURL) {
  65. @throw RLMException(@"User object isn't configured with an auth server URL.");
  66. }
  67. self.completionBlock = completionBlock;
  68. self.realmURL = realmURL;
  69. // For the initial bind, we want to prolong the session's lifetime.
  70. _strongSession = std::move(session);
  71. _session = _strongSession;
  72. _user = user;
  73. // Immediately fire off the network request.
  74. [self _timerFired:nil];
  75. return self;
  76. }
  77. return nil;
  78. }
  79. - (void)dealloc {
  80. [self.timer invalidate];
  81. }
  82. - (void)invalidate {
  83. _strongSession = nullptr;
  84. [self.timer invalidate];
  85. }
  86. + (NSDate *)fireDateForTokenExpirationDate:(NSDate *)date nowDate:(NSDate *)nowDate {
  87. NSDate *fireDate = [date dateByAddingTimeInterval:-RLMRefreshBuffer];
  88. // Only fire times in the future are valid.
  89. return ([fireDate compare:nowDate] == NSOrderedDescending ? fireDate : nil);
  90. }
  91. - (void)scheduleRefreshTimer:(NSDate *)dateWhenTokenExpires {
  92. // Schedule the timer on the main queue.
  93. // It's very likely that this method will be run on a side thread, for example
  94. // on the thread that runs `NSURLSession`'s completion blocks. We can't be
  95. // guaranteed that there's an existing runloop on those threads, and we don't want
  96. // to create and start a new one if one doesn't already exist.
  97. dispatch_async(dispatch_get_main_queue(), ^{
  98. [self.timer invalidate];
  99. NSDate *fireDate = [RLMSyncSessionRefreshHandle fireDateForTokenExpirationDate:dateWhenTokenExpires
  100. nowDate:[NSDate date]];
  101. if (!fireDate) {
  102. unregisterRefreshHandle(_user, _path);
  103. return;
  104. }
  105. self.timer = [[NSTimer alloc] initWithFireDate:fireDate
  106. interval:0
  107. target:self
  108. selector:@selector(_timerFired:)
  109. userInfo:nil
  110. repeats:NO];
  111. [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
  112. });
  113. }
  114. /// Handler for network requests whose responses successfully parse into an auth response model.
  115. - (BOOL)_handleSuccessfulRequest:(RLMAuthResponseModel *)model {
  116. std::shared_ptr<SyncSession> session = _session.lock();
  117. if (!session) {
  118. // The session is dead or in a fatal error state.
  119. unregisterRefreshHandle(_user, _path);
  120. [self invalidate];
  121. return NO;
  122. }
  123. // Calculate the resolved path.
  124. NSString *resolvedURLString = nil;
  125. RLMServerPath resolvedPath = model.accessToken.tokenData.path;
  126. // Munge the path back onto the original URL, because the `sync` API expects an entire URL.
  127. NSURLComponents *urlBuffer = [NSURLComponents componentsWithURL:self.realmURL
  128. resolvingAgainstBaseURL:YES];
  129. urlBuffer.path = resolvedPath;
  130. resolvedURLString = [[urlBuffer URL] absoluteString];
  131. if (!resolvedURLString) {
  132. @throw RLMException(@"Resolved path returned from the server was invalid (%@).", resolvedPath);
  133. }
  134. // Pass the token and resolved path to the underlying sync subsystem.
  135. session->refresh_access_token([model.accessToken.token UTF8String], {resolvedURLString.UTF8String});
  136. // Schedule a refresh. If we're successful we must already have `bind()`ed the session
  137. // initially, so we can null out the strong pointer.
  138. _strongSession = nullptr;
  139. NSDate *expires = [NSDate dateWithTimeIntervalSince1970:model.accessToken.tokenData.expires];
  140. [self scheduleRefreshTimer:expires];
  141. if (self.completionBlock) {
  142. self.completionBlock(nil);
  143. }
  144. return true;
  145. }
  146. /// Handler for network requests that failed before the JSON parsing stage.
  147. - (void)_handleFailedRequest:(NSError *)error {
  148. NSError *authError;
  149. if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
  150. // Network client may return sync related error
  151. authError = error;
  152. // Try to report this error to the expiration callback.
  153. reportInvalidAccessToken(_user, authError);
  154. } else {
  155. // Something else went wrong
  156. authError = make_auth_error_bad_response();
  157. }
  158. if (self.completionBlock) {
  159. self.completionBlock(authError);
  160. }
  161. [[RLMSyncManager sharedManager] _fireError:make_sync_error(authError)];
  162. // Certain errors related to network connectivity should trigger a retry.
  163. NSDate *nextTryDate = nil;
  164. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  165. switch (error.code) {
  166. case NSURLErrorCannotConnectToHost:
  167. case NSURLErrorNotConnectedToInternet:
  168. case NSURLErrorNetworkConnectionLost:
  169. case NSURLErrorTimedOut:
  170. case NSURLErrorDNSLookupFailed:
  171. case NSURLErrorCannotFindHost:
  172. // FIXME: 10 seconds is an arbitrarily chosen value, consider rationalizing it.
  173. nextTryDate = [NSDate dateWithTimeIntervalSinceNow:RLMRefreshBuffer + 10];
  174. break;
  175. default:
  176. break;
  177. }
  178. }
  179. if (!nextTryDate) {
  180. // This error isn't a network failure error. Just invalidate the refresh handle and stop.
  181. if (_strongSession) {
  182. _strongSession->log_out();
  183. }
  184. unregisterRefreshHandle(_user, _path);
  185. [self invalidate];
  186. return;
  187. }
  188. // If we tried to initially bind the session and failed, we'll try again. However, each
  189. // subsequent attempt will use a weak pointer to avoid prolonging the session's lifetime
  190. // unnecessarily.
  191. _strongSession = nullptr;
  192. [self scheduleRefreshTimer:nextTryDate];
  193. return;
  194. }
  195. /// Callback handler for network requests.
  196. - (BOOL)_onRefreshCompletionWithError:(NSError *)error json:(NSDictionary *)json {
  197. if (json && !error) {
  198. RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json
  199. requireAccessToken:YES
  200. requireRefreshToken:NO];
  201. if (model) {
  202. return [self _handleSuccessfulRequest:model];
  203. }
  204. // Otherwise, malformed JSON
  205. unregisterRefreshHandle(_user, _path);
  206. [self.timer invalidate];
  207. NSError *error = make_sync_error(make_auth_error_bad_response(json));
  208. if (self.completionBlock) {
  209. self.completionBlock(error);
  210. }
  211. [[RLMSyncManager sharedManager] _fireError:error];
  212. } else {
  213. REALM_ASSERT(error);
  214. [self _handleFailedRequest:error];
  215. }
  216. return NO;
  217. }
  218. - (void)_timerFired:(__unused NSTimer *)timer {
  219. RLMServerToken refreshToken = nil;
  220. if (auto user = _user.lock()) {
  221. refreshToken = @(user->refresh_token().c_str());
  222. }
  223. if (!refreshToken) {
  224. unregisterRefreshHandle(_user, _path);
  225. [self.timer invalidate];
  226. return;
  227. }
  228. NSDictionary *json = @{
  229. kRLMSyncProviderKey: @"realm",
  230. kRLMSyncPathKey: @(_path.c_str()),
  231. kRLMSyncDataKey: refreshToken,
  232. kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
  233. };
  234. __weak RLMSyncSessionRefreshHandle *weakSelf = self;
  235. RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
  236. [weakSelf _onRefreshCompletionWithError:error json:json];
  237. };
  238. [RLMNetworkClient sendRequestToEndpoint:[RLMSyncAuthEndpoint endpoint]
  239. server:self.authServerURL
  240. JSON:json
  241. timeout:60
  242. options:[[RLMSyncManager sharedManager] networkRequestOptions]
  243. completion:handler];
  244. }
  245. @end