added iOS source code
[wl-app.git] / iOS / Pods / Realm / Realm / RLMSyncSessionRefreshHandle.mm
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
19 #import "RLMSyncSessionRefreshHandle.hpp"
20
21 #import "RLMJSONModels.h"
22 #import "RLMNetworkClient.h"
23 #import "RLMSyncManager_Private.h"
24 #import "RLMSyncUser_Private.hpp"
25 #import "RLMSyncUtil_Private.hpp"
26 #import "RLMUtil.hpp"
27
28 #import "sync/sync_session.hpp"
29
30 using namespace realm;
31
32 namespace {
33
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);
37     }
38 }
39
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)];
44             [theUser logOut];
45             block(theUser, error);
46         }
47     }
48 }
49
50 }
51
52 static const NSTimeInterval RLMRefreshBuffer = 10;
53
54 @interface RLMSyncSessionRefreshHandle () {
55     std::weak_ptr<SyncUser> _user;
56     std::string _path;
57     std::weak_ptr<SyncSession> _session;
58     std::shared_ptr<SyncSession> _strongSession;
59 }
60
61 @property (nonatomic) NSTimer *timer;
62
63 @property (nonatomic) NSURL *realmURL;
64 @property (nonatomic) NSURL *authServerURL;
65 @property (nonatomic, copy) RLMSyncBasicErrorReportingBlock completionBlock;
66
67 @end
68
69 @implementation RLMSyncSessionRefreshHandle
70
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.");
81         }
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;
87         _user = user;
88         // Immediately fire off the network request.
89         [self _timerFired:nil];
90         return self;
91     }
92     return nil;
93 }
94
95 - (void)dealloc {
96     [self.timer invalidate];
97 }
98
99 - (void)invalidate {
100     _strongSession = nullptr;
101     [self.timer invalidate];
102 }
103
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);
108 }
109
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]];
120         if (!fireDate) {
121             unregisterRefreshHandle(_user, _path);
122             return;
123         }
124         self.timer = [[NSTimer alloc] initWithFireDate:fireDate
125                                               interval:0
126                                                 target:self
127                                               selector:@selector(_timerFired:)
128                                               userInfo:nil
129                                                repeats:NO];
130         [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
131     });
132 }
133
134 /// Handler for network requests whose responses successfully parse into an auth response model.
135 - (BOOL)_handleSuccessfulRequest:(RLMAuthResponseModel *)model {
136     // Success
137     std::shared_ptr<SyncSession> session = _session.lock();
138     if (!session) {
139         // The session is dead or in a fatal error state.
140         unregisterRefreshHandle(_user, _path);
141         [self invalidate];
142         return NO;
143     }
144     bool success = session->state() != SyncSession::PublicState::Error;
145     if (success) {
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);
156         }
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;
160         if (success) {
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];
166         } else {
167             // The session is dead or in a fatal error state.
168             unregisterRefreshHandle(_user, _path);
169             [self invalidate];
170         }
171     }
172     if (self.completionBlock) {
173         self.completionBlock(success ? nil : make_auth_error_client_issue());
174     }
175     return success;
176 }
177
178 /// Handler for network requests that failed before the JSON parsing stage.
179 - (void)_handleFailedRequest:(NSError *)error {
180     NSError *authError;
181     if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
182         // Network client may return sync related error
183         authError = error;
184         // Try to report this error to the expiration callback.
185         reportInvalidAccessToken(_user, authError);
186     } else {
187         // Something else went wrong
188         authError = make_auth_error_bad_response();
189     }
190     if (self.completionBlock) {
191         self.completionBlock(authError);
192     }
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];
206                 break;
207             default:
208                 break;
209         }
210     }
211     if (!nextTryDate) {
212         // This error isn't a network failure error. Just invalidate the refresh handle and stop.
213         unregisterRefreshHandle(_user, _path);
214         [self invalidate];
215         return;
216     }
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
219     // unnecessarily.
220     _strongSession = nullptr;
221     [self scheduleRefreshTimer:nextTryDate];
222     return;
223 }
224
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];
231         if (model) {
232             return [self _handleSuccessfulRequest:model];
233         }
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);
240         }
241         [[RLMSyncManager sharedManager] _fireError:error];
242     } else {
243         REALM_ASSERT(error);
244         [self _handleFailedRequest:error];
245     }
246     return NO;
247 }
248
249 - (void)_timerFired:(__unused NSTimer *)timer {
250     RLMServerToken refreshToken = nil;
251     if (auto user = _user.lock()) {
252         refreshToken = @(user->refresh_token().c_str());
253     }
254     if (!refreshToken) {
255         unregisterRefreshHandle(_user, _path);
256         [self.timer invalidate];
257         return;
258     }
259
260     NSDictionary *json = @{
261                            kRLMSyncProviderKey: @"realm",
262                            kRLMSyncPathKey: @(_path.c_str()),
263                            kRLMSyncDataKey: refreshToken,
264                            kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
265                            };
266
267     __weak RLMSyncSessionRefreshHandle *weakSelf = self;
268     RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
269         [weakSelf _onRefreshCompletionWithError:error json:json];
270     };
271     [RLMNetworkClient sendRequestToEndpoint:[RLMSyncAuthEndpoint endpoint]
272                                      server:self.authServerURL
273                                        JSON:json
274                                  completion:handler];
275 }
276
277 @end