libs-base/Source/GSHTTPAuthentication.m

645 lines
16 KiB
Objective-C
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Implementation for GSHTTPAuthentication for GNUstep
Copyright (C) 2006 Software Foundation, Inc.
Written by: Richard Frith-Macdonald <rfm@gnu.org>
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 Lesser 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA.
*/
#import "common.h"
#import "GSURLPrivate.h"
#import "Foundation/NSDictionary.h"
#import "Foundation/NSEnumerator.h"
#import "Foundation/NSScanner.h"
#import "Foundation/NSSet.h"
#import "Foundation/NSValue.h"
#import "GNUstepBase/GSMime.h"
#import "GNUstepBase/NSData+GNUstepBase.h"
static NSMutableDictionary *domainMap = nil;
static NSMutableSet *spaces = nil;
static NSMutableDictionary *store = nil;
static NSLock *storeLock = nil;
static GSMimeParser *mimeParser = 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;
unsigned spos = 0;
unsigned dpos = 0;
NSData *data;
NSString *string;
dst = (char*)NSZoneMalloc(NSDefaultMallocZone(), dlen);
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 GSHTTPAuthentication
+ (void) initialize
{
if (store == nil)
{
mimeParser = [GSMimeParser new];
[[NSObject leakAt: &mimeParser] release];
spaces = [NSMutableSet new];
[[NSObject leakAt: &spaces] release];
domainMap = [NSMutableDictionary new];
[[NSObject leakAt: &domainMap] release];
store = [NSMutableDictionary new];
[[NSObject leakAt: &store] release];
storeLock = [NSLock new];
[[NSObject leakAt: &storeLock] release];
}
}
+ (GSHTTPAuthentication *) authenticationWithCredential:
(NSURLCredential*)credential
inProtectionSpace: (NSURLProtectionSpace*)space
{
NSMutableDictionary *cDict = nil;
NSURLProtectionSpace *known = nil;
GSHTTPAuthentication *authentication = nil;
NSAssert([credential isKindOfClass: [NSURLCredential class]] == YES,
NSInvalidArgumentException);
NSAssert([space isKindOfClass: [NSURLProtectionSpace class]] == YES,
NSInvalidArgumentException);
[storeLock lock];
NS_DURING
{
/*
* Keep track of known protection spaces so we don't make lots of
* duplicate copies, but share one copy between authentication objects.
*/
known = [spaces member: space];
if (known == nil)
{
[spaces addObject: space];
known = [spaces member: space];
}
space = known;
cDict = [store objectForKey: space];
if (cDict == nil)
{
cDict = [NSMutableDictionary new];
[store setObject: cDict forKey: space];
RELEASE(cDict);
}
authentication = [cDict objectForKey: credential];
if (authentication == nil)
{
authentication = [[GSHTTPAuthentication alloc]
initWithCredential: credential
inProtectionSpace: space];
if (authentication != nil)
{
[cDict setObject: authentication
forKey: [authentication credential]];
RELEASE(authentication);
}
}
IF_NO_ARC([[authentication retain] autorelease];)
}
NS_HANDLER
{
[storeLock unlock];
[localException raise];
}
NS_ENDHANDLER
[storeLock unlock];
return authentication;
}
+ (NSURLProtectionSpace*) protectionSpaceForAuthentication: (NSString*)auth
requestURL: (NSURL*)URL;
{
if ([auth isKindOfClass: [NSString class]] == YES)
{
NSString *method = nil;
NSURLProtectionSpace *space;
NSScanner *sc;
NSString *domain = nil;
NSString *realm = nil;
NSString *key;
NSString *val;
space = [self protectionSpaceForURL: URL];
sc = [NSScanner scannerWithString: auth];
key = [mimeParser scanName: sc];
if ([key caseInsensitiveCompare: @"Basic"] == NSOrderedSame)
{
method = NSURLAuthenticationMethodHTTPBasic;
domain = [URL path];
}
else if ([key caseInsensitiveCompare: @"Digest"] == NSOrderedSame)
{
method = NSURLAuthenticationMethodHTTPDigest;
}
else if ([key caseInsensitiveCompare: @"NTLM"] == NSOrderedSame)
{
method = NSURLAuthenticationMethodNTLM;
}
else if ([key caseInsensitiveCompare: @"Negotiate"] == NSOrderedSame)
{
method = NSURLAuthenticationMethodNegotiate;
}
else
{
return nil; // Unknown authentication
}
while ((key = [mimeParser scanName: sc]) != nil)
{
if ([sc scanString: @"=" intoString: 0] == NO)
{
return nil; // Bad name=value specification
}
if ((val = [mimeParser scanToken: sc]) == nil)
{
return nil; // Bad name=value specification
}
if ([key caseInsensitiveCompare: @"domain"] == NSOrderedSame)
{
domain = val;
}
else if ([key caseInsensitiveCompare: @"realm"] == NSOrderedSame)
{
realm = val;
}
if ([sc scanString: @"," intoString: 0] == NO)
{
break; // No more in list.
}
}
if (realm == nil)
{
return nil; // No realm to authenticate in
}
/*
* If the realm and authentication method match the space we
* found for the URL, assume that it is unchanged.
*/
if ([[space realm] isEqualToString: realm]
&& [space authenticationMethod] == method)
{
return space;
}
space = [[NSURLProtectionSpace alloc] initWithHost: [URL host]
port: [[URL port] intValue]
protocol: [URL scheme]
realm: realm
authenticationMethod: method];
[self setProtectionSpace: space
forDomains: [domain componentsSeparatedByString: @" "]
baseURL: URL];
return AUTORELEASE(space);
}
return nil;
}
+ (NSURLProtectionSpace *) protectionSpaceForURL: (NSURL*)URL
{
NSURLProtectionSpace *space = nil;
NSString *scheme;
NSNumber *port;
NSString *server;
scheme = [URL scheme];
port = [URL port];
if ([port intValue] == 80 && [scheme isEqualToString: @"http"])
{
port = nil;
}
else if ([port intValue] == 443 && [scheme isEqualToString: @"https"])
{
port = nil;
}
if ([port intValue] == 0)
{
server = [NSString stringWithFormat: @"%@://%@",
scheme, [URL host]];
}
else
{
server = [NSString stringWithFormat: @"%@://%@:%@",
scheme, [URL host], port];
}
[storeLock lock];
NS_DURING
{
NSString *found = nil;
NSDictionary *sDict;
NSArray *keys;
unsigned count;
NSString *path;
sDict = [domainMap objectForKey: server];
keys = [sDict allKeys];
count = [keys count];
path = [URL path];
while (count-- > 0)
{
NSString *key = [keys objectAtIndex: count];
unsigned kl = [key length];
if (found == nil || kl > [found length])
{
if (kl == 0 || [path hasPrefix: key] == YES)
{
found = key;
}
}
}
if (found != nil)
{
space = AUTORELEASE(RETAIN([sDict objectForKey: found]));
}
}
NS_HANDLER
{
[storeLock unlock];
[localException raise];
}
NS_ENDHANDLER
[storeLock unlock];
return space;
}
+ (void) setProtectionSpace: (NSURLProtectionSpace *)space
forDomains: (NSArray*)domains
baseURL: (NSURL*)base
{
/*
* If there are no URIs specified, everything on the
* host of the base URL is in the protection space
*/
if ([domains count] == 0)
{
domains = [NSArray arrayWithObject: @"/"];
}
[storeLock lock];
NS_DURING
{
NSEnumerator *e = [domains objectEnumerator];
NSString *domain;
while ((domain = [e nextObject]) != nil)
{
NSURL *u;
NSString *path;
NSNumber *port;
NSString *scheme;
NSString *server;
NSMutableDictionary *sDict;
u = [NSURL URLWithString: domain];
scheme = [u scheme];
if (scheme == nil)
{
u = [NSURL URLWithString: domain relativeToURL: base];
scheme = [u scheme];
}
port = [u port];
if ([port intValue] == 80 && [scheme isEqualToString: @"http"])
{
port = nil;
}
else if ([port intValue] == 443 && [scheme isEqualToString: @"https"])
{
port = nil;
}
path = [u path];
if (path == nil)
{
path = @"";
}
if ([port intValue] == 0)
{
server = [NSString stringWithFormat: @"%@://%@",
scheme, [u host]];
}
else
{
server = [NSString stringWithFormat: @"%@://%@:%@",
scheme, [u host], port];
}
sDict = [domainMap objectForKey: server];
if (sDict == nil)
{
sDict = [NSMutableDictionary new];
[domainMap setObject: sDict forKey: server];
RELEASE(sDict);
}
[sDict setObject: space forKey: path];
}
}
NS_HANDLER
{
[storeLock unlock];
[localException raise];
}
NS_ENDHANDLER
[storeLock unlock];
}
- (NSString*) authorizationForAuthentication: (NSString*)authentication
method: (NSString*)method
path: (NSString*)path
{
NSMutableString *authorisation;
if ([self->_space authenticationMethod]
== NSURLAuthenticationMethodHTTPDigest)
{
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;
int nc;
if (authentication != nil)
{
NSScanner *sc;
NSString *key;
NSString *val;
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 = [mimeParser scanName: sc]) != nil)
{
if ([sc scanString: @"=" intoString: 0] == NO)
{
NSDebugMLog(@"Missing '=' in HTTP digest '%@'",
authentication);
return nil; // Bad name=value specification
}
if ((val = [mimeParser 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 isEqualToString: [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 ([algorithm isEqualToString: @"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 isEqualToString: _nonce] == NO)
{
_nc = 1;
}
ASSIGN(_nonce, nonce);
ASSIGN(_qop, qop);
ASSIGN(_opaque, opaque);
}
else
{
[self->_lock lock];
nonce = _nonce;
opaque = _opaque;
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 = [NSMutableString stringWithCapacity: 512];
[authorisation appendFormat: @"Digest realm=\"%@\"", realm];
[authorisation appendFormat: @",username=\"%@\"",
[self->_credential user]];
[authorisation appendFormat: @",nonce=\"%@\"", nonce];
[authorisation appendFormat: @",uri=\"%@\"", path];
[authorisation appendFormat: @",response=\"%@\"", response];
[authorisation appendFormat: @",qop=\"%@\"", qop];
[authorisation appendFormat: @",nc=%08x", nc];
[authorisation appendFormat: @",cnonce=\"%@\"", cnonce];
if (opaque != nil)
{
[authorisation appendFormat: @",opaque=\"%@\"", opaque];
}
[self->_lock unlock];
}
else if ([self->_space authenticationMethod]
== NSURLAuthenticationMethodHTMLForm)
{
// This should not generate any authentication header.
return nil;
}
else if ([self->_space authenticationMethod]
== NSURLAuthenticationMethodNTLM)
{
// FIXME: this needs to be implemented
return nil;
}
else if ([self->_space authenticationMethod]
== NSURLAuthenticationMethodNegotiate)
{
// FIXME: this needs to be implemented
return nil;
}
else if ([self->_space authenticationMethod]
== NSURLAuthenticationMethodDefault
|| [self->_space authenticationMethod]
== NSURLAuthenticationMethodHTTPBasic)
{
NSString *toEncode;
if (authentication != nil)
{
NSScanner *sc;
sc = [NSScanner scannerWithString: authentication];
if ([sc scanString: @"Basic" intoString: 0] == NO)
{
NSDebugMLog(@"Bad format HTTP basic in '%@'", authentication);
return nil; // Not a basic authentication
}
}
authorisation = [NSMutableString stringWithCapacity: 64];
if ([[self->_credential password] length] > 0)
{
toEncode = [NSString stringWithFormat: @"%@:%@",
[self->_credential user], [self->_credential password]];
}
else
{
toEncode = [NSString stringWithFormat: @"%@",
[self->_credential user]];
}
[authorisation appendFormat: @"Basic %@",
[GSMimeDocument encodeBase64String: toEncode]];
}
else
{
// FIXME: Currently, ClientCertificate and ServerTrust authentication
// methods are NOT implemented and will end up here. They should, in fact,
// be handled in the SSL connection layer (in GSHTTPURLHandle) rather than
// in this method.
return nil;
}
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 = [NSLock new];
ASSIGN(self->_space, space);
ASSIGN(self->_credential, credential);
}
return self;
}
- (NSURLProtectionSpace *) space
{
return self->_space;
}
@end