Skip to content
This repository
Browse code

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
  • Loading branch information...
commit 277cf46e2ca2045f98d5d3d75a4bd180af78586a 1 parent 92e1a7a
Jens Alfke authored July 19, 2012
43  Demo-Mac/TouchServ.m
@@ -46,7 +46,10 @@
46 46
 }
47 47
 
48 48
 
49  
-static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL createTarget) {
  49
+static bool doReplicate( TDServer* server, const char* replArg,
  50
+                        BOOL pull, BOOL createTarget,
  51
+                        const char *user, const char *password)
  52
+{
50 53
     NSURL* remote = [NSMakeCollectable(CFURLCreateWithBytes(NULL, (const UInt8*)replArg,
51 54
                                                            strlen(replArg),
52 55
                                                            kCFStringEncodingUTF8, NULL)) autorelease];
@@ -59,6 +62,28 @@ static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL
59 62
         fprintf(stderr, "Invalid database name '%s'\n", dbName.UTF8String);
60 63
         return false;
61 64
     }
  65
+
  66
+    if (user && password) {
  67
+        NSString* userStr = [NSString stringWithCString: user encoding: NSUTF8StringEncoding];
  68
+        NSString* passStr = [NSString stringWithCString: password encoding: NSUTF8StringEncoding];
  69
+        Log(@"Setting credentials for user '%@'", userStr);
  70
+        NSURLCredential* cred;
  71
+        cred = [NSURLCredential credentialWithUser: userStr
  72
+                                          password: passStr
  73
+                                       persistence: NSURLCredentialPersistenceForSession];
  74
+        int port = remote.port.intValue;
  75
+        if (port == 0)
  76
+            port = [remote.scheme isEqualToString: @"https"] ? 443 : 80;
  77
+        NSURLProtectionSpace* space;
  78
+        space = [[[NSURLProtectionSpace alloc] initWithHost: remote.host
  79
+                                                       port: port
  80
+                                                   protocol: remote.scheme
  81
+                                                      realm: nil
  82
+                                       authenticationMethod: NSURLAuthenticationMethodDefault]
  83
+                 autorelease];
  84
+        [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential: cred
  85
+                                                            forProtectionSpace: space];
  86
+    }
62 87
     
63 88
     if (pull)
64 89
         Log(@"Pulling from <%@> --> %@ ...", remote, dbName);
@@ -74,8 +99,8 @@ static bool doReplicate( TDServer* server, const char* replArg, BOOL pull, BOOL
74 99
                     fprintf(stderr, "Couldn't delete existing database '%s'\n", dbName.UTF8String);
75 100
                     return;
76 101
                 }
77  
-                db = [dbm databaseNamed: dbName];
78 102
             }
  103
+            db = [dbm databaseNamed: dbName];
79 104
         }
80 105
         if (!db) {
81 106
             fprintf(stderr, "No such database '%s'\n", dbName.UTF8String);
@@ -118,7 +143,7 @@ int main (int argc, const char * argv[])
118 143
         NSData* value = [@"value" dataUsingEncoding: NSUTF8StringEncoding];
119 144
         listener.TXTRecordDictionary = [NSDictionary dictionaryWithObject: value forKey: @"Key"];
120 145
         
121  
-        const char* replArg = NULL;
  146
+        const char* replArg = NULL, *user = NULL, *password = NULL;
122 147
         BOOL pull = NO, createTarget = NO;
123 148
         
124 149
         for (int i = 1; i < argc; ++i) {
@@ -131,19 +156,23 @@ int main (int argc, const char * argv[])
131 156
                                                                  forKey: @"touchdb"];
132 157
                 Log(@"Auth required: user='touchdb', password='%@'", password);
133 158
             } else if (strcmp(argv[i], "--pull") == 0) {
134  
-                replArg = argv[i+1];
  159
+                replArg = argv[++i];
135 160
                 pull = YES;
136 161
             } else if (strcmp(argv[i], "--push") == 0) {
137  
-                replArg = argv[i+1];
  162
+                replArg = argv[++i];
138 163
             } else if (strcmp(argv[i], "--create-target") == 0) {
139 164
                 createTarget = YES;
  165
+            } else if (strcmp(argv[i], "--user") == 0) {
  166
+                user = argv[++i];
  167
+            } else if (strcmp(argv[i], "--password") == 0) {
  168
+                password = argv[++i];
140 169
             }
141 170
         }
142  
-        
  171
+
143 172
         [listener start];
144 173
         
145 174
         if (replArg) {
146  
-            if (!doReplicate(server, replArg, pull, createTarget))
  175
+            if (!doReplicate(server, replArg, pull, createTarget, user, password))
147 176
                 return 1;
148 177
         } else {
149 178
             Log(@"TouchServ %@ is listening%@ on port %d ... relax!",
16  Source/ChangeTracker/TDConnectionChangeTracker.m
@@ -85,11 +85,8 @@ - (bool) retryWithCredential {
85 85
     if (_authorizer || _challenged)
86 86
         return false;
87 87
     _challenged = YES;
88  
-    NSURLProtectionSpace* space = [_databaseURL
89  
-                                my_protectionSpaceWithRealm: nil
90  
-                                       authenticationMethod: NSURLAuthenticationMethodHTTPBasic];
91  
-    NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage]
92  
-                                    defaultCredentialForProtectionSpace: space];
  88
+    NSURLCredential* cred = [_databaseURL my_credentialForRealm: nil
  89
+                                           authenticationMethod: NSURLAuthenticationMethodHTTPBasic];
93 90
     if (!cred) {
94 91
         LogTo(ChangeTracker, @"Got 401 but no stored credential found (with nil realm)");
95 92
         return false;
@@ -107,8 +104,15 @@ - (bool) retryWithCredential {
107 104
 - (void)connection:(NSURLConnection *)connection
108 105
         willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
109 106
 {
110  
-    _challenged = true;
111 107
     id<NSURLAuthenticationChallengeSender> sender = challenge.sender;
  108
+    NSString* authMethod = [[challenge protectionSpace] authenticationMethod];
  109
+    if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) {
  110
+        // TODO: Check trust of server cert
  111
+        [sender performDefaultHandlingForAuthenticationChallenge: challenge];
  112
+        return;
  113
+    }
  114
+
  115
+    _challenged = true;
112 116
     if (challenge.proposedCredential) {
113 117
         [sender performDefaultHandlingForAuthenticationChallenge: challenge];
114 118
         return;
24  Source/TDMultipartDownloader.m
@@ -67,17 +67,21 @@ - (NSDictionary*) document {
67 67
 
68 68
 
69 69
 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
70  
-    [super connection: connection didReceiveResponse: response];
71  
-    if (!_connection)
72  
-        return;
  70
+    TDStatus status = (TDStatus) ((NSHTTPURLResponse*)response).statusCode;
  71
+    if (status < 300) {
  72
+        // Check the content type to see whether it's a multipart response:
  73
+        NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
  74
+        NSString* contentType = [headers objectForKey: @"Content-Type"];
  75
+        if ([contentType hasPrefix: @"text/plain"])
  76
+            contentType = nil;      // Workaround for CouchDB returning JSON docs with text/plain type
  77
+        if (![_reader setContentType: contentType]) {
  78
+            LogTo(RemoteRequest, @"%@ got invalid Content-Type '%@'", self, contentType);
  79
+            [self cancelWithStatus: _reader.status];
  80
+            return;
  81
+        }
  82
+    }
73 83
     
74  
-    // Check the content type to see whether it's a multipart response:
75  
-    NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
76  
-    NSString* contentType = [headers objectForKey: @"Content-Type"];
77  
-    if ([contentType hasPrefix: @"text/plain"])
78  
-        contentType = nil;      // Workaround for CouchDB returning JSON docs with text/plain type
79  
-    if (![_reader setContentType: contentType])
80  
-        [self cancelWithStatus: _reader.status];
  84
+    [super connection: connection didReceiveResponse: response];
81 85
 }
82 86
 
83 87
 
49  Source/TDRemoteRequest.m
@@ -163,11 +163,8 @@ - (bool) retryWithCredential {
163 163
     if (_authorizer || _challenged)
164 164
         return false;
165 165
     _challenged = YES;
166  
-    NSURLProtectionSpace* space = [_request.URL
167  
-                                my_protectionSpaceWithRealm: nil
168  
-                                       authenticationMethod: NSURLAuthenticationMethodHTTPBasic];
169  
-    NSURLCredential* cred = [[NSURLCredentialStorage sharedCredentialStorage]
170  
-                                    defaultCredentialForProtectionSpace: space];
  166
+    NSURLCredential* cred = [_request.URL my_credentialForRealm: nil
  167
+                                           authenticationMethod: NSURLAuthenticationMethodHTTPBasic];
171 168
     if (!cred) {
172 169
         LogTo(RemoteRequest, @"Got 401 but no stored credential found (with nil realm)");
173 170
         return false;
@@ -175,7 +172,7 @@ - (bool) retryWithCredential {
175 172
 
176 173
     [_connection cancel];
177 174
     self.authorizer = [[[TDBasicAuthorizer alloc] initWithCredential: cred] autorelease];
178  
-    LogTo(RemoteRequest, @"Got 401 but retrying with %@", _authorizer);
  175
+    LogTo(RemoteRequest, @"%@ retrying with %@", self, _authorizer);
179 176
     [self startAfterDelay: 0.0];
180 177
     return true;
181 178
 }
@@ -187,11 +184,25 @@ - (bool) retryWithCredential {
187 184
 - (void)connection:(NSURLConnection *)connection
188 185
         willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
189 186
 {
190  
-    LogTo(RemoteRequest, @"Got challenge: %@", challenge);
191  
-    _challenged = true;
  187
+    NSString* authMethod = [[challenge protectionSpace] authenticationMethod];
  188
+    LogTo(RemoteRequest, @"Got challenge: %@ (%@)", challenge, authMethod);
  189
+    if ($equal(authMethod, NSURLAuthenticationMethodHTTPBasic)) {
  190
+        _challenged = true;
  191
+        if (challenge.previousFailureCount == 0) {
  192
+            NSURLCredential* cred = [_request.URL my_credentialForRealm: challenge.protectionSpace.realm
  193
+                                                   authenticationMethod: authMethod];
  194
+            if (cred) {
  195
+                [challenge.sender useCredential: cred forAuthenticationChallenge:challenge];
  196
+                return;
  197
+            }
  198
+        }
  199
+    } else if ($equal(authMethod, NSURLAuthenticationMethodServerTrust)) {
  200
+        // TODO: Check trust of server cert
  201
+    }
192 202
     [challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge];
193 203
 }
194 204
 
  205
+
195 206
 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
196 207
     _status = (int) ((NSHTTPURLResponse*)response).statusCode;
197 208
     LogTo(RemoteRequest, @"%@: Got response, status %d", self, _status);
@@ -205,10 +216,31 @@ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLRespon
205 216
         [self cancelWithStatus: _status];
206 217
 }
207 218
 
  219
+
  220
+- (NSURLRequest *)connection:(NSURLConnection *)connection
  221
+             willSendRequest:(NSURLRequest *)request
  222
+            redirectResponse:(NSURLResponse *)response
  223
+{
  224
+    // The redirected request needs to be authorized again:
  225
+    if (![request valueForHTTPHeaderField: @"Authorization"]) {
  226
+        NSMutableURLRequest* nuRequest = [[request mutableCopy] autorelease];
  227
+        NSString* auth;
  228
+        if (_authorizer)
  229
+            auth = [_authorizer authorizeURLRequest: nuRequest forRealm: nil];
  230
+        else
  231
+            auth = [_request valueForHTTPHeaderField: @"Authorization"];
  232
+        [nuRequest setValue: auth forHTTPHeaderField: @"Authorization"];
  233
+        request = nuRequest;
  234
+    }
  235
+    return request;
  236
+}
  237
+
  238
+
208 239
 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
209 240
     LogTo(RemoteRequest, @"%@: Got %lu bytes", self, (unsigned long)data.length);
210 241
 }
211 242
 
  243
+
212 244
 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
213 245
     if (WillLog()) {
214 246
         if (!(_dontLog404 && error.code == kTDStatusNotFound && $equal(error.domain, TDHTTPErrorDomain)))
@@ -229,6 +261,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
229 261
     [self respondWithResult: self error: nil];
230 262
 }
231 263
 
  264
+
232 265
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection
233 266
                   willCacheResponse:(NSCachedURLResponse *)cachedResponse
234 267
 {
2  vendor/MYUtilities
... ...
@@ -1 +1 @@
1  
-Subproject commit e8e65f61f44af31182f09a97fb630bce940f4c4a
  1
+Subproject commit afe6991b970fb04fced8b368fbeb2e49aff274af

0 notes on commit 277cf46

Please sign in to comment.
Something went wrong with that request. Please try again.