/* Copyright (C) 2000-2005 SKYRIX Software AG This file is part of SOPE. SOPE 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, or (at your option) any later version. SOPE 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 SOPE; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #import #include "WOSimpleHTTPParser.h" #include "WOHttpAdaptor/WORecordRequestStream.h" @interface WOHTTPConnection(Privates) - (BOOL)_connect; - (void)_disconnect; - (void)_unregisterNotification; @end @interface WOCookie(Privates) + (id)cookieWithString:(NSString *)_string; @end NSString *WOHTTPConnectionCanReadResponse = @"WOHTTPConnectionCanReadResponse"; @interface NSURL(SocketAddress) - (id)socketAddressForURL; - (BOOL)shouldUseWOProxyServer; @end @interface WOHTTPConnection(Privates2) + (NSString *)proxyServer; + (NSURL *)proxyServerURL; + (NSArray *)noProxySuffixes; @end @implementation WOHTTPConnection static Class SSLSocketClass = Nil; static BOOL useSimpleParser = YES; static NSString *proxyServer = nil; static NSArray *noProxy = nil; static BOOL doDebug = NO; static BOOL logStream = NO; + (int)version { return 3; } + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; static BOOL didInit = NO; if (didInit) return; didInit = YES; useSimpleParser = [ud boolForKey:@"WOHTTPConnectionUseSimpleParser"]; proxyServer = [ud stringForKey:@"WOProxyServer"]; noProxy = [ud arrayForKey:@"WONoProxySuffixes"]; doDebug = [ud boolForKey:@"WODebugHTTPConnection"]; logStream = [ud boolForKey:@"WODebugHTTPConnectionLogStream"]; } + (NSString *)proxyServer { return proxyServer; } + (NSURL *)proxyServerURL { NSString *ps; ps = [self proxyServer]; if ([ps length] == 0) return nil; return [NSURL URLWithString:ps]; } + (NSArray *)noProxySuffixes { return noProxy; } - (id)initWithNSURL:(NSURL *)_url { if ((self = [super init])) { self->url = [_url retain]; self->useSSL = [[_url scheme] isEqualToString:@"https"]; self->useProxy = [_url shouldUseWOProxyServer]; if (self->useSSL) { static BOOL didCheck = NO; if (!didCheck) { didCheck = YES; SSLSocketClass = NSClassFromString(@"NGActiveSSLSocket"); } } } return self; } - (id)initWithURL:(id)_url { NSURL *lurl; /* create an NSURL object if necessary */ lurl = [_url isKindOfClass:[NSURL class]] ? _url : [NSURL URLWithString:[_url stringValue]]; if (lurl == nil) { if (doDebug) [self logWithFormat:@"could not construct URL from object '%@' !", _url]; [self release]; return nil; } if (doDebug) [self logWithFormat:@"init with URL: %@", lurl]; return [self initWithNSURL:lurl]; } - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port secure:(BOOL)_flag { NSString *s; s = [NSString stringWithFormat:@"http%s://%@:%i/", _flag ? "s" : "", _hostName, _port == 0 ? (_flag?443:80) : _port]; return [self initWithURL:s]; } - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port { return [self initWithHost:_hostName onPort:_port secure:NO]; } - (id)init { return [self initWithHost:@"localhost" onPort:80 secure:NO]; } - (void)dealloc { [self _unregisterNotification]; [self->lastException release]; [self->log release]; [self->io release]; [self->socket release]; [self->url release]; [super dealloc]; } /* error handling */ - (NSException *)lastException { return self->lastException; } - (BOOL)isDebuggingEnabled { return doDebug ? YES : NO; } - (NSString *)loggingPrefix { /* improve perf ... */ if (self->url) { return [NSString stringWithFormat:@"WOHTTP[0x%p]<%@>", self, [self->url absoluteString]]; } else return [NSString stringWithFormat:@"WOHTTP[0x%p]", self]; } /* accessors */ - (NSString *)hostName { return [self->url host]; } /* IO */ - (BOOL)_connect { id address; [self _disconnect]; #if DEBUG NSAssert(self->socket == nil, @"socket still available after disconnect"); NSAssert(self->io == nil, @"IO stream still available after disconnect"); #endif if (self->useSSL) { if (SSLSocketClass == Nil) { /* no SSL support is available */ static BOOL didLog = NO; if (!didLog) { didLog = YES; NSLog(@"NOTE: SSL support is not available !"); } return NO; } } if (self->useProxy) { NSURL *purl; purl = [[self class] proxyServerURL]; address = [purl socketAddressForURL]; } else { address = [self->url socketAddressForURL]; } if (address == nil) { [self debugWithFormat:@"got no address for connect .."]; return NO; } NS_DURING { self->socket = self->useSSL ? [SSLSocketClass socketConnectedToAddress:address] : [NGActiveSocket socketConnectedToAddress:address]; } NS_HANDLER { #if 0 fprintf(stderr, "couldn't create socket: %s\n", [[localException description] cString]); #endif ASSIGN(self->lastException, localException); self->socket = nil; } NS_ENDHANDLER; if (self->socket == nil) { [self debugWithFormat:@"socket is not setup: %@", [self lastException]]; return NO; } if (![self->socket isConnected]) { self->socket = nil; [self debugWithFormat:@"socket is not connected .."]; return NO; } self->socket = [self->socket retain]; [(NGActiveSocket *)self->socket setSendTimeout:[self sendTimeout]]; [(NGActiveSocket *)self->socket setReceiveTimeout:[self receiveTimeout]]; if (self->socket != nil) { NGBufferedStream *bStr; bStr = [NGBufferedStream alloc]; // keep gcc happy bStr = [bStr initWithSource:self->socket]; if (logStream) { self->log = [WORecordRequestStream alloc]; // keep gcc happy self->log = [(WORecordRequestStream *)self->log initWithSource:bStr]; } else self->log = nil; self->io = [NGCTextStream alloc]; // keep gcc happy self->io = [self->io initWithSource: (id)(self->log != nil ? self->log : (id)bStr)]; [bStr release]; bStr = nil; } return YES; } - (void)_disconnect { [self->log release]; self->log = nil; [self->io release]; self->io = nil; NS_DURING (void)[self->socket shutdown]; NS_HANDLER {} NS_ENDHANDLER; [self->socket release]; self->socket = nil; } /* runloop based IO */ - (NSNotificationCenter *)notificationCenter { return [NSNotificationCenter defaultCenter]; } - (NSRunLoop *)runLoop { return [NSRunLoop currentRunLoop]; } - (NSString *)runLoopMode { return NSDefaultRunLoopMode; } - (void)_socketActivated:(NSNotification *)_n { if ([_n object] != self->socket) return; #if DEBUG && 0 [self debugWithFormat:@"socket activated ..."]; #endif [[self notificationCenter] postNotificationName:WOHTTPConnectionCanReadResponse object:self]; } - (void)_registerForNotification { NSRunLoop *rl; if (self->didRegisterForNotification) return; [[self notificationCenter] addObserver:self selector:@selector(_socketActivated:) name:NSFileObjectBecameActiveNotificationName object:self->socket]; rl = [self runLoop]; [rl addFileObject:self->socket activities:(NSPosixReadableActivity|NSPosixExceptionalActivity) forMode:[self runLoopMode]]; } - (void)_unregisterNotification { if (!self->didRegisterForNotification) return; [[self notificationCenter] removeObserver:self]; [[self runLoop] removeFileObject:self->socket forMode:[self runLoopMode]]; } /* logging IO */ - (void)logRequest:(WORequest *)_response data:(NSData *)_data { if (_data == nil) return; #if 1 NSLog(@"request is\n"); fflush(stderr); fwrite([_data bytes], 1, [_data length], stderr); fflush(stderr); fprintf(stderr,"\n"); fflush(stderr); #endif } - (void)logResponse:(WOResponse *)_response data:(NSData *)_data { if (_data == nil) return; #if 1 NSLog(@"response is\n"); fflush(stderr); fwrite([_data bytes], 1, [_data length], stderr); fflush(stderr); fprintf(stderr,"\n"); fflush(stderr); #endif } /* sending/receiving HTTP */ - (BOOL)sendRequest:(WORequest *)_request { NSData *content; BOOL isok = YES; if (doDebug) [self debugWithFormat:@"send request: %@", _request]; if (![self->socket isConnected]) { if (![self _connect]) { /* could not connect */ if (doDebug) [self debugWithFormat:@" could not connect socket"]; return NO; } /* now connected */ } content = [_request content]; /* write request line (eg 'GET / HTTP/1.0') */ if (doDebug) [self debugWithFormat:@" method: %@", [_request method]]; if (isok) isok = [self->io writeString:[_request method]]; if (isok) isok = [self->io writeString:@" "]; if (self->useProxy) { if (isok) // TODO: check whether this produces a '//' (may need to strip uri) isok = [self->io writeString:[self->url absoluteString]]; [self debugWithFormat:@" wrote proxy start ..."]; } if (isok) isok = [self->io writeString:[_request uri]]; if (isok) isok = [self->io writeString:@" "]; if (isok) isok = [self->io writeString:[_request httpVersion]]; if (isok) isok = [self->io writeString:@"\r\n"]; /* set content-length header */ if ([content length] > 0) { [_request setHeader:[NSString stringWithFormat:@"%d", [content length]] forKey:@"content-length"]; } if ([[self->url scheme] hasPrefix:@"http"]) { /* host header */ if (isok) isok = [self->io writeString:@"Host: "]; if (isok) isok = [self->io writeString:[self hostName]]; if (isok) isok = [self->io writeString:@"\r\n"]; [self debugWithFormat:@" wrote host header: %@", [self hostName]]; } /* write request headers */ if (isok) { NSEnumerator *fields; NSString *fieldName; int cnt; fields = [[_request headerKeys] objectEnumerator]; cnt = 0; while (isok && (fieldName = [fields nextObject])) { NSEnumerator *values; id value; if ([fieldName length] == 4) { if ([fieldName isEqualToString:@"host"]) /* did already write host ... */ continue; if ([fieldName isEqualToString:@"Host"]) /* did already write host ... */ continue; } values = [[_request headersForKey:fieldName] objectEnumerator]; while ((value = [values nextObject]) && isok) { if (isok) isok = [self->io writeString:fieldName]; if (isok) isok = [self->io writeString:@": "]; if (isok) isok = [self->io writeString:value]; if (isok) isok = [self->io writeString:@"\r\n"]; cnt++; } } [self debugWithFormat:@" wrote %i request headers ...", cnt]; } /* write some required headers */ if ([_request headerForKey:@"accept"] == nil) { if (isok) isok = [self->io writeString:@"Accept: */*\r\n"]; [self debugWithFormat:@" wrote accept header ..."]; } if ([_request headerForKey:@"user-agent"] == nil) { if (isok) { static NSString *s = nil; if (s == nil) { s = [[NSString alloc] initWithFormat:@"User-Agent: SOPE/%i.%i.%i\r\n", SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION, SOPE_SUBMINOR_VERSION]; } isok = [self->io writeString:s]; } [self debugWithFormat:@" wrote user-agent header ..."]; } /* write cookie headers */ if ([[_request cookies] count] > 0 && isok) { NSEnumerator *cookies; WOCookie *cookie; BOOL isFirst; int cnt; [self->io writeString:@"set-cookie: "]; cnt = 0; cookies = [[_request cookies] objectEnumerator]; isFirst = YES; while (isok && (cookie = [cookies nextObject])) { if (isFirst) isFirst = NO; else if (isok) isok = [self->io writeString:@"; "]; if (isok) isok = [self->io writeString:[cookie stringValue]]; cnt ++; } if (isok) isok = [self->io writeString:@"\r\n"]; [self debugWithFormat:@" wrote %i cookies ...", cnt]; } /* flush request header on socket */ if (isok) isok = [self->io writeString:@"\r\n"]; if (isok) isok = [self->io flush]; [self debugWithFormat:@" flushed HTTP header."]; /* write content */ if ([content length] > 0) { [self debugWithFormat:@" writing HTTP entity (length=%i).", [content length]]; if ([content isKindOfClass:[NSString class]]) { if (isok) isok = [self->io writeString:(NSString *)content]; } else if ([content isKindOfClass:[NSData class]]) { if (isok) isok = [[self->io source] safeWriteBytes:[content bytes] count:[content length]]; } else { if (isok) isok = [self->io writeString:[content description]]; } if (isok) isok = [self->io flush]; } else if (doDebug) { [self debugWithFormat:@" no HTTP entity to write ..."]; } if (logStream) [self logRequest:_request data:[self->log writeLog]]; [self->log resetWriteLog]; [self debugWithFormat:@"=> finished:\n url: %@\n sock: %@", self->url, self->socket]; if (!isok) { ASSIGN(self->lastException, [self->socket lastException]); [self->socket shutdown]; return NO; } if (![self->socket isConnected]) return NO; [self _registerForNotification]; return YES; } - (NSException *)handleResponseParsingError:(NSException *)_exception { fprintf(stderr, "%s: caught: %s\n", __PRETTY_FUNCTION__, [[_exception description] cString]); return nil; } - (WOResponse *)readResponse { /* TODO: split up method */ WOResponse *response; *(&response) = nil; [self _unregisterNotification]; if (self->socket == nil) { [self debugWithFormat:@"no socket available for reading response ..."]; return nil; } [self debugWithFormat:@"parsing response from socket: %@", self->socket]; if (useSimpleParser) { WOSimpleHTTPParser *parser; [self debugWithFormat:@" using simple HTTP parser ..."]; parser = [[WOSimpleHTTPParser alloc] initWithStream:[self->io source]]; if (parser == nil) return nil; parser = [parser autorelease]; if ((response = [parser parseResponse]) == nil) { if (doDebug) [self debugWithFormat:@"parsing failed: %@", [parser lastException]]; } } else { NGHttpMessageParser *parser; NGHttpResponse *mresponse; NGMimeType *ctype; id body; *(&mresponse) = nil; if ((parser = [[[NGHttpMessageParser alloc] init] autorelease]) == nil) return nil; [self debugWithFormat:@" using MIME HTTP parser (complex parser) ..."]; NS_DURING { [parser setDelegate:self]; mresponse = [parser parseResponseFromStream:self->socket]; } NS_HANDLER [[self handleResponseParsingError:localException] raise]; NS_ENDHANDLER; [self debugWithFormat:@"finished parsing response: %@", mresponse]; /* transform parsed MIME response to WOResponse */ body = [mresponse body]; if (body == nil) body = [NSData data]; response = [[[WOResponse alloc] init] autorelease]; [response setHTTPVersion:[mresponse httpVersion]]; [response setStatus:[mresponse statusCode]]; [response setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys: self, @"NGHTTPConnection", mresponse, @"NGMimeResponse", body, @"NGMimeBody", nil]]; { /* check content-type */ id value; value = [[mresponse valuesOfHeaderFieldWithName:@"content-type"] nextObject]; if (value) { NSString *charset; ctype = [NGMimeType mimeType:[value stringValue]]; charset = [[ctype valueOfParameter:@"charset"] lowercaseString]; if ([charset length] == 0) { /* autodetect charset ... */ if ([[ctype type] isEqualToString:@"text"]) { if ([[ctype subType] isEqualToString:@"xml"]) { /* default XML encoding is UTF-8 */ [response setContentEncoding:NSUTF8StringEncoding]; } } } else { NSStringEncoding enc; enc = [NGMimeType stringEncodingForCharset:charset]; [response setContentEncoding:enc]; } [response setHeader:[ctype stringValue] forKey:@"content-type"]; } else { ctype = [NGMimeType mimeType:@"application/octet-stream"]; } } /* check content */ if ([body isKindOfClass:[NSData class]]) { [response setContent:body]; } else if ([body isKindOfClass:[NSString class]]) { NSData *data; data = [body dataUsingEncoding:[response contentEncoding]]; if (data) [response setContent:data]; } else if (body) { /* generate data from structured body .. */ NGMimeBodyGenerator *gen; NSData *data; gen = [[[NGMimeBodyGenerator alloc] init] autorelease]; data = [gen generateBodyOfPart:body additionalHeaders:nil delegate:self]; [response setContent:data]; } { /* transfer headers */ NSEnumerator *names; NSString *name; names = [mresponse headerFieldNames]; while ((name = [names nextObject])) { NSEnumerator *values; id value; if ([name isEqualToString:@"content-type"]) continue; if ([name isEqualToString:@"set-cookie"]) continue; values = [mresponse valuesOfHeaderFieldWithName:name]; while ((value = [values nextObject])) { value = [value stringValue]; [response appendHeader:value forKey:name]; } } } { /* transfer cookies */ NSEnumerator *cookies; NGHttpCookie *mcookie; cookies = [mresponse valuesOfHeaderFieldWithName:@"set-cookie"]; while ((mcookie = [cookies nextObject])) { WOCookie *woCookie; if (![mcookie isKindOfClass:[NGHttpCookie class]]) { /* parse cookie */ woCookie = [WOCookie cookieWithString:[mcookie stringValue]]; } else { woCookie = [WOCookie cookieWithName:[mcookie cookieName] value:[mcookie value] path:[mcookie path] domain:[mcookie domainName] expires:[mcookie expireDate] isSecure:[mcookie needsSecureChannel]]; } if (woCookie == nil) { [self logWithFormat: @"Couldn't create WOCookie from NGHttp cookie: %@", mcookie]; // could not create cookie continue; } [self debugWithFormat:@"adding cookie: %@", woCookie]; [response addCookie:woCookie]; } } } if (logStream) [self logResponse:response data:[self->log readLog]]; [self->log resetReadLog]; if (doDebug) [self debugWithFormat:@"processed response: %@", response]; /* check keep-alive */ { NSString *conn; conn = [response headerForKey:@"connection"]; conn = [conn lowercaseString]; if ([conn isEqualToString:@"close"]) { [self setKeepAliveEnabled:NO]; [self _disconnect]; } else if ([conn isEqualToString:@"keep-alive"]) { [self setKeepAliveEnabled:YES]; } else { [self setKeepAliveEnabled:NO]; [self _disconnect]; } } return response; } - (void)setKeepAliveEnabled:(BOOL)_flag { self->keepAlive = _flag; } - (BOOL)keepAliveEnabled { return self->keepAlive; } /* timeouts */ - (void)setConnectTimeout:(int)_seconds { self->connectTimeout = _seconds; } - (int)connectTimeout { return self->connectTimeout; } - (void)setReceiveTimeout:(int)_seconds { self->receiveTimeout = _seconds; } - (int)receiveTimeout { return self->receiveTimeout; } - (void)setSendTimeout:(int)_seconds { self->sendTimeout = _seconds; } - (int)sendTimeout { return self->sendTimeout; } /* description */ - (NSString *)description { NSMutableString *str; str = [NSMutableString stringWithCapacity:128]; [str appendFormat:@"<%@[0x%p]:", NSStringFromClass([self class]), self]; if (self->url) [str appendFormat:@" url=%@", self->url]; if (self->useProxy) [str appendString:@" proxy"]; if (self->useSSL) [str appendString:@" SSL"]; if (self->socket) [str appendFormat:@" socket=%@", self->socket]; [str appendString:@">"]; return str; } @end /* WOHTTPConnection */ @implementation NSURL(SocketAddress) - (id)socketAddressForURL { NSString *s; s = [self scheme]; if ([s isEqualToString:@"http"]) { int p; s = [self host]; if ([s length] == 0) s = @"localhost"; p = [[self port] intValue]; return [NGInternetSocketAddress addressWithPort:p == 0 ? 80 : p onHost:s]; } else if ([s isEqualToString:@"https"]) { int p; s = [self host]; if ([s length] == 0) s = @"localhost"; p = [[self port] intValue]; return [NGInternetSocketAddress addressWithPort:p == 0 ? 443 : p onHost:s]; } else if ([s isEqualToString:@"unix"] || [s isEqualToString:@"file"]) { return [NGLocalSocketAddress addressWithPath:[self path]]; } return nil; } - (BOOL)shouldUseWOProxyServer { if ([[self scheme] hasPrefix:@"http"]) { NSString *h; if ((h = [self host]) == nil) return NO; if ([h isEqualToString:@"127.0.0.1"]) return NO; if ([h isEqualToString:@"localhost"]) return NO; if ([[WOHTTPConnection proxyServer] length] > 0) { NSEnumerator *e; NSString *suffix; BOOL useProxy; useProxy = YES; e = [[WOHTTPConnection noProxySuffixes] objectEnumerator]; while ((suffix = [e nextObject])) { if ([h hasSuffix:suffix]) { useProxy = NO; break; } } return useProxy; } } return NO; } @end /* NSURL(SocketAddress) */