RLMNetworkClient.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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 "RLMNetworkClient.h"
  19. #import "RLMRealmConfiguration.h"
  20. #import "RLMJSONModels.h"
  21. #import "RLMSyncUtil_Private.hpp"
  22. #import "RLMUtil.hpp"
  23. #import <realm/util/scope_exit.hpp>
  24. typedef void(^RLMServerURLSessionCompletionBlock)(NSData *, NSURLResponse *, NSError *);
  25. static NSUInteger const kHTTPCodeRange = 100;
  26. typedef enum : NSUInteger {
  27. Informational = 1, // 1XX
  28. Success = 2, // 2XX
  29. Redirection = 3, // 3XX
  30. ClientError = 4, // 4XX
  31. ServerError = 5, // 5XX
  32. } RLMServerHTTPErrorCodeType;
  33. static NSRange rangeForErrorType(RLMServerHTTPErrorCodeType type) {
  34. return NSMakeRange(type*100, kHTTPCodeRange);
  35. }
  36. static std::atomic<NSTimeInterval> g_defaultTimeout{60.0};
  37. @interface RLMSyncServerEndpoint ()
  38. - (instancetype)initPrivate NS_DESIGNATED_INITIALIZER;
  39. /// The HTTP method the endpoint expects. Defaults to POST.
  40. - (NSString *)httpMethod;
  41. /// The URL to which the request should be made. Must be implemented.
  42. - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json;
  43. /// The body for the request, if any.
  44. - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error;
  45. /// The HTTP headers to be added to the request, if any.
  46. - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json
  47. options:(nullable RLMNetworkRequestOptions *)options;
  48. @end
  49. @implementation RLMSyncServerEndpoint
  50. + (void)sendRequestToServer:(NSURL *)serverURL
  51. JSON:(NSDictionary *)jsonDictionary
  52. options:(nullable RLMNetworkRequestOptions *)options
  53. completion:(void (^)(NSError *))completionBlock {
  54. [RLMNetworkClient sendRequestToEndpoint:[self endpoint]
  55. server:serverURL
  56. JSON:jsonDictionary
  57. timeout:g_defaultTimeout.load()
  58. options:options
  59. completion:^(NSError *error, NSDictionary *) {
  60. completionBlock(error);
  61. }];
  62. }
  63. + (instancetype)endpoint {
  64. return [[self alloc] initPrivate];
  65. }
  66. - (instancetype)initPrivate {
  67. return (self = [super init]);
  68. }
  69. - (NSString *)httpMethod {
  70. return @"POST";
  71. }
  72. - (NSURL *)urlForAuthServer:(__unused NSURL *)authServerURL payload:(__unused NSDictionary *)json {
  73. NSAssert(NO, @"This method must be overriden by concrete subclasses.");
  74. return nil;
  75. }
  76. - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error {
  77. NSError *localError = nil;
  78. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:json
  79. options:(NSJSONWritingOptions)0
  80. error:&localError];
  81. if (jsonData && !localError) {
  82. return jsonData;
  83. }
  84. NSAssert(localError, @"If there isn't a converted data object there must be an error.");
  85. if (error) {
  86. *error = localError;
  87. }
  88. return nil;
  89. }
  90. - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(__unused NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
  91. NSMutableDictionary<NSString *, NSString *> *headers = [[NSMutableDictionary alloc] init];
  92. headers[@"Content-Type"] = @"application/json;charset=utf-8";
  93. headers[@"Accept"] = @"application/json";
  94. if (NSDictionary<NSString *, NSString *> *customHeaders = options.customHeaders) {
  95. [headers addEntriesFromDictionary:customHeaders];
  96. }
  97. return headers;
  98. }
  99. @end
  100. @implementation RLMSyncAuthEndpoint
  101. - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
  102. return [authServerURL URLByAppendingPathComponent:@"auth"];
  103. }
  104. @end
  105. @implementation RLMSyncChangePasswordEndpoint
  106. - (NSString *)httpMethod {
  107. return @"PUT";
  108. }
  109. - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
  110. return [authServerURL URLByAppendingPathComponent:@"auth/password"];
  111. }
  112. - (NSDictionary *)httpHeadersForPayload:(NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
  113. NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
  114. if (!authToken) {
  115. @throw RLMException(@"Malformed request; this indicates an internal error.");
  116. }
  117. NSMutableDictionary *headers = [[super httpHeadersForPayload:json options:options] mutableCopy];
  118. headers[options.authorizationHeaderName ?: @"Authorization"] = authToken;
  119. return headers;
  120. }
  121. @end
  122. @implementation RLMSyncUpdateAccountEndpoint
  123. - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
  124. return [authServerURL URLByAppendingPathComponent:@"auth/password/updateAccount"];
  125. }
  126. @end
  127. @implementation RLMSyncGetUserInfoEndpoint
  128. - (NSString *)httpMethod {
  129. return @"GET";
  130. }
  131. - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json {
  132. NSString *provider = json[kRLMSyncProviderKey];
  133. NSString *providerID = json[kRLMSyncProviderIDKey];
  134. NSAssert([provider isKindOfClass:[NSString class]] && [providerID isKindOfClass:[NSString class]],
  135. @"malformed request; this indicates a logic error in the binding.");
  136. NSCharacterSet *allowed = [NSCharacterSet URLQueryAllowedCharacterSet];
  137. NSString *pathComponent = [NSString stringWithFormat:@"auth/users/%@/%@",
  138. [provider stringByAddingPercentEncodingWithAllowedCharacters:allowed],
  139. [providerID stringByAddingPercentEncodingWithAllowedCharacters:allowed]];
  140. return [authServerURL URLByAppendingPathComponent:pathComponent];
  141. }
  142. - (NSData *)httpBodyForPayload:(__unused NSDictionary *)json error:(__unused NSError **)error {
  143. return nil;
  144. }
  145. - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json options:(nullable RLMNetworkRequestOptions *)options {
  146. NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
  147. if (!authToken) {
  148. @throw RLMException(@"Malformed request; this indicates an internal error.");
  149. }
  150. NSMutableDictionary *headers = [[super httpHeadersForPayload:json options:options] mutableCopy];
  151. headers[options.authorizationHeaderName ?: @"Authorization"] = authToken;
  152. return headers;
  153. }
  154. @end
  155. @interface RLMSessionDelegate <NSURLSessionDelegate> : NSObject
  156. @end
  157. @implementation RLMSessionDelegate {
  158. NSDictionary<NSString *, NSURL *> *_certificatePaths;
  159. NSData *_data;
  160. void (^_completionBlock)(NSError *, NSDictionary *);
  161. }
  162. + (instancetype)delegateWithCertificatePaths:(NSDictionary *)paths completion:(void (^)(NSError *, NSDictionary *))completion {
  163. RLMSessionDelegate *delegate = [RLMSessionDelegate new];
  164. delegate->_certificatePaths = paths;
  165. delegate->_completionBlock = completion;
  166. return delegate;
  167. }
  168. - (void)URLSession:(__unused NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
  169. completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
  170. auto protectionSpace = challenge.protectionSpace;
  171. // Just fall back to the default logic for HTTP basic auth
  172. if (protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust || !protectionSpace.serverTrust) {
  173. completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
  174. return;
  175. }
  176. // If we have a pinned certificate for this hostname, we want to validate
  177. // against that, and otherwise just do the default thing
  178. auto certPath = _certificatePaths[protectionSpace.host];
  179. if (!certPath) {
  180. completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
  181. return;
  182. }
  183. if ([certPath isKindOfClass:[NSString class]]) {
  184. certPath = [NSURL fileURLWithPath:(id)certPath];
  185. }
  186. // Reject the server auth and report an error if any errors occur along the way
  187. CFArrayRef items = nil;
  188. NSError *error;
  189. auto reportStatus = realm::util::make_scope_exit([&]() noexcept {
  190. if (items) {
  191. CFRelease(items);
  192. }
  193. if (error) {
  194. _completionBlock(error, nil);
  195. // Don't also report errors about the connection itself failing later
  196. _completionBlock = ^(NSError *, id) { };
  197. completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
  198. }
  199. });
  200. NSData *data = [NSData dataWithContentsOfURL:certPath options:0 error:&error];
  201. if (!data) {
  202. return;
  203. }
  204. // Load our pinned certificate and add it to the anchor set
  205. #if TARGET_OS_IPHONE
  206. id certificate = (__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
  207. if (!certificate) {
  208. error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecUnknownFormat userInfo:nil];
  209. return;
  210. }
  211. items = (CFArrayRef)CFBridgingRetain(@[certificate]);
  212. #else
  213. SecItemImportExportKeyParameters params{
  214. .version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION
  215. };
  216. if (OSStatus status = SecItemImport((__bridge CFDataRef)data, (__bridge CFStringRef)certPath.absoluteString,
  217. nullptr, nullptr, 0, &params, nullptr, &items)) {
  218. error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
  219. return;
  220. }
  221. #endif
  222. SecTrustRef serverTrust = protectionSpace.serverTrust;
  223. if (OSStatus status = SecTrustSetAnchorCertificates(serverTrust, items)) {
  224. error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
  225. return;
  226. }
  227. // Only use our certificate and not the ones from the default CA roots
  228. if (OSStatus status = SecTrustSetAnchorCertificatesOnly(serverTrust, true)) {
  229. error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
  230. return;
  231. }
  232. // Verify that our pinned certificate is valid for this connection
  233. SecTrustResultType trustResult;
  234. if (OSStatus status = SecTrustEvaluate(serverTrust, &trustResult)) {
  235. error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
  236. return;
  237. }
  238. if (trustResult != kSecTrustResultProceed && trustResult != kSecTrustResultUnspecified) {
  239. completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
  240. return;
  241. }
  242. completionHandler(NSURLSessionAuthChallengeUseCredential,
  243. [NSURLCredential credentialForTrust:protectionSpace.serverTrust]);
  244. }
  245. - (void)URLSession:(__unused NSURLSession *)session
  246. dataTask:(__unused NSURLSessionDataTask *)dataTask
  247. didReceiveData:(NSData *)data {
  248. if (!_data) {
  249. _data = data;
  250. return;
  251. }
  252. if (![_data respondsToSelector:@selector(appendData:)]) {
  253. _data = [_data mutableCopy];
  254. }
  255. [(id)_data appendData:data];
  256. }
  257. - (void)URLSession:(__unused NSURLSession *)session
  258. task:(NSURLSessionTask *)task
  259. didCompleteWithError:(NSError *)error
  260. {
  261. if (error) {
  262. _completionBlock(error, nil);
  263. return;
  264. }
  265. if (NSError *error = [self validateResponse:task.response data:_data]) {
  266. _completionBlock(error, nil);
  267. return;
  268. }
  269. id json = [NSJSONSerialization JSONObjectWithData:_data
  270. options:(NSJSONReadingOptions)0
  271. error:&error];
  272. if (!json) {
  273. _completionBlock(error, nil);
  274. return;
  275. }
  276. if (![json isKindOfClass:[NSDictionary class]]) {
  277. _completionBlock(make_auth_error_bad_response(json), nil);
  278. return;
  279. }
  280. _completionBlock(nil, (NSDictionary *)json);
  281. }
  282. - (NSError *)validateResponse:(NSURLResponse *)response data:(NSData *)data {
  283. if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
  284. // FIXME: Provide error message
  285. return make_auth_error_bad_response();
  286. }
  287. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  288. BOOL badResponse = (NSLocationInRange(httpResponse.statusCode, rangeForErrorType(ClientError))
  289. || NSLocationInRange(httpResponse.statusCode, rangeForErrorType(ServerError)));
  290. if (badResponse) {
  291. if (RLMSyncErrorResponseModel *responseModel = [self responseModelFromData:data]) {
  292. switch (responseModel.code) {
  293. case RLMSyncAuthErrorInvalidParameters:
  294. case RLMSyncAuthErrorMissingPath:
  295. case RLMSyncAuthErrorInvalidCredential:
  296. case RLMSyncAuthErrorUserDoesNotExist:
  297. case RLMSyncAuthErrorUserAlreadyExists:
  298. case RLMSyncAuthErrorAccessDeniedOrInvalidPath:
  299. case RLMSyncAuthErrorInvalidAccessToken:
  300. case RLMSyncAuthErrorExpiredPermissionOffer:
  301. case RLMSyncAuthErrorAmbiguousPermissionOffer:
  302. case RLMSyncAuthErrorFileCannotBeShared:
  303. return make_auth_error(responseModel);
  304. default:
  305. // Right now we assume that any codes not described
  306. // above are generic HTTP error codes.
  307. return make_auth_error_http_status(responseModel.status);
  308. }
  309. }
  310. return make_auth_error_http_status(httpResponse.statusCode);
  311. }
  312. if (!data) {
  313. // FIXME: provide error message
  314. return make_auth_error_bad_response();
  315. }
  316. return nil;
  317. }
  318. - (RLMSyncErrorResponseModel *)responseModelFromData:(NSData *)data {
  319. if (data.length == 0) {
  320. return nil;
  321. }
  322. id json = [NSJSONSerialization JSONObjectWithData:data
  323. options:(NSJSONReadingOptions)0
  324. error:nil];
  325. if (!json || ![json isKindOfClass:[NSDictionary class]]) {
  326. return nil;
  327. }
  328. return [[RLMSyncErrorResponseModel alloc] initWithDictionary:json];
  329. }
  330. @end
  331. @implementation RLMNetworkClient
  332. + (void)setDefaultTimeout:(NSTimeInterval)timeOut {
  333. g_defaultTimeout = timeOut;
  334. }
  335. + (void)sendRequestToEndpoint:(RLMSyncServerEndpoint *)endpoint
  336. server:(NSURL *)serverURL
  337. JSON:(NSDictionary *)jsonDictionary
  338. timeout:(NSTimeInterval)timeout
  339. options:(nullable RLMNetworkRequestOptions *)options
  340. completion:(RLMSyncCompletionBlock)completionBlock {
  341. // Create the request
  342. NSError *localError = nil;
  343. NSURL *requestURL = [endpoint urlForAuthServer:serverURL payload:jsonDictionary];
  344. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
  345. request.HTTPBody = [endpoint httpBodyForPayload:jsonDictionary error:&localError];
  346. if (localError) {
  347. completionBlock(localError, nil);
  348. return;
  349. }
  350. request.HTTPMethod = [endpoint httpMethod];
  351. request.timeoutInterval = MAX(timeout, 10);
  352. NSDictionary<NSString *, NSString *> *headers = [endpoint httpHeadersForPayload:jsonDictionary options:options];
  353. for (NSString *key in headers) {
  354. [request addValue:headers[key] forHTTPHeaderField:key];
  355. }
  356. id delegate = [RLMSessionDelegate delegateWithCertificatePaths:options.pinnedCertificatePaths
  357. completion:completionBlock];
  358. auto session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration
  359. delegate:delegate delegateQueue:nil];
  360. // Add the request to a task and start it
  361. [[session dataTaskWithRequest:request] resume];
  362. // Tell the session to destroy itself once it's done with the request
  363. [session finishTasksAndInvalidate];
  364. }
  365. @end
  366. @implementation RLMNetworkRequestOptions
  367. @end