2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include <NGObjWeb/WOHTTPConnection.h>
23 #include <NGObjWeb/WOCookie.h>
24 #include <NGObjWeb/WORequest.h>
25 #include <NGObjWeb/WOResponse.h>
26 #include <NGObjWeb/WOCookie.h>
27 #include <NGObjWeb/WORunLoop.h>
28 #include <NGStreams/NGStreams.h>
29 #include <NGStreams/NGCTextStream.h>
30 #include <NGStreams/NGBufferedStream.h>
31 #include <NGStreams/NGNet.h>
32 #include <NGHttp/NGHttp.h>
33 #include <NGMime/NGMime.h>
34 #import <Foundation/Foundation.h>
35 #include "WOSimpleHTTPParser.h"
36 #include "WOHttpAdaptor/WORecordRequestStream.h"
38 @interface WOHTTPConnection(Privates)
41 - (void)_unregisterNotification;
44 @interface WOCookie(Privates)
45 + (id)cookieWithString:(NSString *)_string;
48 NSString *WOHTTPConnectionCanReadResponse = @"WOHTTPConnectionCanReadResponse";
50 @interface NSURL(SocketAddress)
51 - (id)socketAddressForURL;
52 - (BOOL)shouldUseWOProxyServer;
55 @interface WOHTTPConnection(Privates2)
56 + (NSString *)proxyServer;
57 + (NSURL *)proxyServerURL;
58 + (NSArray *)noProxySuffixes;
61 @implementation WOHTTPConnection
63 static Class SSLSocketClass = Nil;
64 static BOOL useSimpleParser = YES;
65 static NSString *proxyServer = nil;
66 static NSArray *noProxy = nil;
67 static BOOL doDebug = NO;
68 static BOOL logStream = NO;
75 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
76 static BOOL didInit = NO;
80 useSimpleParser = [ud boolForKey:@"WOHTTPConnectionUseSimpleParser"];
81 proxyServer = [ud stringForKey:@"WOProxyServer"];
82 noProxy = [ud arrayForKey:@"WONoProxySuffixes"];
83 doDebug = [ud boolForKey:@"WODebugHTTPConnection"];
84 logStream = [ud boolForKey:@"WODebugHTTPConnectionLogStream"];
87 + (NSString *)proxyServer {
90 + (NSURL *)proxyServerURL {
93 ps = [self proxyServer];
97 return [NSURL URLWithString:ps];
99 + (NSArray *)noProxySuffixes {
103 - (id)initWithNSURL:(NSURL *)_url {
104 if ((self = [super init])) {
105 self->url = [_url retain];
106 self->useSSL = [[_url scheme] isEqualToString:@"https"];
107 self->useProxy = [_url shouldUseWOProxyServer];
110 static BOOL didCheck = NO;
113 SSLSocketClass = NSClassFromString(@"NGActiveSSLSocket");
120 - (id)initWithURL:(id)_url {
123 /* create an NSURL object if necessary */
124 lurl = [_url isKindOfClass:[NSURL class]]
126 : [NSURL URLWithString:[_url stringValue]];
129 [self logWithFormat:@"could not construct URL from object '%@' !", _url];
134 [self logWithFormat:@"init with URL: %@", lurl];
135 return [self initWithNSURL:lurl];
138 - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port
143 s = [NSString stringWithFormat:@"http%s://%@:%i/",
144 _flag ? "s" : "", _hostName,
145 _port == 0 ? (_flag?443:80) : _port];
146 return [self initWithURL:s];
148 - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port {
149 return [self initWithHost:_hostName onPort:_port secure:NO];
152 return [self initWithHost:@"localhost" onPort:80 secure:NO];
156 [self _unregisterNotification];
157 [self->lastException release];
160 [self->socket release];
167 - (NSException *)lastException {
168 return self->lastException;
171 - (BOOL)isDebuggingEnabled {
172 return doDebug ? YES : NO;
174 - (NSString *)loggingPrefix {
175 /* improve perf ... */
177 return [NSString stringWithFormat:@"WOHTTP[0x%08X]<%@>",
178 self, [self->url absoluteString]];
181 return [NSString stringWithFormat:@"WOHTTP[0x%08X]", self];
186 - (NSString *)hostName {
187 return [self->url host];
193 id<NGSocketAddress> address;
198 NSAssert(self->socket == nil, @"socket still available after disconnect");
199 NSAssert(self->io == nil, @"IO stream still available after disconnect");
203 if (SSLSocketClass == Nil) {
204 /* no SSL support is available */
205 static BOOL didLog = NO;
208 NSLog(@"NOTE: SSL support is not available !");
214 if (self->useProxy) {
217 purl = [[self class] proxyServerURL];
218 address = [purl socketAddressForURL];
221 address = [self->url socketAddressForURL];
223 if (address == nil) {
224 [self debugWithFormat:@"got no address for connect .."];
229 self->socket = self->useSSL
230 ? [SSLSocketClass socketConnectedToAddress:address]
231 : [NGActiveSocket socketConnectedToAddress:address];
235 fprintf(stderr, "couldn't create socket: %s\n",
236 [[localException description] cString]);
238 ASSIGN(self->lastException, localException);
243 if (self->socket == nil) {
244 [self debugWithFormat:@"socket is not setup: %@", [self lastException]];
248 if (![self->socket isConnected]) {
250 [self debugWithFormat:@"socket is not connected .."];
254 self->socket = [self->socket retain];
256 [(NGActiveSocket *)self->socket setSendTimeout:[self sendTimeout]];
257 [(NGActiveSocket *)self->socket setReceiveTimeout:[self receiveTimeout]];
262 bStr = [[NGBufferedStream alloc] initWithSource:self->socket];
264 self->log = [[WORecordRequestStream alloc] initWithSource:bStr];
269 [[NGCTextStream alloc] initWithSource:(id)(self->log?self->log:bStr)];
270 [bStr release]; bStr = nil;
275 - (void)_disconnect {
276 [self->log release]; self->log = nil;
277 [self->io release]; self->io = nil;
280 (void)[self->socket shutdown];
284 [self->socket release]; self->socket = nil;
287 /* runloop based IO */
289 - (NSNotificationCenter *)notificationCenter {
290 return [NSNotificationCenter defaultCenter];
292 - (NSRunLoop *)runLoop {
293 return [NSRunLoop currentRunLoop];
295 - (NSString *)runLoopMode {
296 return NSDefaultRunLoopMode;
299 - (void)_socketActivated:(NSNotification *)_n {
300 if ([_n object] != self->socket)
304 [self debugWithFormat:@"socket activated ..."];
307 [[self notificationCenter]
308 postNotificationName:WOHTTPConnectionCanReadResponse
312 - (void)_registerForNotification {
315 if (self->didRegisterForNotification)
318 [[self notificationCenter]
319 addObserver:self selector:@selector(_socketActivated:)
320 name:NSFileObjectBecameActiveNotificationName
321 object:self->socket];
324 [rl addFileObject:self->socket
325 activities:(NSPosixReadableActivity|NSPosixExceptionalActivity)
326 forMode:[self runLoopMode]];
328 - (void)_unregisterNotification {
329 if (!self->didRegisterForNotification)
332 [[self notificationCenter] removeObserver:self];
334 [[self runLoop] removeFileObject:self->socket
335 forMode:[self runLoopMode]];
340 - (void)logRequest:(WORequest *)_response data:(NSData *)_data {
341 if (_data == nil) return;
344 NSLog(@"request is\n");
346 fwrite([_data bytes], 1, [_data length], stderr);
348 fprintf(stderr,"\n");
352 - (void)logResponse:(WOResponse *)_response data:(NSData *)_data {
353 if (_data == nil) return;
356 NSLog(@"response is\n");
358 fwrite([_data bytes], 1, [_data length], stderr);
360 fprintf(stderr,"\n");
365 /* sending/receiving HTTP */
367 - (BOOL)sendRequest:(WORequest *)_request {
372 [self debugWithFormat:@"send request: %@", _request];
374 if (![self->socket isConnected]) {
375 if (![self _connect]) {
376 /* could not connect */
378 [self debugWithFormat:@" could not connect socket"];
384 content = [_request content];
386 /* write request line (eg 'GET / HTTP/1.0') */
388 [self debugWithFormat:@" method: %@", [_request method]];
390 if (isok) isok = [self->io writeString:[_request method]];
391 if (isok) isok = [self->io writeString:@" "];
393 if (self->useProxy) {
395 // TODO: check whether this produces a '//' (may need to strip uri)
396 isok = [self->io writeString:[self->url absoluteString]];
397 [self debugWithFormat:@" wrote proxy start ..."];
399 if (isok) isok = [self->io writeString:[_request uri]];
401 if (isok) isok = [self->io writeString:@" "];
402 if (isok) isok = [self->io writeString:[_request httpVersion]];
403 if (isok) isok = [self->io writeString:@"\r\n"];
405 /* set content-length header */
407 if ([content length] > 0) {
408 [_request setHeader:[NSString stringWithFormat:@"%d", [content length]]
409 forKey:@"content-length"];
412 if ([[self->url scheme] hasPrefix:@"http"]) {
415 if (isok) isok = [self->io writeString:@"Host: "];
416 if (isok) isok = [self->io writeString:[self hostName]];
417 if (isok) isok = [self->io writeString:@"\r\n"];
418 [self debugWithFormat:@" wrote host header: %@", [self hostName]];
421 /* write request headers */
424 NSEnumerator *fields;
428 fields = [[_request headerKeys] objectEnumerator];
430 while (isok && (fieldName = [fields nextObject])) {
431 NSEnumerator *values;
434 if ([fieldName length] == 4) {
435 if ([fieldName isEqualToString:@"host"])
436 /* did already write host ... */
438 if ([fieldName isEqualToString:@"Host"])
439 /* did already write host ... */
443 values = [[_request headersForKey:fieldName] objectEnumerator];
445 while ((value = [values nextObject]) && isok) {
446 if (isok) isok = [self->io writeString:fieldName];
447 if (isok) isok = [self->io writeString:@": "];
448 if (isok) isok = [self->io writeString:value];
449 if (isok) isok = [self->io writeString:@"\r\n"];
453 [self debugWithFormat:@" wrote %i request headers ...", cnt];
456 /* write some required headers */
458 if ([_request headerForKey:@"accept"] == nil) {
459 if (isok) isok = [self->io writeString:@"Accept: */*\r\n"];
460 [self debugWithFormat:@" wrote accept header ..."];
462 if ([_request headerForKey:@"user-agent"] == nil) {
463 if (isok) isok = [self->io writeString:@"User-Agent: SOPE/4.2\r\n"];
464 [self debugWithFormat:@" wrote user-agent header ..."];
467 /* write cookie headers */
469 if ([[_request cookies] count] > 0 && isok) {
470 NSEnumerator *cookies;
475 [self->io writeString:@"set-cookie: "];
477 cookies = [[_request cookies] objectEnumerator];
479 while (isok && (cookie = [cookies nextObject])) {
480 if (isFirst) isFirst = NO;
481 else if (isok) isok = [self->io writeString:@"; "];
483 if (isok) isok = [self->io writeString:[cookie stringValue]];
486 if (isok) isok = [self->io writeString:@"\r\n"];
487 [self debugWithFormat:@" wrote %i cookies ...", cnt];
490 /* flush request header on socket */
492 if (isok) isok = [self->io writeString:@"\r\n"];
493 if (isok) isok = [self->io flush];
494 [self debugWithFormat:@" flushed HTTP header."];
498 if ([content length] > 0) {
499 [self debugWithFormat:@" writing HTTP entity (length=%i).",
502 if ([content isKindOfClass:[NSString class]]) {
503 if (isok) isok = [self->io writeString:(NSString *)content];
505 else if ([content isKindOfClass:[NSData class]]) {
506 if (isok) isok = [[self->io source]
507 safeWriteBytes:[content bytes]
508 count:[content length]];
511 if (isok) isok = [self->io writeString:[content description]];
513 if (isok) isok = [self->io flush];
516 [self debugWithFormat:@" no HTTP entity to write ..."];
520 [self logRequest:_request data:[self->log writeLog]];
521 [self->log resetWriteLog];
523 [self debugWithFormat:@"=> finished:\n url: %@\n sock: %@",
524 self->url, self->socket];
526 ASSIGN(self->lastException, [self->socket lastException]);
527 [self->socket shutdown];
531 if (![self->socket isConnected])
534 [self _registerForNotification];
539 - (NSException *)handleResponseParsingError:(NSException *)_exception {
540 fprintf(stderr, "%s: caught: %s\n",
542 [[_exception description] cString]);
546 - (WOResponse *)readResponse {
547 /* TODO: split up method */
548 WOResponse *response;
551 [self _unregisterNotification];
553 if (self->socket == nil) {
554 [self debugWithFormat:@"no socket available for reading response ..."];
558 [self debugWithFormat:@"parsing response from socket: %@", self->socket];
560 if (useSimpleParser) {
561 WOSimpleHTTPParser *parser;
563 [self debugWithFormat:@" using simple HTTP parser ..."];
565 parser = [[WOSimpleHTTPParser alloc] initWithStream:[self->io source]];
568 parser = [parser autorelease];
570 if ((response = [parser parseResponse]) == nil) {
572 [self debugWithFormat:@"parsing failed: %@", [parser lastException]];
576 NGHttpMessageParser *parser;
577 NGHttpResponse *mresponse;
583 if ((parser = [[[NGHttpMessageParser alloc] init] autorelease]) == nil)
586 [self debugWithFormat:@" using MIME HTTP parser (complex parser) ..."];
589 [parser setDelegate:self];
590 mresponse = [parser parseResponseFromStream:self->socket];
593 [[self handleResponseParsingError:localException] raise];
596 [self debugWithFormat:@"finished parsing response: %@", mresponse];
598 /* transform parsed MIME response to WOResponse */
600 body = [mresponse body];
601 if (body == nil) body = [NSData data];
603 response = [[[WOResponse alloc] init] autorelease];
604 [response setHTTPVersion:[mresponse httpVersion]];
605 [response setStatus:[mresponse statusCode]];
606 [response setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys:
607 self, @"NGHTTPConnection",
608 mresponse, @"NGMimeResponse",
612 { /* check content-type */
615 value = [[mresponse valuesOfHeaderFieldWithName:@"content-type"]
620 ctype = [NGMimeType mimeType:[value stringValue]];
621 charset = [[ctype valueOfParameter:@"charset"] lowercaseString];
623 if ([charset length] == 0) {
624 /* autodetect charset ... */
626 if ([[ctype type] isEqualToString:@"text"]) {
627 if ([[ctype subType] isEqualToString:@"xml"]) {
628 /* default XML encoding is UTF-8 */
629 [response setContentEncoding:NSUTF8StringEncoding];
634 NSStringEncoding enc;
636 enc = [NGMimeType stringEncodingForCharset:charset];
637 [response setContentEncoding:enc];
640 [response setHeader:[ctype stringValue] forKey:@"content-type"];
644 ctype = [NGMimeType mimeType:@"application/octet-stream"];
650 if ([body isKindOfClass:[NSData class]]) {
651 [response setContent:body];
653 else if ([body isKindOfClass:[NSString class]]) {
656 data = [body dataUsingEncoding:[response contentEncoding]];
658 [response setContent:data];
661 /* generate data from structured body .. */
662 NGMimeBodyGenerator *gen;
665 gen = [[[NGMimeBodyGenerator alloc] init] autorelease];
666 data = [gen generateBodyOfPart:body
667 additionalHeaders:nil
669 [response setContent:data];
672 { /* transfer headers */
676 names = [mresponse headerFieldNames];
677 while ((name = [names nextObject])) {
678 NSEnumerator *values;
681 if ([name isEqualToString:@"content-type"])
683 if ([name isEqualToString:@"set-cookie"])
686 values = [mresponse valuesOfHeaderFieldWithName:name];
687 while ((value = [values nextObject])) {
688 value = [value stringValue];
689 [response appendHeader:value forKey:name];
694 { /* transfer cookies */
695 NSEnumerator *cookies;
696 NGHttpCookie *mcookie;
698 cookies = [mresponse valuesOfHeaderFieldWithName:@"set-cookie"];
700 while ((mcookie = [cookies nextObject])) {
703 if (![mcookie isKindOfClass:[NGHttpCookie class]]) {
705 woCookie = [WOCookie cookieWithString:[mcookie stringValue]];
708 woCookie = [WOCookie cookieWithName:[mcookie cookieName]
709 value:[mcookie value]
711 domain:[mcookie domainName]
712 expires:[mcookie expireDate]
713 isSecure:[mcookie needsSecureChannel]];
715 if (woCookie == nil) {
717 @"Couldn't create WOCookie from NGHttp cookie: %@",
719 // could not create cookie
723 [self debugWithFormat:@"adding cookie: %@", woCookie];
725 [response addCookie:woCookie];
731 [self logResponse:response data:[self->log readLog]];
732 [self->log resetReadLog];
735 [self debugWithFormat:@"processed response: %@", response];
737 /* check keep-alive */
741 conn = [response headerForKey:@"connection"];
742 conn = [conn lowercaseString];
744 if ([conn isEqualToString:@"close"]) {
745 [self setKeepAliveEnabled:NO];
748 else if ([conn isEqualToString:@"keep-alive"]) {
749 [self setKeepAliveEnabled:YES];
752 [self setKeepAliveEnabled:NO];
760 - (void)setKeepAliveEnabled:(BOOL)_flag {
761 self->keepAlive = _flag;
763 - (BOOL)keepAliveEnabled {
764 return self->keepAlive;
769 - (void)setConnectTimeout:(int)_seconds {
770 self->connectTimeout = _seconds;
772 - (int)connectTimeout {
773 return self->connectTimeout;
776 - (void)setReceiveTimeout:(int)_seconds {
777 self->receiveTimeout = _seconds;
779 - (int)receiveTimeout {
780 return self->receiveTimeout;
783 - (void)setSendTimeout:(int)_seconds {
784 self->sendTimeout = _seconds;
787 return self->sendTimeout;
792 - (NSString *)description {
793 NSMutableString *str;
795 str = [NSMutableString stringWithCapacity:128];
796 [str appendFormat:@"<%@[0x%08X]:", NSStringFromClass([self class]), self];
798 if (self->url) [str appendFormat:@" url=%@", self->url];
799 if (self->useProxy) [str appendString:@" proxy"];
800 if (self->useSSL) [str appendString:@" SSL"];
802 if (self->socket) [str appendFormat:@" socket=%@", self->socket];
804 [str appendString:@">"];
808 @end /* WOHTTPConnection */
810 @implementation NSURL(SocketAddress)
812 - (id)socketAddressForURL {
817 if ([s isEqualToString:@"http"]) {
821 if ([s length] == 0) s = @"localhost";
822 p = [[self port] intValue];
824 return [NGInternetSocketAddress addressWithPort:p == 0 ? 80 : p onHost:s];
826 else if ([s isEqualToString:@"https"]) {
830 if ([s length] == 0) s = @"localhost";
831 p = [[self port] intValue];
833 return [NGInternetSocketAddress addressWithPort:p == 0 ? 443 : p onHost:s];
835 else if ([s isEqualToString:@"unix"] || [s isEqualToString:@"file"]) {
836 return [NGLocalSocketAddress addressWithPath:[self path]];
841 - (BOOL)shouldUseWOProxyServer {
842 if ([[self scheme] hasPrefix:@"http"]) {
845 if ((h = [self host]) == nil)
848 if ([h isEqualToString:@"127.0.0.1"])
850 if ([h isEqualToString:@"localhost"])
853 if ([[WOHTTPConnection proxyServer] length] > 0) {
859 e = [[WOHTTPConnection noProxySuffixes] objectEnumerator];
860 while ((suffix = [e nextObject])) {
861 if ([h hasSuffix:suffix]) {
872 @end /* NSURL(SocketAddress) */