RLMSyncSessionRefreshHandle.mm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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. // Realm Cloud will give us a url prefix in the auth response that we need
  124. // to pass onto objectstore to have it connect to the proper sync worker
  125. if (model.urlPrefix) {
  126. session->set_url_prefix(model.urlPrefix.UTF8String);
  127. }
  128. // Calculate the resolved path.
  129. NSString *resolvedURLString = nil;
  130. RLMServerPath resolvedPath = model.accessToken.tokenData.path;
  131. // Munge the path back onto the original URL, because the `sync` API expects an entire URL.
  132. NSURLComponents *urlBuffer = [NSURLComponents componentsWithURL:self.realmURL
  133. resolvingAgainstBaseURL:YES];
  134. urlBuffer.path = resolvedPath;
  135. resolvedURLString = [[urlBuffer URL] absoluteString];
  136. if (!resolvedURLString) {
  137. @throw RLMException(@"Resolved path returned from the server was invalid (%@).", resolvedPath);
  138. }
  139. // Pass the token and resolved path to the underlying sync subsystem.
  140. session->refresh_access_token([model.accessToken.token UTF8String], {resolvedURLString.UTF8String});
  141. // Schedule a refresh. If we're successful we must already have `bind()`ed the session
  142. // initially, so we can null out the strong pointer.
  143. _strongSession = nullptr;
  144. NSDate *expires = [NSDate dateWithTimeIntervalSince1970:model.accessToken.tokenData.expires];
  145. [self scheduleRefreshTimer:expires];
  146. if (self.completionBlock) {
  147. self.completionBlock(nil);
  148. }
  149. return true;
  150. }
  151. /// Handler for network requests that failed before the JSON parsing stage.
  152. - (void)_handleFailedRequest:(NSError *)error {
  153. NSError *authError;
  154. if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
  155. // Network client may return sync related error
  156. authError = error;
  157. // Try to report this error to the expiration callback.
  158. reportInvalidAccessToken(_user, authError);
  159. } else {
  160. // Something else went wrong
  161. authError = make_auth_error_bad_response();
  162. }
  163. if (self.completionBlock) {
  164. self.completionBlock(authError);
  165. }
  166. [[RLMSyncManager sharedManager] _fireError:make_sync_error(authError)];
  167. // Certain errors related to network connectivity should trigger a retry.
  168. NSDate *nextTryDate = nil;
  169. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  170. switch (error.code) {
  171. case NSURLErrorCannotConnectToHost:
  172. case NSURLErrorNotConnectedToInternet:
  173. case NSURLErrorNetworkConnectionLost:
  174. case NSURLErrorTimedOut:
  175. case NSURLErrorDNSLookupFailed:
  176. case NSURLErrorCannotFindHost:
  177. // FIXME: 10 seconds is an arbitrarily chosen value, consider rationalizing it.
  178. nextTryDate = [NSDate dateWithTimeIntervalSinceNow:RLMRefreshBuffer + 10];
  179. break;
  180. default:
  181. break;
  182. }
  183. }
  184. if (!nextTryDate) {
  185. // This error isn't a network failure error. Just invalidate the refresh handle and stop.
  186. if (_strongSession) {
  187. _strongSession->log_out();
  188. }
  189. unregisterRefreshHandle(_user, _path);
  190. [self invalidate];
  191. return;
  192. }
  193. // If we tried to initially bind the session and failed, we'll try again. However, each
  194. // subsequent attempt will use a weak pointer to avoid prolonging the session's lifetime
  195. // unnecessarily.
  196. _strongSession = nullptr;
  197. [self scheduleRefreshTimer:nextTryDate];
  198. return;
  199. }
  200. /// Callback handler for network requests.
  201. - (BOOL)_onRefreshCompletionWithError:(NSError *)error json:(NSDictionary *)json {
  202. if (json && !error) {
  203. RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json
  204. requireAccessToken:YES
  205. requireRefreshToken:NO];
  206. if (model) {
  207. return [self _handleSuccessfulRequest:model];
  208. }
  209. // Otherwise, malformed JSON
  210. unregisterRefreshHandle(_user, _path);
  211. [self.timer invalidate];
  212. NSError *error = make_sync_error(make_auth_error_bad_response(json));
  213. if (self.completionBlock) {
  214. self.completionBlock(error);
  215. }
  216. [[RLMSyncManager sharedManager] _fireError:error];
  217. } else {
  218. REALM_ASSERT(error);
  219. [self _handleFailedRequest:error];
  220. }
  221. return NO;
  222. }
  223. - (void)_timerFired:(__unused NSTimer *)timer {
  224. RLMServerToken refreshToken = nil;
  225. if (auto user = _user.lock()) {
  226. refreshToken = @(user->refresh_token().c_str());
  227. }
  228. if (!refreshToken) {
  229. unregisterRefreshHandle(_user, _path);
  230. [self.timer invalidate];
  231. return;
  232. }
  233. NSDictionary *json = @{
  234. kRLMSyncProviderKey: @"realm",
  235. kRLMSyncPathKey: @(_path.c_str()),
  236. kRLMSyncDataKey: refreshToken,
  237. kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
  238. };
  239. __weak RLMSyncSessionRefreshHandle *weakSelf = self;
  240. RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
  241. [weakSelf _onRefreshCompletionWithError:error json:json];
  242. };
  243. [RLMSyncAuthEndpoint sendRequestToServer:self.authServerURL
  244. JSON:json
  245. timeout:60.0
  246. completion:handler];
  247. }
  248. @end