From 277cf46e2ca2045f98d5d3d75a4bd180af78586a Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 19 Jul 2012 17:53:05 -0700 Subject: [PATCH] Yet more replication auth fixes - Use credentials embedded in the URL - Make sure Authorization: header gets preserved across a redirect (NSURLConnection drops it) - Weed out ServerTrust auth 'challenges' so they don't confuse my code - Added optional username/password args to TouchServ's replication command Fixes problems reported by Joel at 36codes.com --- Demo-Mac/TouchServ.m | 43 +++++++++++++--- .../ChangeTracker/TDConnectionChangeTracker.m | 16 +++--- Source/TDMultipartDownloader.m | 24 +++++---- Source/TDRemoteRequest.m | 49 ++++++++++++++++--- vendor/MYUtilities | 2 +- 5 files changed, 102 insertions(+), 32 deletions(-) diff --git a/Demo-Mac/TouchServ.m b/Demo-Mac/TouchServ.m index 8c0ba07..2f592dd 100644 --- a/Demo-Mac/TouchServ.m +++ b/Demo-Mac/TouchServ.m @@ -46,7 +46,10 @@ } -static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL createTarget) { +static bool doReplicate( TDServer* server, const char* replArg, + BOOL pull, BOOL createTarget, + const char *user, const char *password) +{ NSURL* remote = [NSMakeCollectable(CFURLCreateWithBytes(NULL, (const UInt8*)replArg, strlen(replArg), kCFStringEncodingUTF8, NULL)) autorelease]; @@ -59,6 +62,28 @@ static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL fprintf(stderr, "Invalid database name '%s'\n", dbName.UTF8String); return false; } + + if (user && password) { + NSString* userStr = [NSString stringWithCString: user encoding: NSUTF8StringEncoding]; + NSString* passStr = [NSString stringWithCString: password encoding: NSUTF8StringEncoding]; + Log(@"Setting credentials for user '%@'", userStr); + NSURLCredential* cred; + cred = [NSURLCredential credentialWithUser: userStr + password: passStr + persistence: NSURLCredentialPersistenceForSession]; + int port = remote.port.intValue; + if (port == 0) + port = [remote.scheme isEqualToString: @"https"] ? 443 : 80; + NSURLProtectionSpace* space; + space = [[[NSURLProtectionSpace alloc] initWithHost: remote.host + port: port + protocol: remote.scheme + realm: nil + authenticationMethod: NSURLAuthenticationMethodDefault] + autorelease]; + [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential: cred + forProtectionSpace: space]; + } if (pull) Log(@"Pulling from <%@> --> %@ ...", remote, dbName); @@ -74,8 +99,8 @@ static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL fprintf(stderr, "Couldn't delete existing database '%s'\n", dbName.UTF8String); return; } - db = [dbm databaseNamed: dbName]; } + db = [dbm databaseNamed: dbName]; } if (!db) { fprintf(stderr, "No such database '%s'\n", dbName.UTF8String); @@ -118,7 +143,7 @@ int main (int argc, const char * argv[]) NSData* value = [@"value" dataUsingEncoding: NSUTF8StringEncoding]; listener.TXTRecordDictionary = [NSDictionary dictionaryWithObject: value forKey: @"Key"]; - const char* replArg = NULL; + const char* replArg = NULL, *user = NULL, *password = NULL; BOOL pull = NO, createTarget = NO; for (int i = 1; i < argc; ++i) { @@ -131,19 +156,23 @@ int main (int argc, const char * argv[]) forKey: @"touchdb"]; Log(@"Auth required: user='touchdb', password='%@'", password); } else if (strcmp(argv[i], "--pull") == 0) { - replArg = argv[i+1]; + replArg = argv[++i]; pull = YES; } else if (strcmp(argv[i], "--push") == 0) { - replArg = argv[i+1]; + replArg = argv[++i]; } else if (strcmp(argv[i], "--create-target") == 0) { createTarget = YES; + } else if (strcmp(argv[i], "--user") == 0) { + user = argv[++i]; + } else if (strcmp(argv[i], "--password") == 0) { + password = argv[++i]; } } - + [listener start]; if (replArg) { - if (!doReplicate(server, replArg, pull, createTarget)) + if (!doReplicate(server, replArg, pull, createTarget, user, password)) return 1; } else { Log(@"TouchServ %@ is listening%@ on port %d ... relax!", diff --git a/Source/ChangeTracker/TDConnectionChangeTracker.m b/Source/ChangeTracker/TDConnectionChangeTracker.m index 649ba18..6a74def 100644 --- a/Source/ChangeTracker/TDConnectionChangeTracker.m +++ b/Source/ChangeTracker/TDConnectionChangeTracker.m @@ -85,11 +85,8 @@ - (bool) retryWithCredential { if (_authorizer || _challenged) return false; _challenged = YES; - NSURLProtectionSpace* space = [_databaseURL - my_protectionSpaceWithRealm: nil - authenticationMethod: NSURLAuthenticationMethodHTTPBasic]; - NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage] - defaultCredentialForProtectionSpace: space]; + NSURLCredential* cred = [_databaseURL my_credentialForRealm: nil + authenticationMethod: NSURLAuthenticationMethodHTTPBasic]; if (!cred) { LogTo(ChangeTracker, @"Got 401 but no stored credential found (with nil realm)"); return false; @@ -107,8 +104,15 @@ - (bool) retryWithCredential { - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { - _challenged = true; id sender = challenge.sender; + NSString* authMethod = [[challenge protectionSpace] authenticationMethod]; + if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) { + // TODO: Check trust of server cert + [sender performDefaultHandlingForAuthenticationChallenge: challenge]; + return; + } + + _challenged = true; if (challenge.proposedCredential) { [sender performDefaultHandlingForAuthenticationChallenge: challenge]; return; diff --git a/Source/TDMultipartDownloader.m b/Source/TDMultipartDownloader.m index 523d3ef..236e21d 100644 --- a/Source/TDMultipartDownloader.m +++ b/Source/TDMultipartDownloader.m @@ -67,17 +67,21 @@ - (NSDictionary*) document { - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { - [super connection: connection didReceiveResponse: response]; - if (!_connection) - return; + TDStatus status = (TDStatus) ((NSHTTPURLResponse*)response).statusCode; + if (status < 300) { + // Check the content type to see whether it's a multipart response: + NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields]; + NSString* contentType = [headers objectForKey: @"Content-Type"]; + if ([contentType hasPrefix: @"text/plain"]) + contentType = nil; // Workaround for CouchDB returning JSON docs with text/plain type + if (![_reader setContentType: contentType]) { + LogTo(RemoteRequest, @"%@ got invalid Content-Type '%@'", self, contentType); + [self cancelWithStatus: _reader.status]; + return; + } + } - // Check the content type to see whether it's a multipart response: - NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields]; - NSString* contentType = [headers objectForKey: @"Content-Type"]; - if ([contentType hasPrefix: @"text/plain"]) - contentType = nil; // Workaround for CouchDB returning JSON docs with text/plain type - if (![_reader setContentType: contentType]) - [self cancelWithStatus: _reader.status]; + [super connection: connection didReceiveResponse: response]; } diff --git a/Source/TDRemoteRequest.m b/Source/TDRemoteRequest.m index e510d1a..cb19de7 100644 --- a/Source/TDRemoteRequest.m +++ b/Source/TDRemoteRequest.m @@ -163,11 +163,8 @@ - (bool) retryWithCredential { if (_authorizer || _challenged) return false; _challenged = YES; - NSURLProtectionSpace* space = [_request.URL - my_protectionSpaceWithRealm: nil - authenticationMethod: NSURLAuthenticationMethodHTTPBasic]; - NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage] - defaultCredentialForProtectionSpace: space]; + 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; @@ -175,7 +172,7 @@ - (bool) retryWithCredential { [_connection cancel]; self.authorizer = [[[TDBasicAuthorizer alloc] initWithCredential: cred] autorelease]; - LogTo(RemoteRequest, @"Got 401 but retrying with %@", _authorizer); + LogTo(RemoteRequest, @"%@ retrying with %@", self, _authorizer); [self startAfterDelay: 0.0]; return true; } @@ -187,11 +184,25 @@ - (bool) retryWithCredential { - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { - LogTo(RemoteRequest, @"Got challenge: %@", challenge); - _challenged = true; + NSString* authMethod = [[challenge protectionSpace] authenticationMethod]; + LogTo(RemoteRequest, @"Got challenge: %@ (%@)", challenge, authMethod); + if ($equal(authMethod, NSURLAuthenticationMethodHTTPBasic)) { + _challenged = true; + if (challenge.previousFailureCount == 0) { + NSURLCredential* cred = [_request.URL my_credentialForRealm: challenge.protectionSpace.realm + authenticationMethod: authMethod]; + if (cred) { + [challenge.sender useCredential: cred forAuthenticationChallenge:challenge]; + return; + } + } + } else if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) { + // TODO: Check trust of server cert + } [challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge]; } + - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { _status = (int) ((NSHTTPURLResponse*)response).statusCode; LogTo(RemoteRequest, @"%@: Got response, status %d", self, _status); @@ -205,10 +216,31 @@ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLRespon [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] autorelease]; + 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(RemoteRequest, @"%@: 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))) @@ -229,6 +261,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self respondWithResult: self error: nil]; } + - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { diff --git a/vendor/MYUtilities b/vendor/MYUtilities index e8e65f6..afe6991 160000 --- a/vendor/MYUtilities +++ b/vendor/MYUtilities @@ -1 +1 @@ -Subproject commit e8e65f61f44af31182f09a97fb630bce940f4c4a +Subproject commit afe6991b970fb04fced8b368fbeb2e49aff274af