This repository has been archived by the owner on Mar 9, 2022. It is now read-only.
/
TDRemoteRequest.m
421 lines (341 loc) · 14.1 KB
/
TDRemoteRequest.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//
// TDRemoteRequest.m
// TouchDB
//
// Created by Jens Alfke on 12/15/11.
// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
//
// 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 "TDRemoteRequest.h"
#import "TDAuthorizer.h"
#import "TDMisc.h"
#import "TDBlobStore.h"
#import <TouchDB/TD_Database.h>
#import "TDRouter.h"
#import "TDReplicator.h"
#import "CollectionUtils.h"
#import "Logging.h"
#import "Test.h"
#import "MYURLUtils.h"
// Max number of retry attempts for a transient failure, and the backoff time formula
#define kMaxRetries 2
#define RetryDelay(COUNT) (4 << (COUNT)) // COUNT starts at 0
@implementation TDRemoteRequest
+ (NSString*) userAgentHeader {
return $sprintf(@"TouchDB/%@", [TDRouter versionString]);
}
- (id) initWithMethod: (NSString*)method
URL: (NSURL*)url
body: (id)body
requestHeaders: (NSDictionary *)requestHeaders
onCompletion: (TDRemoteRequestCompletionBlock)onCompletion
{
self = [super init];
if (self) {
_onCompletion = [onCompletion copy];
_request = [[NSMutableURLRequest alloc] initWithURL: url];
_request.HTTPMethod = method;
_request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// Add headers.
[_request setValue: [[self class] userAgentHeader] forHTTPHeaderField:@"User-Agent"];
[requestHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
[_request setValue:value forHTTPHeaderField:key];
}];
[self setupRequest: _request withBody: body];
}
return self;
}
- (NSTimeInterval) timeoutInterval {
return _request.timeoutInterval;
}
- (void) setTimeoutInterval:(NSTimeInterval)timeout {
_request.timeoutInterval = timeout;
}
- (id<TDAuthorizer>) authorizer {
return _authorizer;
}
- (void) setAuthorizer: (id<TDAuthorizer>)authorizer {
if (_authorizer != authorizer) {
_authorizer = authorizer;
[_request setValue: [authorizer authorizeURLRequest: _request forRealm: nil]
forHTTPHeaderField: @"Authorization"];
}
}
- (void) setupRequest: (NSMutableURLRequest*)request withBody: (id)body {
// subclasses can override this.
}
- (void) dontLog404 {
_dontLog404 = true;
}
- (void) start {
if (!_request)
return; // -clearConnection already called
LogTo(RemoteRequest, @"%@: Starting...", self);
Assert(!_connection);
_connection = [NSURLConnection connectionWithRequest: _request delegate: self];
// Retaining myself shouldn't be necessary, because NSURLConnection is documented as retaining
// its delegate while it's running. But GNUstep doesn't (currently) do this, so for
// compatibility I retain myself until the connection completes (see -clearConnection.)
// TODO: Remove this and the [self autorelease] below when I get the fix from GNUstep.
#ifdef GNUSTEP
[self retain];
#endif
}
- (void) clearConnection {
_request = nil;
if (_connection) {
_connection = nil;
#ifdef GNUSTEP
[self release];
#endif
}
}
- (void)dealloc {
[self clearConnection];
}
- (NSString*) description {
return $sprintf(@"%@[%@ %@]", [self class], _request.HTTPMethod, _request.URL);
}
- (NSMutableDictionary*) statusInfo {
return $mdict({@"URL", _request.URL.absoluteString}, {@"method", _request.HTTPMethod});
}
- (void) respondWithResult: (id)result error: (NSError*)error {
Assert(result || error);
_onCompletion(result, error);
_onCompletion = nil; // break cycles
}
- (void) startAfterDelay: (NSTimeInterval)delay {
// assumes _connection already failed or canceled.
_connection = nil;
[self performSelector: @selector(start) withObject: nil afterDelay: delay];
}
- (void) stop {
if (_connection) {
LogTo(RemoteRequest, @"%@: Stopped", self);
[_connection cancel];
}
[self clearConnection];
if (_onCompletion) {
NSError* error = [NSError errorWithDomain: NSURLErrorDomain code: NSURLErrorCancelled
userInfo: nil];
[self respondWithResult: nil error: error];
_onCompletion = nil; // break cycles
}
}
- (void) cancelWithStatus: (int)status {
[_connection cancel];
[self connection: _connection didFailWithError: TDStatusToNSError(status, _request.URL)];
}
- (BOOL) retry {
// Note: This assumes all requests are idempotent, since even though we got an error back, the
// request might have succeeded on the remote server, and by retrying we'd be issuing it again.
// PUT and POST requests aren't generally idempotent, but the ones sent by the replicator are.
if (_retryCount >= kMaxRetries)
return NO;
NSTimeInterval delay = RetryDelay(_retryCount);
++_retryCount;
LogTo(RemoteRequest, @"%@: Will retry in %g sec", self, delay);
[self startAfterDelay: delay];
return YES;
}
- (bool) retryWithCredential {
if (_authorizer || _challenged)
return false;
_challenged = YES;
NSURLCredential* cred = [_request.URL my_credentialForRealm: nil
authenticationMethod: NSURLAuthenticationMethodHTTPBasic];
if (!cred) {
LogTo(RemoteRequest, @"Got 401 but no stored credential found (with nil realm)");
return false;
}
[_connection cancel];
self.authorizer = [[TDBasicAuthorizer alloc] initWithCredential: cred];
LogTo(RemoteRequest, @"%@ retrying with %@", self, _authorizer);
[self startAfterDelay: 0.0];
return true;
}
#pragma mark - NSURLCONNECTION DELEGATE:
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
id<NSURLAuthenticationChallengeSender> sender = challenge.sender;
NSURLProtectionSpace* space = challenge.protectionSpace;
NSString* authMethod = space.authenticationMethod;
LogTo(RemoteRequest, @"Got challenge for %@: method=%@, proposed=%@, err=%@", self, authMethod, challenge.proposedCredential, challenge.error);
if ($equal(authMethod, NSURLAuthenticationMethodHTTPBasic) ||
$equal(authMethod, NSURLAuthenticationMethodHTTPDigest)) {
_challenged = true;
_authorizer = nil;
if (challenge.previousFailureCount <= 1) {
// On basic auth challenge, use proposed credential on first attempt. On second attempt
// or if there's no proposed credential, look one up. After that, give up.
NSURLCredential* cred = challenge.proposedCredential;
if (cred == nil || challenge.previousFailureCount > 0) {
cred = [_request.URL my_credentialForRealm: space.realm
authenticationMethod: authMethod];
}
if (cred) {
LogTo(RemoteRequest, @" challenge: useCredential: %@", cred);
[sender useCredential: cred forAuthenticationChallenge:challenge];
// Update my authorizer so my owner (the replicator) can pick it up when I'm done
_authorizer = [[TDBasicAuthorizer alloc] initWithCredential: cred];
return;
}
}
LogTo(RemoteRequest, @" challenge: continueWithoutCredential");
[sender continueWithoutCredentialForAuthenticationChallenge: challenge];
} else if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) {
SecTrustRef trust = space.serverTrust;
if ([[self class] checkTrust: trust forHost: space.host]) {
LogTo(RemoteRequest, @" useCredential for trust: %@", trust);
[sender useCredential: [NSURLCredential credentialForTrust: trust]
forAuthenticationChallenge: challenge];
} else {
LogTo(RemoteRequest, @" challenge: cancel");
[sender cancelAuthenticationChallenge: challenge];
}
} else {
LogTo(RemoteRequest, @" challenge: performDefaultHandling");
[sender performDefaultHandlingForAuthenticationChallenge: challenge];
}
}
+ (BOOL) checkTrust: (SecTrustRef)trust forHost: (NSString*)host {
SecTrustResultType trustResult;
OSStatus err = SecTrustEvaluate(trust, &trustResult);
if (err == noErr && (trustResult == kSecTrustResultProceed ||
trustResult == kSecTrustResultUnspecified)) {
return YES;
} else {
Warn(@"TouchDB: SSL server <%@> not trusted (err=%d, trustResult=%u); cert chain follows:",
host, (int)err, (unsigned)trustResult);
#if TARGET_OS_IPHONE
for (CFIndex i = 0; i < SecTrustGetCertificateCount(trust); ++i) {
SecCertificateRef cert = SecTrustGetCertificateAtIndex(trust, i);
CFStringRef subject = SecCertificateCopySubjectSummary(cert);
Warn(@" %@", subject);
CFRelease(subject);
}
#else
#ifdef __OBJC_GC__
NSArray* trustProperties = NSMakeCollectable(SecTrustCopyProperties(trust));
#else
NSArray* trustProperties = (__bridge_transfer NSArray *)SecTrustCopyProperties(trust);
#endif
for (NSDictionary* property in trustProperties) {
Warn(@" %@: error = %@",
property[(__bridge id)kSecPropertyTypeTitle],
property[(__bridge id)kSecPropertyTypeError]);
}
#endif
return NO;
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
_status = (int) ((NSHTTPURLResponse*)response).statusCode;
LogTo(RemoteRequest, @"%@: Got response, status %d", self, _status);
if (_status == 401) {
// CouchDB says we're unauthorized but it didn't present a 'WWW-Authenticate' header
// (it actually does this on purpose...) Let's see if we have a credential we can try:
if ([self retryWithCredential])
return;
}
#if DEBUG
if (!TDStatusIsError(_status)) {
// By setting the user default "TDFakeFailureRate" to a number between 0.0 and 1.0,
// you can artificially cause failures of that fraction of requests, for testing.
// The status will be 567, or the value of "TDFakeFailureStatus" if it's set.
NSUserDefaults* dflts = [NSUserDefaults standardUserDefaults];
float fakeFailureRate = [dflts floatForKey: @"TDFakeFailureRate"];
if (fakeFailureRate > 0.0 && random() < fakeFailureRate * 0x7FFFFFFF) {
AlwaysLog(@"***FAKE FAILURE: %@", self);
_status = (int)[dflts integerForKey: @"TDFakeFailureStatus"] ?: 567;
}
}
#endif
if (TDStatusIsError(_status))
[self cancelWithStatus: _status];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)response
{
// The redirected request needs to be authorized again:
if (![request valueForHTTPHeaderField: @"Authorization"]) {
NSMutableURLRequest* nuRequest = [request mutableCopy];
NSString* auth;
if (_authorizer)
auth = [_authorizer authorizeURLRequest: nuRequest forRealm: nil];
else
auth = [_request valueForHTTPHeaderField: @"Authorization"];
[nuRequest setValue: auth forHTTPHeaderField: @"Authorization"];
request = nuRequest;
}
return request;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
LogTo(RemoteRequestVerbose, @"%@: Got %lu bytes", self, (unsigned long)data.length);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (WillLog()) {
if (!(_dontLog404 && error.code == kTDStatusNotFound && $equal(error.domain, TDHTTPErrorDomain)))
Log(@"%@: Got error %@", self, error);
}
// If the error is likely transient, retry:
if (TDMayBeTransientError(error) && [self retry])
return;
[self clearConnection];
[self respondWithResult: nil error: error];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
LogTo(RemoteRequest, @"%@: Finished loading", self);
[self clearConnection];
[self respondWithResult: self error: nil];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
return nil;
}
@end
@implementation TDRemoteJSONRequest
- (void) setupRequest: (NSMutableURLRequest*)request withBody: (id)body {
[request setValue: @"application/json" forHTTPHeaderField: @"Accept"];
if (body) {
request.HTTPBody = [TDJSON dataWithJSONObject: body options: 0 error: NULL];
[request addValue: @"application/json" forHTTPHeaderField: @"Content-Type"];
}
}
- (void) clearConnection {
_jsonBuffer = nil;
[super clearConnection];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[super connection: connection didReceiveData: data];
if (!_jsonBuffer)
_jsonBuffer = [[NSMutableData alloc] initWithCapacity: MAX(data.length, 8192u)];
[_jsonBuffer appendData: data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
LogTo(RemoteRequest, @"%@: Finished loading", self);
id result = nil;
NSError* error = nil;
if (_jsonBuffer.length > 0) {
result = [TDJSON JSONObjectWithData: _jsonBuffer options: 0 error: NULL];
if (!result) {
Warn(@"%@: %@ %@ returned unparseable data '%@'",
self, _request.HTTPMethod, _request.URL, [_jsonBuffer my_UTF8ToString]);
error = TDStatusToNSError(kTDStatusUpstreamError, _request.URL);
}
} else {
result = $dict();
}
[self clearConnection];
[self respondWithResult: result error: error];
}
@end