RLMSyncSessionRefreshHandle.mm 10 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) dispatch_source_t 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];
  75. return self;
  76. }
  77. return nil;
  78. }
  79. - (void)dealloc {
  80. [self cancelTimer];
  81. }
  82. - (void)invalidate {
  83. _strongSession = nullptr;
  84. [self cancelTimer];
  85. }
  86. - (void)cancelTimer {
  87. if (self.timer) {
  88. dispatch_source_cancel(self.timer);
  89. self.timer = nil;
  90. }
  91. }
  92. + (NSDate *)fireDateForTokenExpirationDate:(NSDate *)date nowDate:(NSDate *)nowDate {
  93. NSDate *fireDate = [date dateByAddingTimeInterval:-RLMRefreshBuffer];
  94. // Only fire times in the future are valid.
  95. return ([fireDate compare:nowDate] == NSOrderedDescending ? fireDate : nil);
  96. }
  97. - (void)scheduleRefreshTimer:(NSDate *)dateWhenTokenExpires {
  98. [self cancelTimer];
  99. NSDate *fireDate = [RLMSyncSessionRefreshHandle fireDateForTokenExpirationDate:dateWhenTokenExpires
  100. nowDate:[NSDate date]];
  101. if (!fireDate) {
  102. unregisterRefreshHandle(_user, _path);
  103. return;
  104. }
  105. NSTimeInterval timeToExpiration = [fireDate timeIntervalSinceNow];
  106. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
  107. dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, timeToExpiration * NSEC_PER_SEC),
  108. /* interval */ DISPATCH_TIME_FOREVER,
  109. /* leeway */ NSEC_PER_SEC * (timeToExpiration / 10));
  110. dispatch_source_set_event_handler(self.timer, ^{ [self _timerFired]; });
  111. dispatch_resume(self.timer);
  112. }
  113. /// Handler for network requests whose responses successfully parse into an auth response model.
  114. - (BOOL)_handleSuccessfulRequest:(RLMAuthResponseModel *)model session:(SyncSession&)session {
  115. // Realm Cloud will give us a url prefix in the auth response that we need
  116. // to pass onto objectstore to have it connect to the proper sync worker
  117. if (model.urlPrefix) {
  118. session.set_url_prefix(model.urlPrefix.UTF8String);
  119. }
  120. // Calculate the resolved path.
  121. NSString *resolvedURLString = nil;
  122. RLMServerPath resolvedPath = model.accessToken.tokenData.path;
  123. // Munge the path back onto the original URL, because the `sync` API expects an entire URL.
  124. NSURLComponents *urlBuffer = [NSURLComponents componentsWithURL:self.realmURL
  125. resolvingAgainstBaseURL:YES];
  126. urlBuffer.path = resolvedPath;
  127. resolvedURLString = [[urlBuffer URL] absoluteString];
  128. if (!resolvedURLString) {
  129. @throw RLMException(@"Resolved path returned from the server was invalid (%@).", resolvedPath);
  130. }
  131. // Pass the token and resolved path to the underlying sync subsystem.
  132. session.refresh_access_token([model.accessToken.token UTF8String], {resolvedURLString.UTF8String});
  133. // Schedule a refresh. If we're successful we must already have `bind()`ed the session
  134. // initially, so we can null out the strong pointer.
  135. _strongSession = nullptr;
  136. NSDate *expires = [NSDate dateWithTimeIntervalSince1970:model.accessToken.tokenData.expires];
  137. [self scheduleRefreshTimer:expires];
  138. if (self.completionBlock) {
  139. self.completionBlock(nil);
  140. }
  141. return true;
  142. }
  143. /// Handler for network requests that failed before the JSON parsing stage.
  144. - (void)_handleFailedRequest:(NSError *)error {
  145. NSError *authError;
  146. if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
  147. // Network client may return sync related error
  148. authError = error;
  149. // Try to report this error to the expiration callback.
  150. reportInvalidAccessToken(_user, authError);
  151. } else {
  152. // Something else went wrong
  153. authError = make_auth_error_bad_response();
  154. }
  155. if (self.completionBlock) {
  156. self.completionBlock(authError);
  157. }
  158. [[RLMSyncManager sharedManager] _fireError:make_sync_error(authError)];
  159. // Certain errors related to network connectivity should trigger a retry.
  160. NSDate *nextTryDate = nil;
  161. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  162. switch (error.code) {
  163. case NSURLErrorCannotConnectToHost:
  164. case NSURLErrorNotConnectedToInternet:
  165. case NSURLErrorNetworkConnectionLost:
  166. case NSURLErrorTimedOut:
  167. case NSURLErrorDNSLookupFailed:
  168. case NSURLErrorCannotFindHost:
  169. // FIXME: 10 seconds is an arbitrarily chosen value, consider rationalizing it.
  170. nextTryDate = [NSDate dateWithTimeIntervalSinceNow:RLMRefreshBuffer + 10];
  171. break;
  172. default:
  173. break;
  174. }
  175. }
  176. if (!nextTryDate) {
  177. // This error isn't a network failure error. Just invalidate the refresh handle and stop.
  178. if (_strongSession) {
  179. _strongSession->log_out();
  180. }
  181. unregisterRefreshHandle(_user, _path);
  182. [self invalidate];
  183. return;
  184. }
  185. // If we tried to initially bind the session and failed, we'll try again. However, each
  186. // subsequent attempt will use a weak pointer to avoid prolonging the session's lifetime
  187. // unnecessarily.
  188. _strongSession = nullptr;
  189. [self scheduleRefreshTimer:nextTryDate];
  190. return;
  191. }
  192. /// Callback handler for network requests.
  193. - (BOOL)_onRefreshCompletionWithError:(NSError *)error json:(NSDictionary *)json {
  194. std::shared_ptr<SyncSession> session = _session.lock();
  195. if (!session) {
  196. // The session is dead or in a fatal error state.
  197. unregisterRefreshHandle(_user, _path);
  198. [self invalidate];
  199. return NO;
  200. }
  201. if (json && !error) {
  202. RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json
  203. requireAccessToken:YES
  204. requireRefreshToken:NO];
  205. if (model) {
  206. return [self _handleSuccessfulRequest:model session:*session];
  207. }
  208. // Otherwise, malformed JSON
  209. unregisterRefreshHandle(_user, _path);
  210. NSError *error = make_sync_error(make_auth_error_bad_response(json));
  211. if (self.completionBlock) {
  212. self.completionBlock(error);
  213. }
  214. [[RLMSyncManager sharedManager] _fireError:error];
  215. } else {
  216. REALM_ASSERT(error);
  217. [self _handleFailedRequest:error];
  218. }
  219. return NO;
  220. }
  221. - (void)_timerFired {
  222. RLMServerToken refreshToken = nil;
  223. if (auto user = _user.lock()) {
  224. refreshToken = @(user->refresh_token().c_str());
  225. }
  226. if (!refreshToken) {
  227. unregisterRefreshHandle(_user, _path);
  228. return;
  229. }
  230. NSDictionary *json = @{
  231. kRLMSyncProviderKey: @"realm",
  232. kRLMSyncPathKey: @(_path.c_str()),
  233. kRLMSyncDataKey: refreshToken,
  234. kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
  235. };
  236. __weak RLMSyncSessionRefreshHandle *weakSelf = self;
  237. RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
  238. [weakSelf _onRefreshCompletionWithError:error json:json];
  239. };
  240. [RLMSyncAuthEndpoint sendRequestToServer:self.authServerURL
  241. JSON:json
  242. timeout:60.0
  243. completion:handler];
  244. }
  245. @end