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]];
259 if (self->socket != nil) {
260 NGBufferedStream *bStr;
262 bStr = [NGBufferedStream alloc]; // keep gcc happy
263 bStr = [bStr initWithSource:self->socket];
265 self->log = [WORecordRequestStream alloc]; // keep gcc happy
266 self->log = [(WORecordRequestStream *)self->log initWithSource:bStr];
271 self->io = [NGCTextStream alloc]; // keep gcc happy
272 self->io = [self->io initWithSource:(id)(self->log ? self->log : bStr)];
273 [bStr release]; bStr = nil;
278 - (void)_disconnect {
279 [self->log release]; self->log = nil;
280 [self->io release]; self->io = nil;
283 (void)[self->socket shutdown];
287 [self->socket release]; self->socket = nil;
290 /* runloop based IO */
292 - (NSNotificationCenter *)notificationCenter {
293 return [NSNotificationCenter defaultCenter];
295 - (NSRunLoop *)runLoop {
296 return [NSRunLoop currentRunLoop];
298 - (NSString *)runLoopMode {
299 return NSDefaultRunLoopMode;
302 - (void)_socketActivated:(NSNotification *)_n {
303 if ([_n object] != self->socket)
307 [self debugWithFormat:@"socket activated ..."];
310 [[self notificationCenter]
311 postNotificationName:WOHTTPConnectionCanReadResponse
315 - (void)_registerForNotification {
318 if (self->didRegisterForNotification)
321 [[self notificationCenter]
322 addObserver:self selector:@selector(_socketActivated:)
323 name:NSFileObjectBecameActiveNotificationName
324 object:self->socket];
327 [rl addFileObject:self->socket
328 activities:(NSPosixReadableActivity|NSPosixExceptionalActivity)
329 forMode:[self runLoopMode]];
331 - (void)_unregisterNotification {
332 if (!self->didRegisterForNotification)
335 [[self notificationCenter] removeObserver:self];
337 [[self runLoop] removeFileObject:self->socket
338 forMode:[self runLoopMode]];
343 - (void)logRequest:(WORequest *)_response data:(NSData *)_data {
344 if (_data == nil) return;
347 NSLog(@"request is\n");
349 fwrite([_data bytes], 1, [_data length], stderr);
351 fprintf(stderr,"\n");
355 - (void)logResponse:(WOResponse *)_response data:(NSData *)_data {
356 if (_data == nil) return;
359 NSLog(@"response is\n");
361 fwrite([_data bytes], 1, [_data length], stderr);
363 fprintf(stderr,"\n");
368 /* sending/receiving HTTP */
370 - (BOOL)sendRequest:(WORequest *)_request {
375 [self debugWithFormat:@"send request: %@", _request];
377 if (![self->socket isConnected]) {
378 if (![self _connect]) {
379 /* could not connect */
381 [self debugWithFormat:@" could not connect socket"];
387 content = [_request content];
389 /* write request line (eg 'GET / HTTP/1.0') */
391 [self debugWithFormat:@" method: %@", [_request method]];
393 if (isok) isok = [self->io writeString:[_request method]];
394 if (isok) isok = [self->io writeString:@" "];
396 if (self->useProxy) {
398 // TODO: check whether this produces a '//' (may need to strip uri)
399 isok = [self->io writeString:[self->url absoluteString]];
400 [self debugWithFormat:@" wrote proxy start ..."];
402 if (isok) isok = [self->io writeString:[_request uri]];
404 if (isok) isok = [self->io writeString:@" "];
405 if (isok) isok = [self->io writeString:[_request httpVersion]];
406 if (isok) isok = [self->io writeString:@"\r\n"];
408 /* set content-length header */
410 if ([content length] > 0) {
411 [_request setHeader:[NSString stringWithFormat:@"%d", [content length]]
412 forKey:@"content-length"];
415 if ([[self->url scheme] hasPrefix:@"http"]) {
418 if (isok) isok = [self->io writeString:@"Host: "];
419 if (isok) isok = [self->io writeString:[self hostName]];
420 if (isok) isok = [self->io writeString:@"\r\n"];
421 [self debugWithFormat:@" wrote host header: %@", [self hostName]];
424 /* write request headers */
427 NSEnumerator *fields;
431 fields = [[_request headerKeys] objectEnumerator];
433 while (isok && (fieldName = [fields nextObject])) {
434 NSEnumerator *values;
437 if ([fieldName length] == 4) {
438 if ([fieldName isEqualToString:@"host"])
439 /* did already write host ... */
441 if ([fieldName isEqualToString:@"Host"])
442 /* did already write host ... */
446 values = [[_request headersForKey:fieldName] objectEnumerator];
448 while ((value = [values nextObject]) && isok) {
449 if (isok) isok = [self->io writeString:fieldName];
450 if (isok) isok = [self->io writeString:@": "];
451 if (isok) isok = [self->io writeString:value];
452 if (isok) isok = [self->io writeString:@"\r\n"];
456 [self debugWithFormat:@" wrote %i request headers ...", cnt];
459 /* write some required headers */
461 if ([_request headerForKey:@"accept"] == nil) {
462 if (isok) isok = [self->io writeString:@"Accept: */*\r\n"];
463 [self debugWithFormat:@" wrote accept header ..."];
465 if ([_request headerForKey:@"user-agent"] == nil) {
467 static NSString *s = nil;
469 s = [[NSString alloc] initWithFormat:@"User-Agent: SOPE/%i.%i.%i\r\n",
470 SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION,
471 SOPE_SUBMINOR_VERSION];
473 isok = [self->io writeString:s];
475 [self debugWithFormat:@" wrote user-agent header ..."];
478 /* write cookie headers */
480 if ([[_request cookies] count] > 0 && isok) {
481 NSEnumerator *cookies;
486 [self->io writeString:@"set-cookie: "];
488 cookies = [[_request cookies] objectEnumerator];
490 while (isok && (cookie = [cookies nextObject])) {
491 if (isFirst) isFirst = NO;
492 else if (isok) isok = [self->io writeString:@"; "];
494 if (isok) isok = [self->io writeString:[cookie stringValue]];
497 if (isok) isok = [self->io writeString:@"\r\n"];
498 [self debugWithFormat:@" wrote %i cookies ...", cnt];
501 /* flush request header on socket */
503 if (isok) isok = [self->io writeString:@"\r\n"];
504 if (isok) isok = [self->io flush];
505 [self debugWithFormat:@" flushed HTTP header."];
509 if ([content length] > 0) {
510 [self debugWithFormat:@" writing HTTP entity (length=%i).",
513 if ([content isKindOfClass:[NSString class]]) {
514 if (isok) isok = [self->io writeString:(NSString *)content];
516 else if ([content isKindOfClass:[NSData class]]) {
517 if (isok) isok = [[self->io source]
518 safeWriteBytes:[content bytes]
519 count:[content length]];
522 if (isok) isok = [self->io writeString:[content description]];
524 if (isok) isok = [self->io flush];
527 [self debugWithFormat:@" no HTTP entity to write ..."];
531 [self logRequest:_request data:[self->log writeLog]];
532 [self->log resetWriteLog];
534 [self debugWithFormat:@"=> finished:\n url: %@\n sock: %@",
535 self->url, self->socket];
537 ASSIGN(self->lastException, [self->socket lastException]);
538 [self->socket shutdown];
542 if (![self->socket isConnected])
545 [self _registerForNotification];
550 - (NSException *)handleResponseParsingError:(NSException *)_exception {
551 fprintf(stderr, "%s: caught: %s\n",
553 [[_exception description] cString]);
557 - (WOResponse *)readResponse {
558 /* TODO: split up method */
559 WOResponse *response;
562 [self _unregisterNotification];
564 if (self->socket == nil) {
565 [self debugWithFormat:@"no socket available for reading response ..."];
569 [self debugWithFormat:@"parsing response from socket: %@", self->socket];
571 if (useSimpleParser) {
572 WOSimpleHTTPParser *parser;
574 [self debugWithFormat:@" using simple HTTP parser ..."];
576 parser = [[WOSimpleHTTPParser alloc] initWithStream:[self->io source]];
579 parser = [parser autorelease];
581 if ((response = [parser parseResponse]) == nil) {
583 [self debugWithFormat:@"parsing failed: %@", [parser lastException]];
587 NGHttpMessageParser *parser;
588 NGHttpResponse *mresponse;
594 if ((parser = [[[NGHttpMessageParser alloc] init] autorelease]) == nil)
597 [self debugWithFormat:@" using MIME HTTP parser (complex parser) ..."];
600 [parser setDelegate:self];
601 mresponse = [parser parseResponseFromStream:self->socket];
604 [[self handleResponseParsingError:localException] raise];
607 [self debugWithFormat:@"finished parsing response: %@", mresponse];
609 /* transform parsed MIME response to WOResponse */
611 body = [mresponse body];
612 if (body == nil) body = [NSData data];
614 response = [[[WOResponse alloc] init] autorelease];
615 [response setHTTPVersion:[mresponse httpVersion]];
616 [response setStatus:[mresponse statusCode]];
617 [response setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys:
618 self, @"NGHTTPConnection",
619 mresponse, @"NGMimeResponse",
623 { /* check content-type */
626 value = [[mresponse valuesOfHeaderFieldWithName:@"content-type"]
631 ctype = [NGMimeType mimeType:[value stringValue]];
632 charset = [[ctype valueOfParameter:@"charset"] lowercaseString];
634 if ([charset length] == 0) {
635 /* autodetect charset ... */
637 if ([[ctype type] isEqualToString:@"text"]) {
638 if ([[ctype subType] isEqualToString:@"xml"]) {
639 /* default XML encoding is UTF-8 */
640 [response setContentEncoding:NSUTF8StringEncoding];
645 NSStringEncoding enc;
647 enc = [NGMimeType stringEncodingForCharset:charset];
648 [response setContentEncoding:enc];
651 [response setHeader:[ctype stringValue] forKey:@"content-type"];
655 ctype = [NGMimeType mimeType:@"application/octet-stream"];
661 if ([body isKindOfClass:[NSData class]]) {
662 [response setContent:body];
664 else if ([body isKindOfClass:[NSString class]]) {
667 data = [body dataUsingEncoding:[response contentEncoding]];
669 [response setContent:data];
672 /* generate data from structured body .. */
673 NGMimeBodyGenerator *gen;
676 gen = [[[NGMimeBodyGenerator alloc] init] autorelease];
677 data = [gen generateBodyOfPart:body
678 additionalHeaders:nil
680 [response setContent:data];
683 { /* transfer headers */
687 names = [mresponse headerFieldNames];
688 while ((name = [names nextObject])) {
689 NSEnumerator *values;
692 if ([name isEqualToString:@"content-type"])
694 if ([name isEqualToString:@"set-cookie"])
697 values = [mresponse valuesOfHeaderFieldWithName:name];
698 while ((value = [values nextObject])) {
699 value = [value stringValue];
700 [response appendHeader:value forKey:name];
705 { /* transfer cookies */
706 NSEnumerator *cookies;
707 NGHttpCookie *mcookie;
709 cookies = [mresponse valuesOfHeaderFieldWithName:@"set-cookie"];
711 while ((mcookie = [cookies nextObject])) {
714 if (![mcookie isKindOfClass:[NGHttpCookie class]]) {
716 woCookie = [WOCookie cookieWithString:[mcookie stringValue]];
719 woCookie = [WOCookie cookieWithName:[mcookie cookieName]
720 value:[mcookie value]
722 domain:[mcookie domainName]
723 expires:[mcookie expireDate]
724 isSecure:[mcookie needsSecureChannel]];
726 if (woCookie == nil) {
728 @"Couldn't create WOCookie from NGHttp cookie: %@",
730 // could not create cookie
734 [self debugWithFormat:@"adding cookie: %@", woCookie];
736 [response addCookie:woCookie];
742 [self logResponse:response data:[self->log readLog]];
743 [self->log resetReadLog];
746 [self debugWithFormat:@"processed response: %@", response];
748 /* check keep-alive */
752 conn = [response headerForKey:@"connection"];
753 conn = [conn lowercaseString];
755 if ([conn isEqualToString:@"close"]) {
756 [self setKeepAliveEnabled:NO];
759 else if ([conn isEqualToString:@"keep-alive"]) {
760 [self setKeepAliveEnabled:YES];
763 [self setKeepAliveEnabled:NO];
771 - (void)setKeepAliveEnabled:(BOOL)_flag {
772 self->keepAlive = _flag;
774 - (BOOL)keepAliveEnabled {
775 return self->keepAlive;
780 - (void)setConnectTimeout:(int)_seconds {
781 self->connectTimeout = _seconds;
783 - (int)connectTimeout {
784 return self->connectTimeout;
787 - (void)setReceiveTimeout:(int)_seconds {
788 self->receiveTimeout = _seconds;
790 - (int)receiveTimeout {
791 return self->receiveTimeout;
794 - (void)setSendTimeout:(int)_seconds {
795 self->sendTimeout = _seconds;
798 return self->sendTimeout;
803 - (NSString *)description {
804 NSMutableString *str;
806 str = [NSMutableString stringWithCapacity:128];
807 [str appendFormat:@"<%@[0x%08X]:", NSStringFromClass([self class]), self];
809 if (self->url) [str appendFormat:@" url=%@", self->url];
810 if (self->useProxy) [str appendString:@" proxy"];
811 if (self->useSSL) [str appendString:@" SSL"];
813 if (self->socket) [str appendFormat:@" socket=%@", self->socket];
815 [str appendString:@">"];
819 @end /* WOHTTPConnection */
821 @implementation NSURL(SocketAddress)
823 - (id)socketAddressForURL {
828 if ([s isEqualToString:@"http"]) {
832 if ([s length] == 0) s = @"localhost";
833 p = [[self port] intValue];
835 return [NGInternetSocketAddress addressWithPort:p == 0 ? 80 : p onHost:s];
837 else if ([s isEqualToString:@"https"]) {
841 if ([s length] == 0) s = @"localhost";
842 p = [[self port] intValue];
844 return [NGInternetSocketAddress addressWithPort:p == 0 ? 443 : p onHost:s];
846 else if ([s isEqualToString:@"unix"] || [s isEqualToString:@"file"]) {
847 return [NGLocalSocketAddress addressWithPath:[self path]];
852 - (BOOL)shouldUseWOProxyServer {
853 if ([[self scheme] hasPrefix:@"http"]) {
856 if ((h = [self host]) == nil)
859 if ([h isEqualToString:@"127.0.0.1"])
861 if ([h isEqualToString:@"localhost"])
864 if ([[WOHTTPConnection proxyServer] length] > 0) {
870 e = [[WOHTTPConnection noProxySuffixes] objectEnumerator];
871 while ((suffix = [e nextObject])) {
872 if ([h hasSuffix:suffix]) {
883 @end /* NSURL(SocketAddress) */