RLMNetworkClient.mm 17 KB

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