1 ////////////////////////////////////////////////////////////////////////////
3 // Copyright 2016 Realm Inc.
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
9 // http://www.apache.org/licenses/LICENSE-2.0
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.
17 ////////////////////////////////////////////////////////////////////////////
19 #import "RLMNetworkClient.h"
21 #import "RLMRealmConfiguration.h"
22 #import "RLMJSONModels.h"
23 #import "RLMSyncUtil_Private.hpp"
26 typedef void(^RLMServerURLSessionCompletionBlock)(NSData *, NSURLResponse *, NSError *);
28 static NSUInteger const kHTTPCodeRange = 100;
30 typedef enum : NSUInteger {
31 Informational = 1, // 1XX
33 Redirection = 3, // 3XX
34 ClientError = 4, // 4XX
35 ServerError = 5, // 5XX
36 } RLMServerHTTPErrorCodeType;
38 static NSRange RLM_rangeForErrorType(RLMServerHTTPErrorCodeType type) {
39 return NSMakeRange(type*100, kHTTPCodeRange);
42 @interface RLMSyncServerEndpoint ()
43 - (instancetype)initPrivate NS_DESIGNATED_INITIALIZER;
45 /// The HTTP method the endpoint expects. Defaults to POST.
46 - (NSString *)httpMethod;
48 /// The URL to which the request should be made. Must be implemented.
49 - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json;
51 /// The body for the request, if any.
52 - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error;
54 /// The HTTP headers to be added to the request, if any.
55 - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json;
58 @implementation RLMSyncServerEndpoint
60 - (instancetype)initPrivate {
61 return (self = [super init]);
64 - (NSString *)httpMethod {
68 - (NSURL *)urlForAuthServer:(__unused NSURL *)authServerURL payload:(__unused NSDictionary *)json {
69 NSAssert(NO, @"This method must be overriden by concrete subclasses.");
73 - (NSData *)httpBodyForPayload:(NSDictionary *)json error:(NSError **)error {
74 NSError *localError = nil;
75 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:json
76 options:(NSJSONWritingOptions)0
78 if (jsonData && !localError) {
81 NSAssert(localError, @"If there isn't a converted data object there must be an error.");
88 - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(__unused NSDictionary *)json {
89 return @{@"Content-Type": @"application/json;charset=utf-8",
90 @"Accept": @"application/json"};
95 @implementation RLMSyncAuthEndpoint
97 + (instancetype)endpoint {
98 return [[RLMSyncAuthEndpoint alloc] initPrivate];
101 - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
102 return [authServerURL URLByAppendingPathComponent:@"auth"];
107 @implementation RLMSyncChangePasswordEndpoint
109 + (instancetype)endpoint {
110 return [[RLMSyncChangePasswordEndpoint alloc] initPrivate];
113 - (NSString *)httpMethod {
117 - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(__unused NSDictionary *)json {
118 return [authServerURL URLByAppendingPathComponent:@"auth/password"];
121 - (NSDictionary *)httpHeadersForPayload:(NSDictionary *)json {
122 NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
124 @throw RLMException(@"Malformed request; this indicates an internal error.");
126 NSMutableDictionary *headers = [[super httpHeadersForPayload:json] mutableCopy];
127 [headers setObject:authToken forKey:@"Authorization"];
128 return [headers copy];
133 @implementation RLMSyncGetUserInfoEndpoint
135 + (instancetype)endpoint {
136 return [[RLMSyncGetUserInfoEndpoint alloc] initPrivate];
139 - (NSString *)httpMethod {
143 - (NSURL *)urlForAuthServer:(NSURL *)authServerURL payload:(NSDictionary *)json {
144 NSString *provider = json[kRLMSyncProviderKey];
145 NSString *providerID = json[kRLMSyncProviderIDKey];
146 NSAssert([provider isKindOfClass:[NSString class]] && [providerID isKindOfClass:[NSString class]],
147 @"malformed request; this indicates a logic error in the binding.");
148 NSCharacterSet *allowed = [NSCharacterSet URLQueryAllowedCharacterSet];
149 NSString *pathComponent = [NSString stringWithFormat:@"auth/users/%@/%@",
150 [provider stringByAddingPercentEncodingWithAllowedCharacters:allowed],
151 [providerID stringByAddingPercentEncodingWithAllowedCharacters:allowed]];
152 return [authServerURL URLByAppendingPathComponent:pathComponent];
155 - (NSData *)httpBodyForPayload:(__unused NSDictionary *)json error:(__unused NSError **)error {
159 - (NSDictionary<NSString *, NSString *> *)httpHeadersForPayload:(NSDictionary *)json {
160 NSString *authToken = [json objectForKey:kRLMSyncTokenKey];
162 @throw RLMException(@"Malformed request; this indicates an internal error.");
164 return @{@"Authorization": authToken};
170 @implementation RLMNetworkClient
172 + (NSURLSession *)session {
173 return [NSURLSession sharedSession];
176 + (void)sendRequestToEndpoint:(RLMSyncServerEndpoint *)endpoint
177 server:(NSURL *)serverURL
178 JSON:(NSDictionary *)jsonDictionary
179 completion:(RLMSyncCompletionBlock)completionBlock {
180 static NSTimeInterval const defaultTimeout = 60;
181 [self sendRequestToEndpoint:endpoint
184 timeout:defaultTimeout
185 completion:completionBlock];
188 + (void)sendRequestToEndpoint:(RLMSyncServerEndpoint *)endpoint
189 server:(NSURL *)serverURL
190 JSON:(NSDictionary *)jsonDictionary
191 timeout:(NSTimeInterval)timeout
192 completion:(RLMSyncCompletionBlock)completionBlock {
193 // Create the request
194 NSError *localError = nil;
195 NSURL *requestURL = [endpoint urlForAuthServer:serverURL payload:jsonDictionary];
196 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
197 request.HTTPBody = [endpoint httpBodyForPayload:jsonDictionary error:&localError];
199 completionBlock(localError, nil);
202 request.HTTPMethod = [endpoint httpMethod];
203 request.timeoutInterval = MAX(timeout, 10);
204 NSDictionary<NSString *, NSString *> *headers = [endpoint httpHeadersForPayload:jsonDictionary];
205 for (NSString *key in headers) {
206 [request addValue:headers[key] forHTTPHeaderField:key];
208 RLMServerURLSessionCompletionBlock handler = ^(NSData *data,
209 NSURLResponse *response,
213 completionBlock(error, nil);
217 NSError *localError = nil;
219 if (![self validateResponse:response data:data error:&localError]) {
221 completionBlock(localError, nil);
225 // Parse out the JSON
226 id json = [NSJSONSerialization JSONObjectWithData:data
227 options:(NSJSONReadingOptions)0
229 if (!json || localError) {
230 // JSON parsing error
231 completionBlock(localError, nil);
232 } else if (![json isKindOfClass:[NSDictionary class]]) {
233 // JSON response malformed
234 localError = make_auth_error_bad_response(json);
235 completionBlock(localError, nil);
237 // JSON parsed successfully
238 completionBlock(nil, (NSDictionary *)json);
242 // Add the request to a task and start it
243 NSURLSessionTask *task = [self.session dataTaskWithRequest:request
244 completionHandler:handler];
248 + (BOOL)validateResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError * __autoreleasing *)error {
249 __autoreleasing NSError *localError = nil;
254 if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
255 // FIXME: Provide error message
256 *error = make_auth_error_bad_response();
260 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
261 BOOL badResponse = (NSLocationInRange(httpResponse.statusCode, RLM_rangeForErrorType(ClientError))
262 || NSLocationInRange(httpResponse.statusCode, RLM_rangeForErrorType(ServerError)));
264 if (RLMSyncErrorResponseModel *responseModel = [self responseModelFromData:data]) {
265 switch (responseModel.code) {
266 case RLMSyncAuthErrorInvalidCredential:
267 case RLMSyncAuthErrorUserDoesNotExist:
268 case RLMSyncAuthErrorUserAlreadyExists:
269 case RLMSyncAuthErrorAccessDeniedOrInvalidPath:
270 case RLMSyncAuthErrorInvalidAccessToken:
271 case RLMSyncAuthErrorExpiredPermissionOffer:
272 case RLMSyncAuthErrorAmbiguousPermissionOffer:
273 case RLMSyncAuthErrorFileCannotBeShared:
274 *error = make_auth_error(responseModel);
277 // Right now we assume that any codes not described
278 // above are generic HTTP error codes.
279 *error = make_auth_error_http_status(responseModel.status);
283 *error = make_auth_error_http_status(httpResponse.statusCode);
290 // FIXME: provide error message
291 *error = make_auth_error_bad_response();
298 + (RLMSyncErrorResponseModel *)responseModelFromData:(NSData *)data {
299 if (data.length == 0) {
302 id json = [NSJSONSerialization JSONObjectWithData:data
303 options:(NSJSONReadingOptions)0
305 if (!json || ![json isKindOfClass:[NSDictionary class]]) {
308 return [[RLMSyncErrorResponseModel alloc] initWithDictionary:json];