An amazing project that generates micro reports from tournament results
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pa-tournament-stats/Carthage/Checkouts/realm-cocoa/Realm/RLMSyncSessionRefreshHandle.mm

274 lines
10 KiB

////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
#import "RLMSyncSessionRefreshHandle.hpp"
#import "RLMJSONModels.h"
#import "RLMNetworkClient.h"
#import "RLMSyncManager_Private.h"
#import "RLMSyncUser_Private.hpp"
#import "RLMSyncUtil_Private.hpp"
#import "RLMUtil.hpp"
#import "sync/sync_session.hpp"
using namespace realm;
namespace {
void unregisterRefreshHandle(const std::weak_ptr<SyncUser>& user, const std::string& path) {
if (auto strong_user = user.lock()) {
context_for(strong_user).unregister_refresh_handle(path);
}
}
void reportInvalidAccessToken(const std::weak_ptr<SyncUser>& user, NSError *error) {
if (auto strong_user = user.lock()) {
if (RLMUserErrorReportingBlock block = context_for(strong_user).error_handler()) {
RLMSyncUser *theUser = [[RLMSyncUser alloc] initWithSyncUser:std::move(strong_user)];
[theUser logOut];
block(theUser, error);
}
}
}
}
static const NSTimeInterval RLMRefreshBuffer = 10;
@interface RLMSyncSessionRefreshHandle () {
std::weak_ptr<SyncUser> _user;
std::string _path;
std::weak_ptr<SyncSession> _session;
std::shared_ptr<SyncSession> _strongSession;
}
@property (nonatomic) NSTimer *timer;
@property (nonatomic) NSURL *realmURL;
@property (nonatomic) NSURL *authServerURL;
@property (nonatomic, copy) RLMSyncBasicErrorReportingBlock completionBlock;
@end
@implementation RLMSyncSessionRefreshHandle
- (instancetype)initWithRealmURL:(NSURL *)realmURL
user:(std::shared_ptr<realm::SyncUser>)user
session:(std::shared_ptr<realm::SyncSession>)session
completionBlock:(RLMSyncBasicErrorReportingBlock)completionBlock {
if (self = [super init]) {
NSString *path = [realmURL path];
_path = [path UTF8String];
self.authServerURL = [NSURL URLWithString:@(user->server_url().c_str())];
if (!self.authServerURL) {
@throw RLMException(@"User object isn't configured with an auth server URL.");
}
self.completionBlock = completionBlock;
self.realmURL = realmURL;
// For the initial bind, we want to prolong the session's lifetime.
_strongSession = std::move(session);
_session = _strongSession;
_user = user;
// Immediately fire off the network request.
[self _timerFired:nil];
return self;
}
return nil;
}
- (void)dealloc {
[self.timer invalidate];
}
- (void)invalidate {
_strongSession = nullptr;
[self.timer invalidate];
}
+ (NSDate *)fireDateForTokenExpirationDate:(NSDate *)date nowDate:(NSDate *)nowDate {
NSDate *fireDate = [date dateByAddingTimeInterval:-RLMRefreshBuffer];
// Only fire times in the future are valid.
return ([fireDate compare:nowDate] == NSOrderedDescending ? fireDate : nil);
}
- (void)scheduleRefreshTimer:(NSDate *)dateWhenTokenExpires {
// Schedule the timer on the main queue.
// It's very likely that this method will be run on a side thread, for example
// on the thread that runs `NSURLSession`'s completion blocks. We can't be
// guaranteed that there's an existing runloop on those threads, and we don't want
// to create and start a new one if one doesn't already exist.
dispatch_async(dispatch_get_main_queue(), ^{
[self.timer invalidate];
NSDate *fireDate = [RLMSyncSessionRefreshHandle fireDateForTokenExpirationDate:dateWhenTokenExpires
nowDate:[NSDate date]];
if (!fireDate) {
unregisterRefreshHandle(_user, _path);
return;
}
self.timer = [[NSTimer alloc] initWithFireDate:fireDate
interval:0
target:self
selector:@selector(_timerFired:)
userInfo:nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
});
}
/// Handler for network requests whose responses successfully parse into an auth response model.
- (BOOL)_handleSuccessfulRequest:(RLMAuthResponseModel *)model {
std::shared_ptr<SyncSession> session = _session.lock();
if (!session) {
// The session is dead or in a fatal error state.
unregisterRefreshHandle(_user, _path);
[self invalidate];
return NO;
}
// Calculate the resolved path.
NSString *resolvedURLString = nil;
RLMServerPath resolvedPath = model.accessToken.tokenData.path;
// Munge the path back onto the original URL, because the `sync` API expects an entire URL.
NSURLComponents *urlBuffer = [NSURLComponents componentsWithURL:self.realmURL
resolvingAgainstBaseURL:YES];
urlBuffer.path = resolvedPath;
resolvedURLString = [[urlBuffer URL] absoluteString];
if (!resolvedURLString) {
@throw RLMException(@"Resolved path returned from the server was invalid (%@).", resolvedPath);
}
// Pass the token and resolved path to the underlying sync subsystem.
session->refresh_access_token([model.accessToken.token UTF8String], {resolvedURLString.UTF8String});
// Schedule a refresh. If we're successful we must already have `bind()`ed the session
// initially, so we can null out the strong pointer.
_strongSession = nullptr;
NSDate *expires = [NSDate dateWithTimeIntervalSince1970:model.accessToken.tokenData.expires];
[self scheduleRefreshTimer:expires];
if (self.completionBlock) {
self.completionBlock(nil);
}
return true;
}
/// Handler for network requests that failed before the JSON parsing stage.
- (void)_handleFailedRequest:(NSError *)error {
NSError *authError;
if ([error.domain isEqualToString:RLMSyncAuthErrorDomain]) {
// Network client may return sync related error
authError = error;
// Try to report this error to the expiration callback.
reportInvalidAccessToken(_user, authError);
} else {
// Something else went wrong
authError = make_auth_error_bad_response();
}
if (self.completionBlock) {
self.completionBlock(authError);
}
[[RLMSyncManager sharedManager] _fireError:make_sync_error(authError)];
// Certain errors related to network connectivity should trigger a retry.
NSDate *nextTryDate = nil;
if ([error.domain isEqualToString:NSURLErrorDomain]) {
switch (error.code) {
case NSURLErrorCannotConnectToHost:
case NSURLErrorNotConnectedToInternet:
case NSURLErrorNetworkConnectionLost:
case NSURLErrorTimedOut:
case NSURLErrorDNSLookupFailed:
case NSURLErrorCannotFindHost:
// FIXME: 10 seconds is an arbitrarily chosen value, consider rationalizing it.
nextTryDate = [NSDate dateWithTimeIntervalSinceNow:RLMRefreshBuffer + 10];
break;
default:
break;
}
}
if (!nextTryDate) {
// This error isn't a network failure error. Just invalidate the refresh handle and stop.
if (_strongSession) {
_strongSession->log_out();
}
unregisterRefreshHandle(_user, _path);
[self invalidate];
return;
}
// If we tried to initially bind the session and failed, we'll try again. However, each
// subsequent attempt will use a weak pointer to avoid prolonging the session's lifetime
// unnecessarily.
_strongSession = nullptr;
[self scheduleRefreshTimer:nextTryDate];
return;
}
/// Callback handler for network requests.
- (BOOL)_onRefreshCompletionWithError:(NSError *)error json:(NSDictionary *)json {
if (json && !error) {
RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json
requireAccessToken:YES
requireRefreshToken:NO];
if (model) {
return [self _handleSuccessfulRequest:model];
}
// Otherwise, malformed JSON
unregisterRefreshHandle(_user, _path);
[self.timer invalidate];
NSError *error = make_sync_error(make_auth_error_bad_response(json));
if (self.completionBlock) {
self.completionBlock(error);
}
[[RLMSyncManager sharedManager] _fireError:error];
} else {
REALM_ASSERT(error);
[self _handleFailedRequest:error];
}
return NO;
}
- (void)_timerFired:(__unused NSTimer *)timer {
RLMServerToken refreshToken = nil;
if (auto user = _user.lock()) {
refreshToken = @(user->refresh_token().c_str());
}
if (!refreshToken) {
unregisterRefreshHandle(_user, _path);
[self.timer invalidate];
return;
}
NSDictionary *json = @{
kRLMSyncProviderKey: @"realm",
kRLMSyncPathKey: @(_path.c_str()),
kRLMSyncDataKey: refreshToken,
kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID,
};
__weak RLMSyncSessionRefreshHandle *weakSelf = self;
RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) {
[weakSelf _onRefreshCompletionWithError:error json:json];
};
[RLMNetworkClient sendRequestToEndpoint:[RLMSyncAuthEndpoint endpoint]
server:self.authServerURL
JSON:json
timeout:60
options:[[RLMSyncManager sharedManager] networkRequestOptions]
completion:handler];
}
@end