123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- ////////////////////////////////////////////////////////////////////////////
- //
- // 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 "RLMNetworkClient.h"
- #import "RLMRealmConfiguration.h"
- #import "RLMJSONModels.h"
- #import "RLMSyncUtil_Private.hpp"
- #import "RLMUtil.hpp"
- #import <realm/util/scope_exit.hpp>
- typedef void(^RLMServerURLSessionCompletionBlock)(NSData *, NSURLResponse *, NSError *);
- static NSUInteger const kHTTPCodeRange = 100;
- typedef enum : NSUInteger {
- Informational = 1, // 1XX
- Success = 2, // 2XX
- Redirection = 3, // 3XX
- ClientError = 4, // 4XX
- ServerError = 5, // 5XX
- } RLMServerHTTPErrorCodeType;
- static NSRange rangeForErrorType(RLMServerHTTPErrorCodeType type) {
- return NSMakeRange(type*100, kHTTPCodeRange);
- }
- static std::atomic<NSTimeInterval> g_defaultTimeout{60.0};
- @interface RLMSyncServerEndpoint ()
- - (instancetype)initPrivate NS_DESIGNATED_INITIALIZER;
- /// The HTTP method the endpoint expects. Defaults to POST.
- - (NSString *)httpMethod;
- /// The URL to which the request should be made. Must be implemented.
- - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json;
- /// The body for the request, if any.
- - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error;
- /// The HTTP headers to be added to the request, if any.
- - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json
- options:(nullable RLMNetworkRequestOptions *)options;
- @end
- @implementation RLMSyncServerEndpoint
- + (void)sendRequestToServer:(NSURL *)serverURL
- JSON:(NSDictionary *)jsonDictionary
- options:(nullable RLMNetworkRequestOptions *)options
- completion:(void (^)(NSError *))completionBlock {
- [RLMNetworkClient sendRequestToEndpoint:[self endpoint]
- server:serverURL
- JSON:jsonDictionary
- timeout:g_defaultTimeout.load()
- options:options
- completion:^(NSError *error, NSDictionary *) {
- completionBlock(error);
- }];
- }
- + (instancetype)endpoint {
- return [[self alloc] initPrivate];
- }
- - (instancetype)initPrivate {
- return (self = [super init]);
- }
- - (NSString *)httpMethod {
- return @"POST";
- }
- - (NSURL *)urlForAuthServer:(__unused NSURL *)authServerURL payload:(__unused NSDictionary *)json {
- NSAssert(NO, @"This method must be overriden by concrete subclasses.");
- return nil;
- }
- - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error {
- NSError *localError = nil;
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:json
- options:(NSJSONWritingOptions)0
- error:&localError];
- if (jsonData && !localError) {
- return jsonData;
- }
- NSAssert(localError, @"If there isn't a converted data object there must be an error.");
- if (error) {
- *error = localError;
- }
- return nil;
- }
- - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(__unused NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
- NSMutableDictionary<NSString *, NSString *> *headers = [[NSMutableDictionary alloc] init];
- headers[@"Content-Type"] = @"application/json;charset=utf-8";
- headers[@"Accept"] = @"application/json";
- if (NSDictionary<NSString *, NSString *> *customHeaders = options.customHeaders) {
- [headers addEntriesFromDictionary:customHeaders];
- }
- return headers;
- }
- @end
- @implementation RLMSyncAuthEndpoint
- - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
- return [authServerURL URLByAppendingPathComponent:@"auth"];
- }
- @end
- @implementation RLMSyncChangePasswordEndpoint
- - (NSString *)httpMethod {
- return @"PUT";
- }
- - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
- return [authServerURL URLByAppendingPathComponent:@"auth/password"];
- }
- - (NSDictionary *)httpHeadersForPayload:(NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
- NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
- if (!authToken) {
- @throw RLMException(@"Malformed request; this indicates an internal error.");
- }
- NSMutableDictionary *headers = [[super httpHeadersForPayload:json options:options] mutableCopy];
- headers[options.authorizationHeaderName ?: @"Authorization"] = authToken;
- return headers;
- }
- @end
- @implementation RLMSyncUpdateAccountEndpoint
- - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
- return [authServerURL URLByAppendingPathComponent:@"auth/password/updateAccount"];
- }
- @end
- @implementation RLMSyncGetUserInfoEndpoint
- - (NSString *)httpMethod {
- return @"GET";
- }
- - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json {
- NSString *provider = json[kRLMSyncProviderKey];
- NSString *providerID = json[kRLMSyncProviderIDKey];
- NSAssert([provider isKindOfClass:[NSString class]] && [providerID isKindOfClass:[NSString class]],
- @"malformed request; this indicates a logic error in the binding.");
- NSCharacterSet *allowed = [NSCharacterSet URLQueryAllowedCharacterSet];
- NSString *pathComponent = [NSString stringWithFormat:@"auth/users/%@/%@",
- [provider stringByAddingPercentEncodingWithAllowedCharacters:allowed],
- [providerID stringByAddingPercentEncodingWithAllowedCharacters:allowed]];
- return [authServerURL URLByAppendingPathComponent:pathComponent];
- }
- - (NSData *)httpBodyForPayload:(__unused NSDictionary *)json error:(__unused NSError **)error {
- return nil;
- }
- - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
- NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
- if (!authToken) {
- @throw RLMException(@"Malformed request; this indicates an internal error.");
- }
- NSMutableDictionary *headers = [[super httpHeadersForPayload:json options:options] mutableCopy];
- headers[options.authorizationHeaderName ?: @"Authorization"] = authToken;
- return headers;
- }
- @end
- @interface RLMSessionDelegate <NSURLSessionDelegate> : NSObject
- @end
- @implementation RLMSessionDelegate {
- NSDictionary<NSString *, NSURL *> *_certificatePaths;
- NSData *_data;
- void (^_completionBlock)(NSError *, NSDictionary *);
- }
- + (instancetype)delegateWithCertificatePaths:(NSDictionary *)paths completion:(void (^)(NSError *, NSDictionary *))completion {
- RLMSessionDelegate *delegate = [RLMSessionDelegate new];
- delegate->_certificatePaths = paths;
- delegate->_completionBlock = completion;
- return delegate;
- }
- - (void)URLSession:(__unused NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
- completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
- auto protectionSpace = challenge.protectionSpace;
- // Just fall back to the default logic for HTTP basic auth
- if (protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust || !protectionSpace.serverTrust) {
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
- return;
- }
- // If we have a pinned certificate for this hostname, we want to validate
- // against that, and otherwise just do the default thing
- auto certPath = _certificatePaths[protectionSpace.host];
- if (!certPath) {
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
- return;
- }
- if ([certPath isKindOfClass:[NSString class]]) {
- certPath = [NSURL fileURLWithPath:(id)certPath];
- }
- // Reject the server auth and report an error if any errors occur along the way
- CFArrayRef items = nil;
- NSError *error;
- auto reportStatus = realm::util::make_scope_exit([&]() noexcept {
- if (items) {
- CFRelease(items);
- }
- if (error) {
- _completionBlock(error, nil);
- // Don't also report errors about the connection itself failing later
- _completionBlock = ^(NSError *, id) { };
- completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
- }
- });
- NSData *data = [NSData dataWithContentsOfURL:certPath options:0 error:&error];
- if (!data) {
- return;
- }
- // Load our pinned certificate and add it to the anchor set
- #if TARGET_OS_IPHONE
- id certificate = (__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
- if (!certificate) {
- error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnknownFormat userInfo:nil];
- return;
- }
- items = (CFArrayRef)CFBridgingRetain(@[certificate]);
- #else
- SecItemImportExportKeyParameters params{
- .version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION
- };
- if (OSStatus status = SecItemImport((__bridge CFDataRef)data, (__bridge CFStringRef)certPath.absoluteString,
- nullptr, nullptr, 0, ¶ms, nullptr, &items)) {
- error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
- return;
- }
- #endif
- SecTrustRef serverTrust = protectionSpace.serverTrust;
- if (OSStatus status = SecTrustSetAnchorCertificates(serverTrust, items)) {
- error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
- return;
- }
- // Only use our certificate and not the ones from the default CA roots
- if (OSStatus status = SecTrustSetAnchorCertificatesOnly(serverTrust, true)) {
- error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
- return;
- }
- // Verify that our pinned certificate is valid for this connection
- SecTrustResultType trustResult;
- if (OSStatus status = SecTrustEvaluate(serverTrust, &trustResult)) {
- error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
- return;
- }
- if (trustResult != kSecTrustResultProceed && trustResult != kSecTrustResultUnspecified) {
- completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
- return;
- }
- completionHandler(NSURLSessionAuthChallengeUseCredential,
- [NSURLCredential credentialForTrust:protectionSpace.serverTrust]);
- }
- - (void)URLSession:(__unused NSURLSession *)session
- dataTask:(__unused NSURLSessionDataTask *)dataTask
- didReceiveData:(NSData *)data {
- if (!_data) {
- _data = data;
- return;
- }
- if (![_data respondsToSelector:@selector(appendData:)]) {
- _data = [_data mutableCopy];
- }
- [(id)_data appendData:data];
- }
- - (void)URLSession:(__unused NSURLSession *)session
- task:(NSURLSessionTask *)task
- didCompleteWithError:(NSError *)error
- {
- if (error) {
- _completionBlock(error, nil);
- return;
- }
- if (NSError *error = [self validateResponse:task.response data:_data]) {
- _completionBlock(error, nil);
- return;
- }
- id json = [NSJSONSerialization JSONObjectWithData:_data
- options:(NSJSONReadingOptions)0
- error:&error];
- if (!json) {
- _completionBlock(error, nil);
- return;
- }
- if (![json isKindOfClass:[NSDictionary class]]) {
- _completionBlock(make_auth_error_bad_response(json), nil);
- return;
- }
- _completionBlock(nil, (NSDictionary *)json);
- }
- - (NSError *)validateResponse:(NSURLResponse *)response data:(NSData *)data {
- if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
- // FIXME: Provide error message
- return make_auth_error_bad_response();
- }
- NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
- BOOL badResponse = (NSLocationInRange(httpResponse.statusCode, rangeForErrorType(ClientError))
- || NSLocationInRange(httpResponse.statusCode, rangeForErrorType(ServerError)));
- if (badResponse) {
- if (RLMSyncErrorResponseModel *responseModel = [self responseModelFromData:data]) {
- switch (responseModel.code) {
- case RLMSyncAuthErrorInvalidParameters:
- case RLMSyncAuthErrorMissingPath:
- case RLMSyncAuthErrorInvalidCredential:
- case RLMSyncAuthErrorUserDoesNotExist:
- case RLMSyncAuthErrorUserAlreadyExists:
- case RLMSyncAuthErrorAccessDeniedOrInvalidPath:
- case RLMSyncAuthErrorInvalidAccessToken:
- case RLMSyncAuthErrorExpiredPermissionOffer:
- case RLMSyncAuthErrorAmbiguousPermissionOffer:
- case RLMSyncAuthErrorFileCannotBeShared:
- return make_auth_error(responseModel);
- default:
- // Right now we assume that any codes not described
- // above are generic HTTP error codes.
- return make_auth_error_http_status(responseModel.status);
- }
- }
- return make_auth_error_http_status(httpResponse.statusCode);
- }
- if (!data) {
- // FIXME: provide error message
- return make_auth_error_bad_response();
- }
- return nil;
- }
- - (RLMSyncErrorResponseModel *)responseModelFromData:(NSData *)data {
- if (data.length == 0) {
- return nil;
- }
- id json = [NSJSONSerialization JSONObjectWithData:data
- options:(NSJSONReadingOptions)0
- error:nil];
- if (!json || ![json isKindOfClass:[NSDictionary class]]) {
- return nil;
- }
- return [[RLMSyncErrorResponseModel alloc] initWithDictionary:json];
- }
- @end
- @implementation RLMNetworkClient
- + (void)setDefaultTimeout:(NSTimeInterval)timeOut {
- g_defaultTimeout = timeOut;
- }
- + (void)sendRequestToEndpoint:(RLMSyncServerEndpoint *)endpoint
- server:(NSURL *)serverURL
- JSON:(NSDictionary *)jsonDictionary
- timeout:(NSTimeInterval)timeout
- options:(nullable RLMNetworkRequestOptions *)options
- completion:(RLMSyncCompletionBlock)completionBlock {
- // Create the request
- NSError *localError = nil;
- NSURL *requestURL = [endpoint urlForAuthServer:serverURL payload:jsonDictionary];
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
- request.HTTPBody = [endpoint httpBodyForPayload:jsonDictionary error:&localError];
- if (localError) {
- completionBlock(localError, nil);
- return;
- }
- request.HTTPMethod = [endpoint httpMethod];
- request.timeoutInterval = MAX(timeout, 10);
- NSDictionary<NSString *, NSString *> *headers = [endpoint httpHeadersForPayload:jsonDictionary options:options];
- for (NSString *key in headers) {
- [request addValue:headers[key] forHTTPHeaderField:key];
- }
- id delegate = [RLMSessionDelegate delegateWithCertificatePaths:options.pinnedCertificatePaths
- completion:completionBlock];
- auto session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration
- delegate:delegate delegateQueue:nil];
- // Add the request to a task and start it
- [[session dataTaskWithRequest:request] resume];
- // Tell the session to destroy itself once it's done with the request
- [session finishTasksAndInvalidate];
- }
- @end
- @implementation RLMNetworkRequestOptions
- @end
|