From c4cedc4b158adededa1fc2a8252ec8f06798ddd0 Mon Sep 17 00:00:00 2001 From: rfm Date: Mon, 19 Jun 2006 11:20:17 +0000 Subject: [PATCH] Add class to handle http digest authentication. git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/base/trunk@23082 72102866-910b-0410-8b05-ffd578937521 --- ChangeLog | 9 + .../Foundation/NSURLAuthenticationChallenge.h | 11 + Source/GNUmakefile | 1 + Source/GSHTTPDigest.m | 300 ++++++++++++++++++ Source/GSURLPrivate.h | 34 ++ Source/NSURLProtectionSpace.m | 82 ++++- 6 files changed, 426 insertions(+), 11 deletions(-) create mode 100644 Source/GSHTTPDigest.m diff --git a/ChangeLog b/ChangeLog index 9a4092107..9a7b37bd8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2006-06-19 Richard Frith-Macdonald + + * Source/GSURLPrivate.h: Add GSHTTPDigest interface. + * Source/GSHTTPDigest.m: Add GSHTTPDigest implementation. + * Source/GNUmakefile: Build GSHTTPDigest. + * Source/NSURLProtectionSpace.m: Implement ([-hash]) and ([-isEqual:]) + * Headers/Foundation/NSURLAuthenticationChallenge.h: Improve comments. + Add GSHTTPDigest class to handle HTTP digest authentication. + 2006-06-17 Richard Frith-Macdonald * Source/GSString.m: Fix memory leak. diff --git a/Headers/Foundation/NSURLAuthenticationChallenge.h b/Headers/Foundation/NSURLAuthenticationChallenge.h index ab55a3f69..13630cd4f 100644 --- a/Headers/Foundation/NSURLAuthenticationChallenge.h +++ b/Headers/Foundation/NSURLAuthenticationChallenge.h @@ -36,20 +36,31 @@ @class NSURLResponse; /** + * A challenge sender (usually an NSURLProtocol subclass handling a + * connection/download) provides these methods to permit a client to + * control authentication. */ @protocol NSURLAuthenticationChallengeSender /** + * Cancels the authenticatiopn challenge, ensuring that the load operation + * will fail to retrieve data, completing with only the response headers + * containing the challenge having been read from the server. */ - (void) cancelAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge; /** + * Tells the sender to continue the load without providing a new credential + * for it to use ... if the challenge already had a proposed credential, + * the sender may elect to use it. */ - (void) continueWithoutCredentialForAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge; /** + * Tells the sender to continue the load using the new credential + * provided by this method. */ - (void) useCredential: (NSURLCredential *)credential forAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge; diff --git a/Source/GNUmakefile b/Source/GNUmakefile index d5b7c21b6..f5618a6a4 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -138,6 +138,7 @@ GSCountedSet.m \ GSDictionary.m \ GSFormat.m \ GSFTPURLHandle.m \ +GSHTTPDigest.m \ GSHTTPURLHandle.m \ GSRunLoopWatcher.m \ GSSet.m \ diff --git a/Source/GSHTTPDigest.m b/Source/GSHTTPDigest.m new file mode 100644 index 000000000..0ab6fef4b --- /dev/null +++ b/Source/GSHTTPDigest.m @@ -0,0 +1,300 @@ +/* Implementation for GSHTTPDigest for GNUstep + Copyright (C) 2006 Software Foundation, Inc. + + Written by: Richard Frith-Macdonald + Date: 2006 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02111 USA. + */ + +#include "GSURLPrivate.h" +#include "Foundation/NSDictionary.h" +#include "Foundation/NSScanner.h" +#include "Foundation/NSDebug.h" +#include "GNUstepBase/GSLock.h" +#include "GNUstepBase/GSMime.h" + + +static NSMutableDictionary *store = nil; +static GSLazyLock *storeLock = nil; + +@interface NSData(GSHTTPDigest) +- (NSString*) digestHex; +@end +@implementation NSData(GSHTTPDigest) +- (NSString*) digestHex +{ + static const char *hexChars = "0123456789abcdef"; + unsigned slen = [self length]; + unsigned dlen = slen * 2; + const unsigned char *src = (const unsigned char *)[self bytes]; + char *dst = (char*)NSZoneMalloc(NSDefaultMallocZone(), dlen); + unsigned spos = 0; + unsigned dpos = 0; + NSData *data; + NSString *string; + + while (spos < slen) + { + unsigned char c = src[spos++]; + + dst[dpos++] = hexChars[(c >> 4) & 0x0f]; + dst[dpos++] = hexChars[c & 0x0f]; + } + data = [NSData allocWithZone: NSDefaultMallocZone()]; + data = [data initWithBytesNoCopy: dst length: dlen]; + string = [[NSString alloc] initWithData: data + encoding: NSASCIIStringEncoding]; + RELEASE(data); + return AUTORELEASE(string); +} +@end + + + +@implementation GSHTTPDigest + ++ (void) initialize +{ + if (store == nil) + { + store = [NSMutableDictionary new]; + storeLock = [GSLazyLock new]; + } +} + ++ (GSHTTPDigest *) digestWithCredential: (NSURLCredential*)credential + inProtectionSpace: (NSURLProtectionSpace*)space +{ + NSMutableDictionary *cDict; + GSHTTPDigest *digest = nil; + + [storeLock lock]; + cDict = [store objectForKey: space]; + if (cDict == nil) + { + cDict = [NSMutableDictionary new]; + [store setObject: cDict forKey: space]; + RELEASE(cDict); + } + digest = [cDict objectForKey: credential]; + if (digest == nil) + { + digest = [[GSHTTPDigest alloc] initWithCredential: credential + inProtectionSpace: space]; + [cDict setObject: digest forKey: [digest credential]]; + } + else + { + RETAIN(digest); + } + [storeLock unlock]; + return AUTORELEASE(digest); +} + +- (NSString*) authorizationForAuthentication: (NSString*)authentication + method: (NSString*)method + path: (NSString*)path +{ + NSString *realm = nil; + NSString *qop = nil; + NSString *nonce = nil; + NSString *opaque = nil; + NSString *stale = @"FALSE"; + NSString *algorithm = @"MD5"; + NSString *cnonce; + NSString *HA1; + NSString *HA2; + NSString *response; + NSString *authorisation; + int nc; + + if (authentication != nil) + { + static GSMimeParser *p = nil; + NSScanner *sc; + NSString *key; + NSString *val; + + if (p == nil) + { + p = [GSMimeParser new]; + } + sc = [NSScanner scannerWithString: authentication]; + if ([sc scanString: @"Digest" intoString: 0] == NO) + { + NSDebugMLog(@"Bad format HTTP digest in '%@'", authentication); + return nil; // Not a digest authentication + } + while ((key = [p scanName: sc]) != nil) + { + if ([sc scanString: @"=" intoString: 0] == NO) + { + NSDebugMLog(@"Missing '=' in HTTP digest '%@'", authentication); + return nil; // Bad name=value specification + } + if ((val = [p scanToken: sc]) == nil) + { + NSDebugMLog(@"Missing value in HTTP digest '%@'", authentication); + return nil; // Bad name=value specification + } + if ([key caseInsensitiveCompare: @"realm"] == NSOrderedSame) + { + realm = val; + } + if ([key caseInsensitiveCompare: @"qop"] == NSOrderedSame) + { + qop = val; + } + if ([key caseInsensitiveCompare: @"nonce"] == NSOrderedSame) + { + nonce = val; + } + if ([key caseInsensitiveCompare: @"opaque"] == NSOrderedSame) + { + opaque = val; + } + if ([key caseInsensitiveCompare: @"stale"] == NSOrderedSame) + { + stale = val; + } + if ([key caseInsensitiveCompare: @"algorithm"] == NSOrderedSame) + { + algorithm = val; + } + if ([sc scanString: @"," intoString: 0] == NO) + { + break; // No more in list. + } + } + + if (realm == nil) + { + NSDebugMLog(@"Missing HTTP digest realm in '%@'", authentication); + return nil; + } + if ([realm isEqual: [self->_space realm]] == NO) + { + NSDebugMLog(@"Bad HTTP digest realm in '%@'", authentication); + return nil; + } + if (nonce == nil) + { + NSDebugMLog(@"Missing HTTP digest nonce in '%@'", authentication); + return nil; + } + if (opaque == nil) + { + NSDebugMLog(@"Missing HTTP digest opaque in '%@'", authentication); + return nil; + } + + if ([algorithm isEqual: @"MD5"] == NO) + { + NSDebugMLog(@"Unsupported HTTP digest algorithm in '%@'", + authentication); + return nil; + } + if (![[qop componentsSeparatedByString: @","] containsObject: @"auth"]) + { + NSDebugMLog(@"Unsupported/missing HTTP digest qop in '%@'", + authentication); + return nil; + } + + [self->_lock lock]; + if ([stale boolValue] == YES || [nonce isEqual: _nonce] == NO) + { + _nc = 1; + } + ASSIGN(_nonce, nonce); + ASSIGN(_qop, qop); + ASSIGN(_opaque, opaque); + } + else + { + [self->_lock lock]; + nonce = _nonce; + opaque = _opaque; + qop = _qop; + realm = [self->_space realm]; + } + + nc = _nc++; + + qop = @"auth"; + + cnonce = [[[[[NSProcessInfo processInfo] globallyUniqueString] + dataUsingEncoding: NSUTF8StringEncoding] md5Digest] digestHex]; + + HA1 = [[[[NSString stringWithFormat: @"%@:%@:%@", + [self->_credential user], realm, [self->_credential password]] + dataUsingEncoding: NSUTF8StringEncoding] md5Digest] digestHex]; + + HA2 = [[[[NSString stringWithFormat: @"%@:%@", method, path] + dataUsingEncoding: NSUTF8StringEncoding] md5Digest] digestHex]; + + response = [[[[NSString stringWithFormat: @"%@:%@:%08x:%@:%@:%@", + HA1, nonce, nc, cnonce, qop, HA2] + dataUsingEncoding: NSUTF8StringEncoding] md5Digest] digestHex]; + + authorisation = [NSString stringWithFormat: @"Digest username=\"%@\"," + @"realm=\"%@\",nonce=\"%@\",uri=\"%@\",qop=\"%@\",nc=%08x,cnonce=\"%@\"," + @"response=\"%@\",opaque=\"%@\"", + [self->_credential user], + realm, nonce, path, qop, nc, cnonce, response, opaque]; + + [self->_lock unlock]; + + return authorisation; +} + +- (NSURLCredential *) credential +{ + return self->_credential; +} + +- (void) dealloc +{ + RELEASE(_credential); + RELEASE(_space); + RELEASE(_nonce); + RELEASE(_opaque); + RELEASE(_qop); + RELEASE(_lock); + [super dealloc]; +} + +- (id) initWithCredential: (NSURLCredential*)credential + inProtectionSpace: (NSURLProtectionSpace*)space +{ + if ((self = [super init]) != nil) + { + self->_lock = [GSLazyLock new]; + ASSIGNCOPY(self->_space, space); + ASSIGNCOPY(self->_credential, credential); + } + return self; +} + +- (NSURLProtectionSpace *) space +{ + return self->_space; +} +@end + diff --git a/Source/GSURLPrivate.h b/Source/GSURLPrivate.h index 895ab0f9e..97d2515d9 100644 --- a/Source/GSURLPrivate.h +++ b/Source/GSURLPrivate.h @@ -62,5 +62,39 @@ - (void) _setProperty: (id)value forKey: (NSString*)key; @end + + + +/* + * Internal class for handling HTTP digest authentication + */ +@interface GSHTTPDigest : NSObject +{ + GSLazyLock *_lock; + NSURLCredential *_credential; + NSURLProtectionSpace *_space; + NSString *_nonce; + NSString *_opaque; + NSString *_qop; + int _nc; +} +/* + * Return the object for the specified credential/protection space. + */ ++ (GSHTTPDigest *) digestWithCredential: (NSURLCredential*)credential + inProtectionSpace: (NSURLProtectionSpace*)space; +/* + * Generate next authorisation header for the specified authentication + * header, method, and path. + */ +- (NSString*) authorizationForAuthentication: (NSString*)authentication + method: (NSString*)method + path: (NSString*)path; +- (NSURLCredential *) credential; +- (id) initWithCredential: (NSURLCredential*)credential + inProtectionSpace: (NSURLProtectionSpace*)space; +- (NSURLProtectionSpace *) space; +@end + #endif diff --git a/Source/NSURLProtectionSpace.m b/Source/NSURLProtectionSpace.m index ecc2aa0b7..b611c3ef3 100644 --- a/Source/NSURLProtectionSpace.m +++ b/Source/NSURLProtectionSpace.m @@ -74,19 +74,26 @@ typedef struct { - (id) copyWithZone: (NSZone*)z { - NSURLProtectionSpace *o = [[self class] allocWithZone: z]; - - o = [o initWithHost: this->host - port: this->port - protocol: this->protocol - realm: this->realm - authenticationMethod: this->authenticationMethod]; - if (o != nil) + if (NSShouldRetainWithZone(self, z) == YES) { - inst->isProxy = this->isProxy; - ASSIGN(inst->proxyType, this->proxyType); + return RETAIN(self); + } + else + { + NSURLProtectionSpace *o = [[self class] allocWithZone: z]; + + o = [o initWithHost: this->host + port: this->port + protocol: this->protocol + realm: this->realm + authenticationMethod: this->authenticationMethod]; + if (o != nil) + { + inst->isProxy = this->isProxy; + ASSIGN(inst->proxyType, this->proxyType); + } + return o; } - return o; } - (void) dealloc @@ -103,6 +110,13 @@ typedef struct { [super dealloc]; } +- (unsigned) hash +{ + return [[self host] hash] + [self port] + + [[self realm] hash] + [[self protocol] hash] + + [[self proxyType] hash] + [[self authenticationMethod] hash]; +} + - (NSString *) host { return this->host; @@ -146,6 +160,52 @@ authenticationMethod: (NSString *)authenticationMethod return NO; } +- (unsigned) isEqual: (id)other +{ + if ([other isKindOfClass: [NSURLProtocol class]] == NO) + { + return NO; + } + else + { + NSURLProtectionSpace *o = (NSURLProtectionSpace*)other; + + if (![[self host] isEqual: [o host]] == NO) + { + return NO; + } + if (![[self realm] isEqual: [o realm]] == NO) + { + return NO; + } + if ([self port] != [o port]) + { + return NO; + } + if (![[self authenticationMethod] isEqual: [o authenticationMethod]]) + { + return NO; + } + if ([self isProxy] == YES) + { + if ([o isProxy] == NO + || [[self proxyType] isEqual: [o proxyType]] == NO) + { + return NO; + } + } + else + { + if ([o isProxy] == YES + || [[self protocol] isEqual: [o protocol]] == NO) + { + return NO; + } + } + return YES; + } +} + - (BOOL) isProxy { return this->isProxy;