1 ////////////////////////////////////////////////////////////////////////////
3 // Copyright 2016 Realm Inc.
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
9 // http://www.apache.org/licenses/LICENSE-2.0
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.
17 ////////////////////////////////////////////////////////////////////////////
19 #import "RLMSyncSessionRefreshHandle.hpp"
21 #import "RLMJSONModels.h"
22 #import "RLMNetworkClient.h"
23 #import "RLMSyncManager_Private.h"
24 #import "RLMSyncUser_Private.hpp"
25 #import "RLMSyncUtil_Private.hpp"
28 #import "sync/sync_session.hpp"
30 using namespace realm;
34 void unregisterRefreshHandle(const std::weak_ptr<SyncUser>& user, const std::string& path) {
35 if (auto strong_user = user.lock()) {
36 context_for(strong_user).unregister_refresh_handle(path);
40 void reportInvalidAccessToken(const std::weak_ptr<SyncUser>& user, NSError *error) {
41 if (auto strong_user = user.lock()) {
42 if (RLMUserErrorReportingBlock block = context_for(strong_user).error_handler()) {
43 RLMSyncUser *theUser = [[RLMSyncUser alloc] initWithSyncUser:std::move(strong_user)];
45 block(theUser, error);
52 static const NSTimeInterval RLMRefreshBuffer = 10;
54 @interface RLMSyncSessionRefreshHandle () {
55 std::weak_ptr<SyncUser> _user;
57 std::weak_ptr<SyncSession> _session;
58 std::shared_ptr<SyncSession> _strongSession;
61 @property (nonatomic) NSTimer *timer;
63 @property (nonatomic) NSURL *realmURL;
64 @property (nonatomic) NSURL *authServerURL;
65 @property (nonatomic, copy) RLMSyncBasicErrorReportingBlock completionBlock;
69 @implementation RLMSyncSessionRefreshHandle
71 - (instancetype)initWithRealmURL:(NSURL *)realmURL
72 user:(std::shared_ptr<realm::SyncUser>)user
73 session:(std::shared_ptr<realm::SyncSession>)session
74 completionBlock:(RLMSyncBasicErrorReportingBlock)completionBlock {
75 if (self = [super init]) {
76 NSString *path = [realmURL path];
77 _path = [path UTF8String];
78 self.authServerURL = [NSURL URLWithString:@(user->server_url().c_str())];
79 if (!self.authServerURL) {
80 @throw RLMException(@"User object isn't configured with an auth server URL.");
82 self.completionBlock = completionBlock;
83 self.realmURL = realmURL;
84 // For the initial bind, we want to prolong the session's lifetime.
85 _strongSession = std::move(session);
86 _session = _strongSession;
88 // Immediately fire off the network request.
89 [self _timerFired:nil];
96 [self.timer invalidate];
100 _strongSession = nullptr;
101 [self.timer invalidate];
104 + (NSDate *)fireDateForTokenExpirationDate:(NSDate *)date nowDate:(NSDate *)nowDate {
105 NSDate *fireDate = [date dateByAddingTimeInterval:-RLMRefreshBuffer];
106 // Only fire times in the future are valid.
107 return ([fireDate compare:nowDate] == NSOrderedDescending ? fireDate : nil);
110 - (void)scheduleRefreshTimer:(NSDate *)dateWhenTokenExpires {
111 // Schedule the timer on the main queue.
112 // It's very likely that this method will be run on a side thread, for example
113 // on the thread that runs `NSURLSession`'s completion blocks. We can't be
114 // guaranteed that there's an existing runloop on those threads, and we don't want
115 // to create and start a new one if one doesn't already exist.
116 dispatch_async(dispatch_get_main_queue(), ^{
117 [self.timer invalidate];
118 NSDate *fireDate = [RLMSyncSessionRefreshHandle fireDateForTokenExpirationDate:dateWhenTokenExpires
119 nowDate:[NSDate date]];
121 unregisterRefreshHandle(_user, _path);
124 self.timer = [[NSTimer alloc] initWithFireDate:fireDate
127 selector:@selector(_timerFired:)
130 [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
134 /// Handler for network requests whose responses successfully parse into an auth response model.
135 - (BOOL)_handleSuccessfulRequest:(RLMAuthResponseModel *)model {
137 std::shared_ptr<SyncSession> session = _session.lock();
139 // The session is dead or in a fatal error state.
140 unregisterRefreshHandle(_user, _path);
144 bool success = session->state() != SyncSession::PublicState::Error;
146 // Calculate the resolved path.
147 NSString *resolvedURLString = nil;
148 RLMServerPath resolvedPath = model.accessToken.tokenData.path;
149 // Munge the path back onto the original URL, because the `sync` API expects an entire URL.
150 NSURLComponents *urlBuffer = [NSURLComponents componentsWithURL:self.realmURL
151 resolvingAgainstBaseURL:YES];
152 urlBuffer.path = resolvedPath;
153 resolvedURLString = [[urlBuffer URL] absoluteString];
154 if (!resolvedURLString) {
155 @throw RLMException(@"Resolved path returned from the server was invalid (%@).", resolvedPath);
157 // Pass the token and resolved path to the underlying sync subsystem.
158 session->refresh_access_token([model.accessToken.token UTF8String], {resolvedURLString.UTF8String});
159 success = session->state() != SyncSession::PublicState::Error;
161 // Schedule a refresh. If we're successful we must already have `bind()`ed the session
162 // initially, so we can null out the strong pointer.
163 _strongSession = nullptr;
164 NSDate *expires = [NSDate dateWithTimeIntervalSince1970:model.accessToken.tokenData.expires];
165 [self scheduleRefreshTimer:expires];
167 // The session is dead or in a fatal error state.
168 unregisterRefreshHandle(_user, _path);
172 if (self.completionBlock) {
173 self.completionBlock(success ? nil : make_auth_error_client_issue());
178 /// Handler for network requests that failed before the JSON parsing stage.
179 - (void)_handleFailedRequest:(NSError *)error {
181 if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
182 // Network client may return sync related error
184 // Try to report this error to the expiration callback.
185 reportInvalidAccessToken(_user, authError);
187 // Something else went wrong
188 authError = make_auth_error_bad_response();
190 if (self.completionBlock) {
191 self.completionBlock(authError);
193 [[RLMSyncManager sharedManager] _fireError:make_sync_error(authError)];
194 // Certain errors related to network connectivity should trigger a retry.
195 NSDate *nextTryDate = nil;
196 if ([error.domain isEqualToString:NSURLErrorDomain]) {
197 switch (error.code) {
198 case NSURLErrorCannotConnectToHost:
199 case NSURLErrorNotConnectedToInternet:
200 case NSURLErrorNetworkConnectionLost:
201 case NSURLErrorTimedOut:
202 case NSURLErrorDNSLookupFailed:
203 case NSURLErrorCannotFindHost:
204 // FIXME: 10 seconds is an arbitrarily chosen value, consider rationalizing it.
205 nextTryDate = [NSDate dateWithTimeIntervalSinceNow:RLMRefreshBuffer + 10];
212 // This error isn't a network failure error. Just invalidate the refresh handle and stop.
213 unregisterRefreshHandle(_user, _path);
217 // If we tried to initially bind the session and failed, we'll try again. However, each
218 // subsequent attempt will use a weak pointer to avoid prolonging the session's lifetime
220 _strongSession = nullptr;
221 [self scheduleRefreshTimer:nextTryDate];
225 /// Callback handler for network requests.
226 - (BOOL)_onRefreshCompletionWithError:(NSError *)error json:(NSDictionary *)json {
227 if (json && !error) {
228 RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json
229 requireAccessToken:YES
230 requireRefreshToken:NO];
232 return [self _handleSuccessfulRequest:model];
234 // Otherwise, malformed JSON
235 unregisterRefreshHandle(_user, _path);
236 [self.timer invalidate];
237 NSError *error = make_sync_error(make_auth_error_bad_response(json));
238 if (self.completionBlock) {
239 self.completionBlock(error);
241 [[RLMSyncManager sharedManager] _fireError:error];
244 [self _handleFailedRequest:error];
249 - (void)_timerFired:(__unused NSTimer *)timer {
250 RLMServerToken refreshToken = nil;
251 if (auto user = _user.lock()) {
252 refreshToken = @(user->refresh_token().c_str());
255 unregisterRefreshHandle(_user, _path);
256 [self.timer invalidate];
260 NSDictionary *json = @{
261 kRLMSyncProviderKey: @"realm",
262 kRLMSyncPathKey: @(_path.c_str()),
263 kRLMSyncDataKey: refreshToken,
264 kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
267 __weak RLMSyncSessionRefreshHandle *weakSelf = self;
268 RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
269 [weakSelf _onRefreshCompletionWithError:error json:json];
271 [RLMNetworkClient sendRequestToEndpoint:[RLMSyncAuthEndpoint endpoint]
272 server:self.authServerURL