/** GSHTTPURLHandle.m - Class GSHTTPURLHandle Copyright (C) 2000 Free Software Foundation, Inc. Written by: Mark Allison Integrated by: Richard Frith-Macdonald Date: November 2000 This file is part of the GNUstep 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110 USA. */ #import "common.h" #import "Foundation/NSArray.h" #import "Foundation/NSDictionary.h" #import "Foundation/NSEnumerator.h" #import "Foundation/NSByteOrder.h" #import "Foundation/NSData.h" #import "Foundation/NSException.h" #import "Foundation/NSFileHandle.h" #import "Foundation/NSHost.h" #import "Foundation/NSLock.h" #import "Foundation/NSMapTable.h" #import "Foundation/NSNotification.h" #import "Foundation/NSPathUtilities.h" #import "Foundation/NSProcessInfo.h" #import "Foundation/NSRunLoop.h" #import "Foundation/NSURL.h" #import "Foundation/NSURLHandle.h" #import "Foundation/NSValue.h" #import "GNUstepBase/GSMime.h" #import "GNUstepBase/GSTLS.h" #import "GNUstepBase/NSData+GNUstepBase.h" #import "GNUstepBase/NSString+GNUstepBase.h" #import "GNUstepBase/NSURL+GNUstepBase.h" #import "NSCallBacks.h" #import "GSURLPrivate.h" #import "GSPrivate.h" #ifdef HAVE_SYS_FILE_H # include #endif #if defined(HAVE_SYS_FCNTL_H) # include #elif defined(HAVE_FCNTL_H) # include #endif #ifdef HAVE_SYS_SOCKET_H # include // For MSG_PEEK, etc #endif @interface GSMimeHeader (HTTPRequest) - (void) addToBuffer: (NSMutableData*)buf masking: (NSMutableData**)masked; @end /* * Implement map keys for strings with case insensitive comparisons, * so we can have case insensitive matching of http headers (correct * behavior), but actually preserve case of headers stored and written * in case the remote server is buggy and requires particular * captialisation of headers (some http software is faulty like that). */ static NSUInteger _id_hash(void *table, NSString* o) { return [[o uppercaseString] hash]; } static BOOL _id_is_equal(void *table, NSString *o, NSString *p) { return ([o caseInsensitiveCompare: p] == NSOrderedSame) ? YES : NO; } typedef NSUInteger (*NSMT_hash_func_t)(NSMapTable *, const void *); typedef BOOL (*NSMT_is_equal_func_t)(NSMapTable *, const void *, const void *); typedef void (*NSMT_retain_func_t)(NSMapTable *, const void *); typedef void (*NSMT_release_func_t)(NSMapTable *, void *); typedef NSString *(*NSMT_describe_func_t)(NSMapTable *, const void *); static const NSMapTableKeyCallBacks writeKeyCallBacks = { (NSMT_hash_func_t) _id_hash, (NSMT_is_equal_func_t) _id_is_equal, (NSMT_retain_func_t) _NS_id_retain, (NSMT_release_func_t) _NS_id_release, (NSMT_describe_func_t) _NS_id_describe, NSNotAPointerMapKey }; static NSString *httpVersion = @"1.1"; @interface GSHTTPURLHandle : NSURLHandle { BOOL tunnel; BOOL debug; BOOL keepalive; BOOL returnAll; BOOL inResponse; id ioDelegate; unsigned char challenged; NSFileHandle *sock; NSTimeInterval cacheAge; NSString *urlKey; NSURL *url; NSURL *u; NSURL *proxyURL; NSMutableData *dat; GSMimeParser *parser; GSMimeDocument *document; NSMutableDictionary *pageInfo; NSMapTable *wProperties; NSData *wData; NSMutableDictionary *request; unsigned int bodyPos; unsigned int redirects; enum { idle, connecting, writing, reading, } connectionState; @public NSString *in; NSString *out; } + (void) setMaxCached: (NSUInteger)limit; - (void) _tryLoadInBackground: (NSURL*)fromURL; - (id) setDebugLogDelegate: (id)d; @end /** *

* This is a PRIVATE subclass of NSURLHandle. * It is documented here in order to give you information about the * default behavior of an NSURLHandle created to deal with a URL * that has either the http or https scheme. * The name and/or other implementation details of this class * may be changed at any time. *

*

* A GSHTTPURLHandle instance is used to manage connections to * http and https URLs. * Secure connections are handled automatically * (using openSSL) for URLs with the scheme https. * Connection via proxy server is supported, as is proxy tunneling * for secure connections. Basic parsing of http * headers is performed to extract http status * information, cookies etc. Cookies are * retained and automatically sent during subsequent requests where * the cookie is valid. *

*

* Header information from the current page may be obtained using * -propertyForKey and -propertyForKeyIfAvailable. HTTP * status information can be retrieved as by calling either of these * methods specifying one of the following keys: *

* * * NSHTTPPropertyStatusCodeKey - numeric status code * * * NSHTTPPropertyStatusReasonKey - text describing status * * * NSHTTPPropertyServerHTTPVersionKey - http * version supported by remote server * * *

* According to MacOS-X headers, the following should also * be supported, but currently are not: *

* * NSHTTPPropertyRedirectionHeadersKey * NSHTTPPropertyErrorPageDataKey * *

* The omission of these headers is not viewed as important at * present, since the MacOS-X public beta implementation doesn't * work either. *

*

* Other calls to -propertyForKey and -propertyForKeyIfAvailable may * be made specifying a http header field name. * For example specifying a key name of "Content-Length" * would return the value of the "Content-Length" header * field. *

*

* [GSHTTPURLHandle-writeProperty:forKey:] * can be used to specify the parameters * for the http request. The default request uses the * "GET" method when fetching a page, and the * "POST" method when using -writeData:. * This can be over-ridden by calling -writeProperty:forKey: with * the key name "GSHTTPPropertyMethodKey" and specifying an * alternative method (i.e "PUT"). *

*

* A Proxy may be specified by calling -writeProperty:forKey: to set a * URL as the value for either https_proxy or http_proxy.
* For backward compatibility a proxy may also be specified by calling * -writeProperty:forKey: * with the keys "GSHTTPPropertyProxyHostKey" and * "GSHTTPPropertyProxyPortKey" to set the host and port * of the proxy server respectively.
* The proxy property can specify either the IP address or the hostname of * the proxy server. If an attempt is made to load a page via a * secure connection when a proxy is specified, GSHTTPURLHandle will * attempt to open an SSL Tunnel through the proxy. *

*

* Requests to the remote server may be forced to be bound to a * particular local IP address by using the key * "GSHTTPPropertyLocalHostKey" which must contain the * IP address of a network interface on the local host. *

*/ @implementation GSHTTPURLHandle static NSMutableDictionary *urlCache = nil; static NSMutableArray *urlOrder = nil; static NSLock *urlLock = nil; static NSUInteger maxCached = 16; static Class sslClass = 0; static void debugRead(GSHTTPURLHandle *handle, NSData *data) { int len = (int)[data length]; const uint8_t *ptr = (const uint8_t*)[data bytes]; uint8_t *hex; NSUInteger hl; int pos; hl = ((len + 2) / 3) * 4; hex = malloc(hl + 1); hex[hl] = '\0'; GSPrivateEncodeBase64(ptr, (NSUInteger)len, hex); for (pos = 0; pos < len; pos++) { if (0 == ptr[pos]) { char *esc = [data escapedRepresentation: 0]; NSLog(@"Read for %p %@ of %d bytes (escaped) - '%s'\n<[%s]>", handle, handle->in, len, esc, hex); free(esc); free(hex); return; } } NSLog(@"Read for %p %@ of %d bytes - '%*.*s'\n<[%s]>", handle, handle->in, len, len, len, ptr, hex); free(hex); } static void debugWrite(GSHTTPURLHandle *handle, NSData *data) { int len = (int)[data length]; const uint8_t *ptr = (const uint8_t*)[data bytes]; uint8_t *hex; NSUInteger hl; int pos; hl = ((len + 2) / 3) * 4; hex = malloc(hl + 1); hex[hl] = '\0'; GSPrivateEncodeBase64(ptr, (NSUInteger)len, hex); for (pos = 0; pos < len; pos++) { if (0 == ptr[pos]) { char *esc = [data escapedRepresentation: 0]; NSLog(@"Write for %p %@ of %d bytes (escaped) - '%s'\n<[%s]>", handle, handle->out, len, esc, hex); free(esc); free(hex); return; } } NSLog(@"Write for %p %@ of %d bytes - '%*.*s'\n<[%s]>", handle, handle->out, len, len, len, ptr, hex); free(hex); } + (NSURLHandle*) cachedHandleForURL: (NSURL*)newUrl { NSURLHandle *obj = nil; NSString *s = [newUrl scheme]; if ([s caseInsensitiveCompare: @"http"] == NSOrderedSame || [s caseInsensitiveCompare: @"https"] == NSOrderedSame) { NSString *k = [newUrl cacheKey]; //NSLog(@"Lookup for handle for '%@'", newUrl); [urlLock lock]; obj = RETAIN([urlCache objectForKey: k]); if (obj != nil) { ASSIGN(((GSHTTPURLHandle*)obj)->url, newUrl); [urlOrder removeObjectIdenticalTo: obj]; [urlOrder addObject: obj]; } [urlLock unlock]; //NSLog(@"Found handle %@", obj); } return AUTORELEASE(obj); } + (void) initialize { if (self == [GSHTTPURLHandle class]) { urlCache = [NSMutableDictionary new]; [[NSObject leakAt: &urlCache] release]; urlOrder = [NSMutableArray new]; [[NSObject leakAt: &urlOrder] release]; urlLock = [NSLock new]; [[NSObject leakAt: &urlLock] release]; sslClass = [NSFileHandle sslClass]; } } + (void) setMaxCached: (NSUInteger)limit { maxCached = limit; } - (void) _disconnect { if (sock) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: nil object: sock]; [sock closeFile]; DESTROY(sock); } DESTROY(in); DESTROY(out); connectionState = idle; } - (void) dealloc { [self _disconnect]; DESTROY(out); DESTROY(in); DESTROY(u); DESTROY(urlKey); DESTROY(url); DESTROY(proxyURL); DESTROY(dat); DESTROY(parser); DESTROY(document); DESTROY(pageInfo); DESTROY(wData); if (wProperties != 0) { NSFreeMapTable(wProperties); } DESTROY(request); [super dealloc]; } - (id) initWithURL: (NSURL*)newUrl cached: (BOOL)cached { if ((self = [super initWithURL: newUrl cached: cached]) != nil) { debug = GSDebugSet(@"NSURLHandle"); dat = [NSMutableData new]; pageInfo = [NSMutableDictionary new]; wProperties = NSCreateMapTable(writeKeyCallBacks, NSObjectMapValueCallBacks, 8); request = [NSMutableDictionary new]; ASSIGN(url, newUrl); ASSIGN(urlKey, [newUrl cacheKey]); connectionState = idle; if (cached == YES) { GSHTTPURLHandle *obj; [urlLock lock]; obj = [urlCache objectForKey: urlKey]; [urlCache setObject: self forKey: urlKey]; if (obj != nil) { [urlOrder removeObjectIdenticalTo: obj]; } [urlOrder addObject: self]; while ([urlOrder count] > maxCached) { obj = [urlOrder objectAtIndex: 0]; obj->cacheAge = 0.0; // Not to be re-cached [urlCache removeObjectForKey: obj->urlKey]; [urlOrder removeObjectAtIndex: 0]; } [urlLock unlock]; //NSLog(@"Cache handle %p for '%@'", self, newUrl); } } return self; } + (BOOL) canInitWithURL: (NSURL*)newUrl { NSString *scheme = [newUrl scheme]; if ([scheme isEqualToString: @"http"] || [scheme isEqualToString: @"https"]) { return YES; } return NO; } - (void) bgdApply: (NSString*)basic { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSMutableString *s; NSString *key; NSString *val; NSMutableData *buf; NSMutableData *masked = nil; NSString *version; NSMapEnumerator enumerator; RETAIN(self); if (debug) { NSLog(@"%@ %p %@ %s", NSStringFromSelector(_cmd), self, out, (keepalive ? "re-used connection" : "initial connection")); } s = [basic mutableCopy]; if ([[u query] length] > 0) { [s appendFormat: @"?%@", [u query]]; } version = [request objectForKey: NSHTTPPropertyServerHTTPVersionKey]; if (version == nil) { version = httpVersion; } [s appendFormat: @" HTTP/%@\r\n", version]; if ((id)NSMapGet(wProperties, (void*)@"Host") == nil) { NSString *s = [u scheme]; id p = [u port]; id h = [u host]; if (h == nil) { h = @""; // Must use an empty host header } if (([s isEqualToString: @"http"] && [p intValue] == 80) || ([s isEqualToString: @"https"] && [p intValue] == 443)) { /* Some buggy systems object to the port being in the Host * header when it's the default (optional) value. To keep * them happy let's omit it in those cases. */ p = nil; } if (nil == p) { NSMapInsert(wProperties, (void*)@"Host", (void*)h); } else { NSMapInsert(wProperties, (void*)@"Host", (void*)[NSString stringWithFormat: @"%@:%@", h, p]); } } /* Ensure we set the correct content length (may be zero) */ if ((id)NSMapGet(wProperties, (void*)@"Content-Length") == nil) { NSMapInsert(wProperties, (void*)@"Content-Length", (void*)[NSString stringWithFormat: @"%"PRIuPTR, [wData length]]); } if ([wData length] > 0) { /* * Assume content type if not specified. */ if ((id)NSMapGet(wProperties, (void*)@"Content-Type") == nil) { NSMapInsert(wProperties, (void*)@"Content-Type", (void*)@"application/x-www-form-urlencoded"); } } if ((id)NSMapGet(wProperties, (void*)@"Authorization") == nil) { NSURLProtectionSpace *space; /* * If we have username/password stored in the URL, and there is a * known protection space for that URL, we generate an authentication * header. */ if ([u user] != nil && (space = [GSHTTPAuthentication protectionSpaceForURL: u]) != nil) { NSString *auth; GSHTTPAuthentication *authentication; NSURLCredential *cred; NSString *method; /* Create credential from user and password stored in the URL. * Returns nil if we have no username or password. */ cred = [[NSURLCredential alloc] initWithUser: [u user] password: [u password] persistence: NSURLCredentialPersistenceForSession]; if (cred == nil) { authentication = nil; } else { /* Create authentication from credential ... returns nil if * we have no credential. */ authentication = [GSHTTPAuthentication authenticationWithCredential: cred inProtectionSpace: space]; RELEASE(cred); } method = [request objectForKey: GSHTTPPropertyMethodKey]; if (method == nil) { if ([wData length] > 0) { method = @"POST"; } else { method = @"GET"; } } auth = [authentication authorizationForAuthentication: nil method: method path: [u fullPath]]; /* If authentication is nil then auth will also be nil */ if (auth != nil) { [self writeProperty: auth forKey: @"Authorization"]; } } } buf = [[s dataUsingEncoding: NSISOLatin1StringEncoding] mutableCopy]; enumerator = NSEnumerateMapTable(wProperties); while (NSNextMapEnumeratorPair(&enumerator, (void **)(&key), (void**)&val)) { GSMimeHeader *h; h = [[GSMimeHeader alloc] initWithName: key value: val parameters: nil]; if (debug || masked) { [h addToBuffer: buf masking: &masked]; } else { [h addToBuffer: buf masking: NULL]; } RELEASE(h); } NSEndMapTableEnumeration(&enumerator); [buf appendBytes: "\r\n" length: 2]; if (masked) { [masked appendBytes: "\r\n" length: 2]; } /* * Append any data to be sent */ if (wData != nil) { [buf appendData: wData]; if (masked) { [masked appendData: wData]; } } /* * Watch for write completion. */ [nc addObserver: self selector: @selector(bgdWrite:) name: GSFileHandleWriteCompletionNotification object: sock]; connectionState = writing; /* * Send request to server. */ if (debug) { if (nil == masked) { masked = buf; // Just log unmasked data } if (NO == [ioDelegate putBytes: [masked bytes] ofLength: [masked length] byHandle: self]) { debugWrite(self, masked); } } [sock writeInBackgroundAndNotify: buf]; RELEASE(buf); RELEASE(s); DESTROY(self); } - (void) bgdRead: (NSNotification*) not { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSDictionary *dict = [not userInfo]; NSData *d; NSRange r; unsigned readCount; RETAIN(self); if (debug) NSLog(@"%@ %p %s", NSStringFromSelector(_cmd), self, keepalive?"K":""); d = [dict objectForKey: NSFileHandleNotificationDataItem]; readCount = [d length]; if (debug) { if (NO == [ioDelegate getBytes: [d bytes] ofLength: readCount byHandle: self]) { debugRead(self, d); } } if (connectionState == idle) { /* * We received an event on a handle which is not in use ... * it should just be the connection being closed by the other * end because of a timeout etc. */ if (debug) { NSUInteger length = [d length]; if (length > 0) { if (nil == ioDelegate) { NSLog(@"%@ %p %s Unexpected data (%*.*s) from remote!", NSStringFromSelector(_cmd), self, keepalive?"K":"", (int)[d length], (int)[d length], (char*)[d bytes]); } else { NSLog(@"%@ %p %s Unexpected data from remote!", NSStringFromSelector(_cmd), self, keepalive?"K":""); if (NO == [ioDelegate getBytes: [d bytes] ofLength: length byHandle: self]) { NSLog(@"%@ %p %s (%*.*s)", NSStringFromSelector(_cmd), self, keepalive?"K":"", (int)[d length], (int)[d length], (char*)[d bytes]); } } } } [self _disconnect]; } else if (0 == readCount && NO == inResponse && YES == keepalive) { /* On a keepalive connection where the remote end * dropped the connection without responding. We * should try again. */ if (connectionState != idle) { [self _disconnect]; if (debug) { NSLog(@"%@ %p restart on new connection", NSStringFromSelector(_cmd), self); } [self _tryLoadInBackground: u]; } } else if ([parser parse: d] == NO && [parser isComplete] == NO) { inResponse = YES; if (debug) { NSLog(@"HTTP parse failure - %@", parser); } [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: @"Response parse failed"]; } else { BOOL complete; inResponse = YES; complete = [parser isComplete]; if (complete == NO && [parser isInHeaders] == NO) { GSMimeHeader *info; NSString *enc; NSString *len; int status; info = [document headerNamed: @"http"]; status = [[info objectForKey: NSHTTPPropertyStatusCodeKey] intValue]; len = [[document headerNamed: @"content-length"] value]; enc = [[document headerNamed: @"content-transfer-encoding"] value]; if (enc == nil) { enc = [[document headerNamed: @"transfer-encoding"] value]; } if (status == 204 || status == 304) { complete = YES; // No body expected. } else if ([enc isEqualToString: @"chunked"] == YES) { complete = NO; // Read chunked body data } else if (nil != len && [len intValue] == 0) { complete = YES; // content-length explicitly zero } if (complete == NO && [d length] == 0) { complete = YES; // Had EOF ... terminate } } if (complete == YES) { GSMimeHeader *info; NSString *val; NSNumber *num; float ver; int code; connectionState = idle; [nc removeObserver: self name: nil object: sock]; ver = [[[document headerNamed: @"http"] value] floatValue]; if (ver < 1.1) { [self _disconnect]; } else if (nil != (val = [[document headerNamed: @"connection"] value])) { val = [val lowercaseString]; if (YES == [val isEqualToString: @"close"]) { [self _disconnect]; } else if ([val length] > 5) { NSEnumerator *e; e = [[val componentsSeparatedByString: @","] objectEnumerator]; while (nil != (val = [e nextObject])) { val = [val stringByTrimmingSpaces]; if (YES == [val isEqualToString: @"close"]) { [self _disconnect]; break; } } } } /* * Retrieve essential keys from document */ info = [document headerNamed: @"http"]; num = [info objectForKey: NSHTTPPropertyStatusCodeKey]; code = [num intValue]; if (code == 401 && self->challenged < 2) { GSMimeHeader *ah; self->challenged++; // Prevent repeated challenge/auth if ((ah = [document headerNamed: @"WWW-Authenticate"]) != nil) { NSURLProtectionSpace *space; NSString *ac; GSHTTPAuthentication *authentication; NSString *method; NSString *auth; ac = [ah value]; space = [GSHTTPAuthentication protectionSpaceForAuthentication: ac requestURL: url]; if (space == nil) { authentication = nil; } else { NSURLCredential *cred; /* * Create credential from user and password * stored in the URL. * Returns nil if we have no username or password. */ cred = [[NSURLCredential alloc] initWithUser: [url user] password: [url password] persistence: NSURLCredentialPersistenceForSession]; if (cred == nil) { authentication = nil; } else { /* * Get the digest object and ask it for a header * to use for authorisation. * Returns nil if we have no credential. */ authentication = [GSHTTPAuthentication authenticationWithCredential: cred inProtectionSpace: space]; RELEASE(cred); } } method = [request objectForKey: GSHTTPPropertyMethodKey]; if (method == nil) { if ([wData length] > 0) { method = @"POST"; } else { method = @"GET"; } } auth = [authentication authorizationForAuthentication: ac method: method path: [url fullPath]]; if (auth != nil) { [self writeProperty: auth forKey: @"Authorization"]; [self _tryLoadInBackground: u]; RELEASE(self); return; // Retrying. } } } if (num != nil) { [pageInfo setObject: num forKey: NSHTTPPropertyStatusCodeKey]; } val = [info objectForKey: NSHTTPPropertyServerHTTPVersionKey]; if (val != nil) { [pageInfo setObject: val forKey: NSHTTPPropertyServerHTTPVersionKey]; } val = [info objectForKey: NSHTTPPropertyStatusReasonKey]; if (val != nil) { [pageInfo setObject: val forKey: NSHTTPPropertyStatusReasonKey]; } /* * Tell superclass that we have successfully loaded the data. */ d = [parser data]; r = NSMakeRange(bodyPos, [d length] - bodyPos); bodyPos = 0; DESTROY(wData); NSResetMapTable(wProperties); connectionState = idle; // Finished I/O if (returnAll || (code >= 200 && code < 300)) { [self didLoadBytes: [d subdataWithRange: r] loadComplete: YES]; } else { [self didLoadBytes: [d subdataWithRange: r] loadComplete: NO]; [self cancelLoadInBackground]; } } else { /* * Report partial data if possible. */ if ([parser isInBody]) { d = [parser data]; r = NSMakeRange(bodyPos, [d length] - bodyPos); bodyPos = [d length]; [self didLoadBytes: [d subdataWithRange: r] loadComplete: NO]; } } if (complete == NO && readCount == 0) { /* The read failed ... dropped, but parsing is not complete. * The request was sent, so we can't know whether it was * lost in the network or the remote end received it and * the response was lost. */ if (debug) { NSLog(@"HTTP response not received - %@", parser); } [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: @"Response parse failed"]; } if (sock != nil && connectionState == reading) { if ([sock readInProgress] == NO) { [sock readInBackgroundAndNotify]; } } } DESTROY(self); } - (void) bgdTunnelRead: (NSNotification*) not { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSDictionary *dict = [not userInfo]; NSData *d; GSMimeParser *p; unsigned readCount; RETAIN(self); if (debug) { NSLog(@"%@ %p %s", NSStringFromSelector(_cmd), self, keepalive?"K":""); } d = [dict objectForKey: NSFileHandleNotificationDataItem]; readCount = [d length]; if (debug) { if (NO == [ioDelegate getBytes: [d bytes] ofLength: [d length] byHandle: self]) { debugRead(self, d); } } if (readCount > 0) { inResponse = YES; [dat appendData: d]; } else if (NO == inResponse) { /* remote end dropped the connection without responding */ [self _disconnect]; if (debug) { NSLog(@"%@ %p restart on new connection", NSStringFromSelector(_cmd), self); } [self _tryLoadInBackground: u]; DESTROY(self); return; } p = [GSMimeParser new]; [p parse: dat]; if ([p isInBody] == YES || [d length] == 0) { GSMimeHeader *info; NSString *val; NSNumber *num; [p parse: nil]; info = [[p mimeDocument] headerNamed: @"http"]; val = [info objectForKey: NSHTTPPropertyServerHTTPVersionKey]; if (val != nil) [pageInfo setObject: val forKey: NSHTTPPropertyServerHTTPVersionKey]; num = [info objectForKey: NSHTTPPropertyStatusCodeKey]; if (num != nil) [pageInfo setObject: num forKey: NSHTTPPropertyStatusCodeKey]; val = [info objectForKey: NSHTTPPropertyStatusReasonKey]; if (val != nil) [pageInfo setObject: val forKey: NSHTTPPropertyStatusReasonKey]; [nc removeObserver: self name: NSFileHandleReadCompletionNotification object: sock]; [dat setLength: 0]; tunnel = NO; } else { if ([sock readInProgress] == NO) { [sock readInBackgroundAndNotify]; } } RELEASE(p); DESTROY(self); } - (void) loadInBackground { self->challenged = 0; [self _tryLoadInBackground: nil]; } - (void) endLoadInBackground { DESTROY(wData); NSResetMapTable(wProperties); /* Socket must be removed from I/O notifications and connection state * marked idle, but the socket is already idle it may be re-used for * another request. */ if (sock) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: nil object: sock]; if (connectionState != idle) { [sock closeFile]; // Close to cancel any I/O in progress DESTROY(sock); } } connectionState = idle; [super endLoadInBackground]; } - (void) _apply { NSString *method; NSString *path; NSString *s; /* * Set up request - differs for proxy version unless tunneling via ssl. */ path = [[[u fullPath] stringByTrimmingSpaces] stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; if ([path length] == 0) { path = @"/"; } method = [request objectForKey: GSHTTPPropertyMethodKey]; if (method == nil) { if ([wData length] > 0) { method = @"POST"; } else { method = @"GET"; } } if (proxyURL && [[u scheme] isEqualToString: @"https"] == NO) { if ([u port] == nil) { s = [[NSString alloc] initWithFormat: @"%@ http://%@%@", method, [u host], path]; } else { s = [[NSString alloc] initWithFormat: @"%@ http://%@:%@%@", method, [u host], [u port], path]; } } else // no proxy { s = [[NSString alloc] initWithFormat: @"%@ %@", method, path]; } [self bgdApply: s]; RELEASE(s); } - (void) bgdConnect: (NSNotification*)notification { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSDictionary *userInfo = [notification userInfo]; NSString *e; [nc removeObserver: self name: GSFileHandleConnectCompletionNotification object: sock]; /* * See if the connection attempt caused an error. */ e = [userInfo objectForKey: GSFileHandleNotificationError]; if (e != nil) { NSLog(@"Unable to connect to %@:%@ via socket ... %@", [sock socketAddress], [sock socketService], e); /* * Tell superclass that the load failed - let it do housekeeping. */ [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: [NSString stringWithFormat: @"Failed to connect: %@", e]]; return; } in = [[NSString alloc] initWithFormat: @"(%@:%@ <-- %@:%@)", [sock socketLocalAddress], [sock socketLocalService], [sock socketAddress], [sock socketService]]; out = [[NSString alloc] initWithFormat: @"(%@:%@ --> %@:%@)", [sock socketLocalAddress], [sock socketLocalService], [sock socketAddress], [sock socketService]]; if (debug) { NSLog(@"%@ %p", NSStringFromSelector(_cmd), self); } /* * If SSL via proxy, set up tunnel first */ if (proxyURL && [[u scheme] isEqualToString: @"https"]) { NSRunLoop *loop = [NSRunLoop currentRunLoop]; NSString *cmd; NSTimeInterval last = 0.0; NSTimeInterval limit = 0.01; NSData *buf; NSDate *when; int status; NSString *version; version = [request objectForKey: NSHTTPPropertyServerHTTPVersionKey]; if (version == nil) { version = httpVersion; } if ([u port] == nil) { cmd = [NSString stringWithFormat: @"CONNECT %@:443 HTTP/%@\r\n\r\n", [u host], version]; } else { cmd = [NSString stringWithFormat: @"CONNECT %@:%@ HTTP/%@\r\n\r\n", [u host], [u port], version]; } /* * Set up default status for if connection is lost. */ [pageInfo setObject: @"1.0" forKey: NSHTTPPropertyServerHTTPVersionKey]; [pageInfo setObject: [NSNumber numberWithInt: 503] forKey: NSHTTPPropertyStatusCodeKey]; [pageInfo setObject: @"Connection dropped by proxy server" forKey: NSHTTPPropertyStatusReasonKey]; tunnel = YES; [nc addObserver: self selector: @selector(bgdWrite:) name: GSFileHandleWriteCompletionNotification object: sock]; buf = [cmd dataUsingEncoding: NSASCIIStringEncoding]; if (debug) { if (NO == [ioDelegate putBytes: [buf bytes] ofLength: [buf length] byHandle: self]) { debugWrite(self, buf); } } [sock writeInBackgroundAndNotify: buf]; when = [NSDate alloc]; while (tunnel == YES) { if (limit < 1.0) { NSTimeInterval tmp = limit; limit += last; last = tmp; } when = [when initWithTimeIntervalSinceNow: limit]; [loop runUntilDate: when]; } RELEASE(when); status = [[pageInfo objectForKey: NSHTTPPropertyStatusCodeKey] intValue]; if (status != 200) { [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: @"Failed proxy tunneling"]; return; } } if ([[u scheme] isEqualToString: @"https"]) { static NSArray *keys = nil; NSMutableDictionary *opts; NSUInteger count; BOOL success = NO; /* If we are an https connection, negotiate secure connection. * Make sure we are not an observer of the file handle while * it is connecting... */ [nc removeObserver: self name: nil object: sock]; if (nil == keys) { keys = [[NSArray alloc] initWithObjects: GSTLSCAFile, GSTLSCertificateFile, GSTLSCertificateKeyFile, GSTLSCertificateKeyPassword, GSTLSDebug, GSTLSIssuers, GSTLSOwners, GSTLSPriority, GSTLSRemoteHosts, GSTLSRevokeFile, GSTLSServerName, GSTLSVerify, nil]; } count = [keys count]; opts = [[NSMutableDictionary alloc] initWithCapacity: count]; while (count-- > 0) { NSString *key = [keys objectAtIndex: count]; NSString *str = [request objectForKey: key]; if (nil != str) { [opts setObject: str forKey: key]; } } /* If there is no value set for the server name, and the host in the * URL is a domain name rather than an address, we use that. */ if (nil == [opts objectForKey: GSTLSServerName]) { NSString *host = [u host]; unichar c = [host length] == 0 ? 0 : [host characterAtIndex: 0]; if (c != 0 && c != ':' && !isdigit(c)) { [opts setObject: host forKey: GSTLSServerName]; } } if (debug) [opts setObject: @"YES" forKey: GSTLSDebug]; [sock sslSetOptions: opts]; RELEASE(opts); if ([sock sslHandshakeEstablished: &success outgoing: YES]) { if (NO == success) { if (debug) NSLog(@"%@ %p %s Failed to make ssl connect", NSStringFromSelector(_cmd), self, keepalive?"K":""); [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: @"Failed to make ssl connect"]; return; } } else { [nc addObserver: self selector: @selector(bgdHandshake:) name: NSFileHandleDataAvailableNotification object: sock]; [sock waitForDataInBackgroundAndNotify]; return; } } [self _apply]; } - (void) bgdHandshake: (NSNotification*)notification { BOOL success = NO; if ([sock sslHandshakeEstablished: &success outgoing: YES]) { if (success) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: NSFileHandleDataAvailableNotification object: sock]; [self _apply]; } else { if (debug) NSLog(@"%@ %p %s Failed to make ssl connect", NSStringFromSelector(_cmd), self, keepalive?"K":""); [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: @"Failed to make ssl connect"]; } } else { [sock waitForDataInBackgroundAndNotify]; } } - (void) bgdWrite: (NSNotification*)notification { NSNotificationCenter *nc; NSDictionary *userInfo = [notification userInfo]; NSString *e; RETAIN(self); if (debug) NSLog(@"%@ %p %s", NSStringFromSelector(_cmd), self, keepalive?"K":""); e = [userInfo objectForKey: GSFileHandleNotificationError]; if (e != nil) { tunnel = NO; if (keepalive == YES) { /* * The write failed ... connection dropped ... and we * are re-using an existing connection (keepalive = YES) * then we may try again with a new connection. */ [self _disconnect]; if (debug) { NSLog(@"%@ %p restart on new connection", NSStringFromSelector(_cmd), self); } [self _tryLoadInBackground: u]; RELEASE(self); return; } NSLog(@"Failed to write command to socket - %@ %p %s", e, self, keepalive?"K":""); /* * Tell superclass that the load failed - let it do housekeeping. */ [self endLoadInBackground]; [self backgroundLoadDidFailWithReason: [NSString stringWithFormat: @"Failed to write request: %@", e]]; DESTROY(self); return; } else { /* * Don't watch for write completions any more. */ nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: GSFileHandleWriteCompletionNotification object: sock]; /* * Ok - write completed, let's read the response. */ if (tunnel == YES) { [nc addObserver: self selector: @selector(bgdTunnelRead:) name: NSFileHandleReadCompletionNotification object: sock]; } else { bodyPos = 0; [nc addObserver: self selector: @selector(bgdRead:) name: NSFileHandleReadCompletionNotification object: sock]; } if ([sock readInProgress] == NO) { [sock readInBackgroundAndNotify]; } connectionState = reading; } DESTROY(self); } /** * If necessary, this method calls -loadInForeground to send a * request to the webserver, and get a page back. It then returns * the property for the specified key - * * * NSHTTPPropertyStatusCodeKey - numeric status code returned * by the last request. * * * NSHTTPPropertyStatusReasonKey - text describing status of * the last request * * * NSHTTPPropertyServerHTTPVersionKey - http * version supported by remote server * * * Other keys are taken to be the names of http * headers and the corresponding header value (or nil if there * is none) is returned. * * */ - (id) propertyForKey: (NSString*) propertyKey { if (document == nil) [self loadInForeground]; return [self propertyForKeyIfAvailable: propertyKey]; } - (id) propertyForKeyIfAvailable: (NSString*) propertyKey { id result = [pageInfo objectForKey: propertyKey]; if (result == nil) { NSString *key = [propertyKey lowercaseString]; NSArray *array = [document headersNamed: key]; if ([array count] == 0) { return nil; } else if ([array count] == 1) { GSMimeHeader *hdr = [array objectAtIndex: 0]; result = [hdr value]; } else { NSEnumerator *enumerator = [array objectEnumerator]; GSMimeHeader *val; result = [NSMutableArray arrayWithCapacity: [array count]]; while ((val = [enumerator nextObject]) != nil) { [result addObject: [val value]]; } } } return result; } - (int) setDebug: (int)flag { int old = debug; debug = flag ? YES : NO; return old; } - (id) setDebugLogDelegate: (id)d { id old = ioDelegate; NSAssert(nil == d || [d conformsToProtocol: @protocol(GSLogDelegate)], NSInvalidArgumentException); ioDelegate = d; return old; } - (void) setReturnAll: (BOOL)flag { returnAll = flag; } - (void) setURL: (NSURL*)newUrl { NSAssert(connectionState == idle, NSInternalInconsistencyException); NSAssert([newUrl isKindOfClass: [NSURL class]], NSInvalidArgumentException); if (NO == [newUrl isEqual: url]) { NSString *k = [newUrl cacheKey]; if (NO == [k isEqual: urlKey]) { /* Changing the URL of a handle to one that's not cache-compatible * implies that the handle must be removed from the cache and also * that the underlying network connection can no longer be used. */ [urlLock lock]; if (self == [urlCache objectForKey: urlKey]) { [urlCache removeObjectForKey: urlKey]; [urlOrder removeObjectIdenticalTo: self]; } [urlLock unlock]; [self _disconnect]; ASSIGN(urlKey, k); } ASSIGN(url, newUrl); } } - (void) _tryLoadInBackground: (NSURL*)fromURL { NSNotificationCenter *nc; NSString *host = nil; NSString *port = nil; NSString *s; /* * Don't start a load if one is in progress. */ if (connectionState != idle) { NSLog(@"Attempt to load an http handle which is not idle ... ignored"); return; } inResponse = NO; [dat setLength: 0]; RELEASE(document); RELEASE(parser); [pageInfo removeAllObjects]; parser = [GSMimeParser new]; document = RETAIN([parser mimeDocument]); /* * First time round, fromURL is nil, so we use the url ivar and * we notify that the load is begining. On retries we get a real * value in fromURL to use. */ if (fromURL == nil) { redirects = 0; ASSIGN(u, url); [self beginLoadInBackground]; } else { ASSIGN(u, fromURL); } host = [u host]; port = (id)[u port]; if (port != nil) { port = [NSString stringWithFormat: @"%u", [port intValue]]; } else { port = [u scheme]; } if ([port isEqualToString: @"https"]) { port = @"443"; } else if ([port isEqualToString: @"http"]) { port = @"80"; } /* An existing socket with keepalive may have been closed by the other * end. * On unix systems we can simply peek on the file descriptor for a much * more efficient check. * On windows we use the same system, it is noted to be inefficient but * we don't care because we peek rare enough at each HTTP request. */ if (sock != nil) { int fd = [sock fileDescriptor]; if (debug) { NSLog(@"%@ %p check for reusable socket", NSStringFromSelector(_cmd), self); } if (fd >= 0) { int result; unsigned char c; #if !defined(MSG_DONTWAIT) #define MSG_DONTWAIT 0 #endif result = recv(fd, (void *)&c, 1, MSG_PEEK | MSG_DONTWAIT); if (result == 0 || (result < 0 && errno != EAGAIN && errno != EINTR)) { DESTROY(sock); } } else { DESTROY(sock); } if (debug) { if (sock == nil) { NSLog(@"%@ %p socket closed by remote", NSStringFromSelector(_cmd), self); } else { NSLog(@"%@ %p socket is still open", NSStringFromSelector(_cmd), self); } } } if (sock == nil) { NSURLProtectionSpace *space; NSURL *proxy = nil; NSString *proxyStr = nil; keepalive = NO; // New connection /* * If we have a local address specified, * tell the file handle to bind to it. */ s = [request objectForKey: GSHTTPPropertyLocalHostKey]; if ([s length] > 0) { s = [NSString stringWithFormat: @"bind-%@", s]; } else { s = @"tcp"; // Bind to any. } space = [GSHTTPAuthentication protectionSpaceForURL: u]; if ([space isProxy]) { proxyStr = [NSString stringWithFormat: @"%@://%@:%u/", [u scheme], [space host], (unsigned)[space port]]; } else { NSString *ph; NSString *pp; ph = [request objectForKey: GSHTTPPropertyProxyHostKey]; pp = [request objectForKey: GSHTTPPropertyProxyPortKey]; if (ph) { if (pp) { proxyStr = [NSString stringWithFormat: @"%@://%@:%@/", [u scheme], ph, pp]; } else { proxyStr = [NSString stringWithFormat: @"%@://%@/", [u scheme], ph]; } } /* The preferred proxy specification is by a URL set as a property */ if ([[u scheme] isEqualToString: @"https"]) { proxy = [request objectForKey: @"https_proxy"]; } else { proxy = [request objectForKey: @"http_proxy"]; } } if ([proxy isKindOfClass: [NSString class]]) { proxyStr = (NSString*)proxy; proxy = nil; } /* A generic fallback for the entire process can come from * environment variables. */ if (nil == proxy && nil == proxyStr) { NSDictionary *env; NSString *key; env = [[NSProcessInfo processInfo] environment]; key = [[u scheme] stringByAppendingString: @"_proxy"]; if (nil == (proxyStr = [env objectForKey: key])) { proxyStr = [env objectForKey: [key uppercaseString]]; } } if (nil == proxy) { /* We make the proxy URL from a supplied string unless that is empty; * An empty string in the request can be used to disable the process * wide settings for that request.. */ if ([proxyStr length]) { proxy = [NSURL URLWithString: proxyStr]; } } /* Make sure the proxy URL has a port specified. The default port * depends on the scheme of the request (4430 for TLS, 8080 unencrypted). */ if (proxy && [[proxy port] intValue] == 0) { NSURLComponents *c; c = [NSURLComponents componentsWithURL: proxy resolvingAgainstBaseURL: NO]; if ([[u scheme] isEqualToString: @"https"]) { [c setPort: [NSNumber numberWithInteger: 4430]]; } else { [c setPort: [NSNumber numberWithInteger: 8080]]; } proxy = [c URL]; } ASSIGN(proxyURL, proxy); if (nil == proxyURL) { if ([[u scheme] isEqualToString: @"https"]) { NSString *cert; NSString *key; NSString *pwd; if (sslClass == 0) { [self backgroundLoadDidFailWithReason: @"https not supported" @" ... needs gnustep-base built with GNUTLS"]; return; } sock = [sslClass fileHandleAsClientInBackgroundAtAddress: host service: port protocol: s]; /* Map old SSL keys onto new. */ cert = [request objectForKey: GSHTTPPropertyCertificateFileKey]; if (nil != cert) { [request setObject: cert forKey: GSTLSCertificateFile]; } key = [request objectForKey: GSHTTPPropertyKeyFileKey]; if (nil != key) { [request setObject: key forKey: GSTLSCertificateKeyFile]; } pwd = [request objectForKey: GSHTTPPropertyPasswordKey]; if (nil != pwd) { [request setObject: pwd forKey: GSTLSCertificateKeyPassword]; } } else { sock = [NSFileHandle fileHandleAsClientInBackgroundAtAddress: host service: port protocol: s]; } } else { port = [[proxyURL port] description]; host = [proxyURL host]; if ([[u scheme] isEqualToString: @"https"]) { if (sslClass == 0) { [self backgroundLoadDidFailWithReason: @"https not supported" @" ... needs gnustep-base built with GNUTLS"]; return; } sock = [sslClass fileHandleAsClientInBackgroundAtAddress: host service: port protocol: s]; } else { sock = [NSFileHandle fileHandleAsClientInBackgroundAtAddress: host service: port protocol: s]; } } if (sock == nil) { /* * Tell superclass that the load failed - let it do housekeeping. */ [self backgroundLoadDidFailWithReason: [NSString stringWithFormat: @"Unable to connect to %@:%@ ... %@", host, port, [NSError _last]]]; return; } RETAIN(sock); nc = [NSNotificationCenter defaultCenter]; [nc addObserver: self selector: @selector(bgdConnect:) name: GSFileHandleConnectCompletionNotification object: sock]; connectionState = connecting; if (debug) { NSLog(@"%@ %p start connect to %@:%@", NSStringFromSelector(_cmd), self, host, port); } } else { NSString *method; NSString *path; NSString *basic; // Stop waiting for connection to be closed down. nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: NSFileHandleReadCompletionNotification object: sock]; /* Reusing a connection. Set flag to say that it has been kept * alive and we don't know if the other end has dropped it * until we write to it and read some response. */ keepalive = YES; method = [request objectForKey: GSHTTPPropertyMethodKey]; if (method == nil) { if ([wData length] > 0) { method = @"POST"; } else { method = @"GET"; } } path = [[[u fullPath] stringByTrimmingSpaces] stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; if ([path length] == 0) { path = @"/"; } basic = [NSString stringWithFormat: @"%@ %@", method, path]; [self bgdApply: basic]; } } /** * Writes the specified data as the body of an http * or https request to the web server. * Returns YES on success, * NO on failure. By default, this method performs a POST operation. * On completion, the resource data for this handle is set to the * page returned by the request. */ - (BOOL) writeData: (NSData*)d { ASSIGN(wData, d); return YES; } /** * Sets a property to be used in the next request made by this handle. * The property is set as a header in the next request, unless it is * one of the following - * * * GSHTTPPropertyBodyKey - set an NSData item to be sent to * the server as the body of the request. * * * GSHTTPPropertyMethodKey - override the default method of * the request (eg. "PUT"). * * * GSHTTPPropertyProxyHostKey - specify the name or IP address * of a host to proxy through. Obsolete ... use * https_proxy or http_proxy to specify the URL of the proxy * * * GSHTTPPropertyProxyPortKey - specify the port number to * connect to on the proxy host. If not give, this defaults * to 8080 for http and 4430 for https. * Obsolete ... use https_proxy or http_proxy to specify the URL * of the proxy. * * * Any GSTLS... key to control TLS behavior * * * Any NSHTTPProperty... key * * */ - (BOOL) writeProperty: (id) property forKey: (NSString*) propertyKey { if (propertyKey == nil || [propertyKey isKindOfClass: [NSString class]] == NO) { [NSException raise: NSInvalidArgumentException format: @"%@ %p with invalid key", NSStringFromSelector(_cmd), self]; } if ([propertyKey hasPrefix: @"GSHTTPProperty"] || [propertyKey hasPrefix: @"GSTLS"] || [propertyKey hasPrefix: @"NSHTTPProperty"]) { if (property == nil) { [request removeObjectForKey: propertyKey]; } else { [request setObject: property forKey: propertyKey]; } } else { if (property == nil) { NSMapRemove(wProperties, (void*)propertyKey); } else { NSMapInsert(wProperties, (void*)propertyKey, (void*)property); } } return YES; } @end