From 1e17d5c455491d612299f3c86fbe9510820fd5e3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Fri, 18 Apr 2014 12:32:55 -0300 Subject: [PATCH] #38 Added support for digest authentication --- GCDWebServer/Core/GCDWebServer.h | 1 + GCDWebServer/Core/GCDWebServer.m | 12 ++++++- GCDWebServer/Core/GCDWebServerConnection.m | 39 +++++++++++++++++++++- GCDWebServer/Core/GCDWebServerFunctions.m | 20 +++++++++++ GCDWebServer/Core/GCDWebServerPrivate.h | 2 ++ Mac/main.m | 17 +++++++--- README.md | 2 +- 7 files changed, 86 insertions(+), 7 deletions(-) diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index 59583bb..bc9d811 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -58,6 +58,7 @@ extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; // #endif extern NSString* const GCDWebServerAuthenticationMethod_Basic; +extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; @class GCDWebServer; diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index ec123bb..695855e 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -56,6 +56,7 @@ NSString* _serverName; NSString* _authenticationRealm; NSString* _authenticationBasicAccount; + NSString* _authenticationDigestAccount; Class _connectionClass; BOOL _mapHEADToGET; CFTimeInterval _disconnectDelay; @@ -95,6 +96,7 @@ NSString* const GCDWebServerOption_AutomaticallySuspendInBackground = @"Automati #endif NSString* const GCDWebServerAuthenticationMethod_Basic = @"Basic"; +NSString* const GCDWebServerAuthenticationMethod_DigestAccess = @"DigestAccess"; #ifndef __GCDWEBSERVER_LOGGING_HEADER__ #ifdef NDEBUG @@ -155,7 +157,8 @@ static void _SignalHandler(int signal) { @implementation GCDWebServer @synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, authenticationRealm=_authenticationRealm, - authenticationBasicAccount=_authenticationBasicAccount, shouldAutomaticallyMapHEADToGET=_mapHEADToGET; + authenticationBasicAccount=_authenticationBasicAccount, authenticationDigestAccount=_authenticationDigestAccount, + shouldAutomaticallyMapHEADToGET=_mapHEADToGET; #ifndef __GCDWEBSERVER_LOGGING_HEADER__ @@ -383,6 +386,11 @@ static inline NSString* _EncodeBase64(NSString* string) { NSString* user = _GetOption(_options, GCDWebServerOption_AuthenticationUser, @""); NSString* password = _GetOption(_options, GCDWebServerOption_AuthenticationPassword, @""); _authenticationBasicAccount = ARC_RETAIN(_EncodeBase64([NSString stringWithFormat:@"%@:%@", user, password])); + } else if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_DigestAccess]) { + _authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy]; + NSString* user = _GetOption(_options, GCDWebServerOption_AuthenticationUser, @""); + NSString* password = _GetOption(_options, GCDWebServerOption_AuthenticationPassword, @""); + _authenticationDigestAccount = ARC_RETAIN(GCDWebServerComputeMD5Digest(@"%@:%@:%@", user, _authenticationRealm, password)); } _connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); _mapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; @@ -502,6 +510,8 @@ static inline NSString* _EncodeBase64(NSString* string) { _authenticationRealm = nil; ARC_RELEASE(_authenticationBasicAccount); _authenticationBasicAccount = nil; + ARC_RELEASE(_authenticationDigestAccount); + _authenticationDigestAccount = nil; LOG_INFO(@"%@ stopped", [self class]); if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { diff --git a/GCDWebServer/Core/GCDWebServerConnection.m b/GCDWebServer/Core/GCDWebServerConnection.m index 434bfed..4023a28 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.m +++ b/GCDWebServer/Core/GCDWebServerConnection.m @@ -48,6 +48,7 @@ static NSData* _CRLFData = nil; static NSData* _CRLFCRLFData = nil; static NSData* _continueData = nil; static NSData* _lastChunkData = nil; +static NSString* _digestAuthenticationNonce = nil; #ifdef __GCDWEBSERVER_ENABLE_TESTING__ static int32_t _connectionCounter = 0; #endif @@ -357,6 +358,11 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (_lastChunkData == nil) { _lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; } + if (_digestAuthenticationNonce == nil) { + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + _digestAuthenticationNonce = ARC_RETAIN(GCDWebServerComputeMD5Digest(@"%@", ARC_BRIDGE_RELEASE(CFUUIDCreateString(kCFAllocatorDefault, uuid)))); + CFRelease(uuid); + } } - (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode { @@ -707,15 +713,46 @@ static NSString* _StringFromAddressData(NSData* data) { #endif } +// https://tools.ietf.org/html/rfc2617 - (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request { LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead); GCDWebServerResponse* response = nil; if (_server.authenticationBasicAccount) { + BOOL authenticated = NO; NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"]; - if (![authorizationHeader hasPrefix:@"Basic "] || ![[authorizationHeader substringFromIndex:6] isEqualToString:_server.authenticationBasicAccount]) { + if ([authorizationHeader hasPrefix:@"Basic "]) { + NSString* basicAccount = [authorizationHeader substringFromIndex:6]; + if ([basicAccount isEqualToString:_server.authenticationBasicAccount]) { + authenticated = YES; + } + } + if (!authenticated) { response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized]; [response setValue:[NSString stringWithFormat:@"Basic realm=\"%@\"", _server.authenticationRealm] forAdditionalHeader:@"WWW-Authenticate"]; } + } else if (_server.authenticationDigestAccount) { + BOOL authenticated = NO; + BOOL isStaled = NO; + NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"]; + if ([authorizationHeader hasPrefix:@"Digest "]) { + NSString* nonce = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"nonce"); + if ([nonce isEqualToString:_digestAuthenticationNonce]) { // TODO: Also check "realm" and "username" provided by client + NSString* uri = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"uri"); + NSString* actualResponse = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"response"); + NSString* ha1 = _server.authenticationDigestAccount; + NSString* ha2 = GCDWebServerComputeMD5Digest(@"%@:%@", request.method, uri); // We cannot use "request.path" as the query string is required + NSString* expectedResponse = GCDWebServerComputeMD5Digest(@"%@:%@:%@", ha1, _digestAuthenticationNonce, ha2); + if ([actualResponse isEqualToString:expectedResponse]) { + authenticated = YES; + } + } else if (nonce.length) { + isStaled = YES; + } + } + if (!authenticated) { + response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized]; + [response setValue:[NSString stringWithFormat:@"Digest realm=\"%@\", nonce=\"%@\"%@", _server.authenticationRealm, _digestAuthenticationNonce, isStaled ? @", stale=TRUE" : @""] forAdditionalHeader:@"WWW-Authenticate"]; // TODO: Support Quality of Protection ("qop") + } } return response; } diff --git a/GCDWebServer/Core/GCDWebServerFunctions.m b/GCDWebServer/Core/GCDWebServerFunctions.m index 328dc5f..59451ab 100644 --- a/GCDWebServer/Core/GCDWebServerFunctions.m +++ b/GCDWebServer/Core/GCDWebServerFunctions.m @@ -31,6 +31,7 @@ #else #import #endif +#import #import #import @@ -264,3 +265,22 @@ NSString* GCDWebServerGetPrimaryIPv4Address() { } return address; } + +NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) { + va_list arguments; + va_start(arguments, format); + const char* string = [ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]) UTF8String]; + va_end(arguments); + unsigned char md5[CC_MD5_DIGEST_LENGTH]; + CC_MD5(string, (CC_LONG)strlen(string), md5); + char buffer[2 * CC_MD5_DIGEST_LENGTH + 1]; + for (int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) { + unsigned char byte = md5[i]; + unsigned char byteHi = (byte & 0xF0) >> 4; + buffer[2 * i + 0] = byteHi >= 10 ? 'a' + byteHi - 10 : '0' + byteHi; + unsigned char byteLo = byte & 0x0F; + buffer[2 * i + 1] = byteLo >= 10 ? 'a' + byteLo - 10 : '0' + byteLo; + } + buffer[2 * CC_MD5_DIGEST_LENGTH] = 0; + return [NSString stringWithUTF8String:buffer]; +} diff --git a/GCDWebServer/Core/GCDWebServerPrivate.h b/GCDWebServer/Core/GCDWebServerPrivate.h index 13afa03..6ed246d 100644 --- a/GCDWebServer/Core/GCDWebServerPrivate.h +++ b/GCDWebServer/Core/GCDWebServerPrivate.h @@ -121,6 +121,7 @@ extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSStr extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); extern BOOL GCDWebServerIsTextContentType(NSString* type); extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType); +extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_FUNCTION(1,2); @interface GCDWebServerConnection () - (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; @@ -131,6 +132,7 @@ extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType); @property(nonatomic, readonly) NSString* serverName; @property(nonatomic, readonly) NSString* authenticationRealm; @property(nonatomic, readonly) NSString* authenticationBasicAccount; +@property(nonatomic, readonly) NSString* authenticationDigestAccount; @property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET; - (void)willStartConnection:(GCDWebServerConnection*)connection; - (void)didEndConnection:(GCDWebServerConnection*)connection; diff --git a/Mac/main.m b/Mac/main.m index 5c4bf92..ff817e8 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -130,12 +130,13 @@ int main(int argc, const char* argv[]) { BOOL recording = NO; NSString* rootDirectory = NSHomeDirectory(); NSString* testDirectory = nil; + NSString* authenticationMethod = nil; NSString* authenticationRealm = nil; NSString* authenticationUser = nil; NSString* authenticationPassword = nil; if (argc == 1) { - fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0])); + fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0])); } else { for (int i = 1; i < argc; ++i) { if (argv[i][0] != '-') { @@ -164,6 +165,9 @@ int main(int argc, const char* argv[]) { } else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) { ++i; testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath]; + } else if (!strcmp(argv[i], "-authenticationMethod") && (i + 1 < argc)) { + ++i; + authenticationMethod = [NSString stringWithUTF8String:argv[i]]; } else if (!strcmp(argv[i], "-authenticationRealm") && (i + 1 < argc)) { ++i; authenticationRealm = [NSString stringWithUTF8String:argv[i]]; @@ -296,11 +300,16 @@ int main(int argc, const char* argv[]) { NSMutableDictionary* options = [NSMutableDictionary dictionary]; [options setObject:@8080 forKey:GCDWebServerOption_Port]; [options setObject:@"" forKey:GCDWebServerOption_BonjourName]; - if (authenticationUser && authenticationPassword) { + if ([authenticationMethod isEqualToString:@"Basic"]) { [options setObject:GCDWebServerAuthenticationMethod_Basic forKey:GCDWebServerOption_AuthenticationMethod]; [options setValue:authenticationRealm forKey:GCDWebServerOption_AuthenticationRealm]; - [options setObject:authenticationUser forKey:GCDWebServerOption_AuthenticationUser]; - [options setObject:authenticationPassword forKey:GCDWebServerOption_AuthenticationPassword]; + [options setValue:authenticationUser forKey:GCDWebServerOption_AuthenticationUser]; + [options setValue:authenticationPassword forKey:GCDWebServerOption_AuthenticationPassword]; + } else if ([authenticationMethod isEqualToString:@"Digest"]) { + [options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod]; + [options setValue:authenticationRealm forKey:GCDWebServerOption_AuthenticationRealm]; + [options setValue:authenticationUser forKey:GCDWebServerOption_AuthenticationUser]; + [options setValue:authenticationPassword forKey:GCDWebServerOption_AuthenticationPassword]; } if ([webServer runWithOptions:options]) { result = 0; diff --git a/README.md b/README.md index 8147561..f95ca76 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Extra built-in features: * [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for request and response HTTP bodies * [HTTP range](https://en.wikipedia.org/wiki/Byte_serving) support for requests of local files * Automatically handle transitions between foreground, background and suspended modes in iOS apps -* [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) for simple password protection +* [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) and [Digest Access](https://en.wikipedia.org/wiki/Digest_access_authentication) Authentications for password protection Included extensions: * [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of ```GCDWebServer``` that implements an interface for uploading and downloading files using a web browser