2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo 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 OGo 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 OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
23 #include <NGObjWeb/WOHTTPConnection.h>
24 #include <NGObjWeb/WOCookie.h>
25 #include <NGObjWeb/WORequest.h>
26 #include <NGObjWeb/WOResponse.h>
27 #include <NGObjWeb/WOCookie.h>
28 #include <NGObjWeb/WORunLoop.h>
29 #include <NGStreams/NGStreams.h>
30 #include <NGStreams/NGCTextStream.h>
31 #include <NGStreams/NGBufferedStream.h>
32 #include <NGStreams/NGNet.h>
33 #include <NGHttp/NGHttp.h>
34 #include <NGMime/NGMime.h>
35 #import <Foundation/Foundation.h>
36 #include "WOSimpleHTTPParser.h"
37 #include "WOHttpAdaptor/WORecordRequestStream.h"
39 @interface WOHTTPConnection(Privates)
42 - (void)_unregisterNotification;
45 @interface WOCookie(Privates)
46 + (id)cookieWithString:(NSString *)_string;
49 NSString *WOHTTPConnectionCanReadResponse = @"WOHTTPConnectionCanReadResponse";
51 @interface NSURL(SocketAddress)
52 - (id)socketAddressForURL;
53 - (BOOL)shouldUseWOProxyServer;
56 @interface WOHTTPConnection(Privates2)
57 + (NSString *)proxyServer;
58 + (NSURL *)proxyServerURL;
59 + (NSArray *)noProxySuffixes;
62 @implementation WOHTTPConnection
64 static Class SSLSocketClass = Nil;
65 static BOOL useSimpleParser = YES;
66 static NSString *proxyServer = nil;
67 static NSArray *noProxy = nil;
68 static BOOL doDebug = NO;
69 static BOOL logStream = NO;
76 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
77 static BOOL didInit = NO;
81 useSimpleParser = [ud boolForKey:@"WOHTTPConnectionUseSimpleParser"];
82 proxyServer = [ud stringForKey:@"WOProxyServer"];
83 noProxy = [ud arrayForKey:@"WONoProxySuffixes"];
84 doDebug = [ud boolForKey:@"WODebugHTTPConnection"];
85 logStream = [ud boolForKey:@"WODebugHTTPConnectionLogStream"];
88 + (NSString *)proxyServer {
91 + (NSURL *)proxyServerURL {
94 ps = [self proxyServer];
98 return [NSURL URLWithString:ps];
100 + (NSArray *)noProxySuffixes {
104 - (id)initWithNSURL:(NSURL *)_url {
105 if ((self = [super init])) {
106 self->url = [_url retain];
107 self->useSSL = [[_url scheme] isEqualToString:@"https"];
108 self->useProxy = [_url shouldUseWOProxyServer];
111 static BOOL didCheck = NO;
114 SSLSocketClass = NSClassFromString(@"NGActiveSSLSocket");
121 - (id)initWithURL:(id)_url {
124 /* create an NSURL object if necessary */
125 lurl = [_url isKindOfClass:[NSURL class]]
127 : [NSURL URLWithString:[_url stringValue]];
130 [self logWithFormat:@"could not construct URL from object '%@' !", _url];
135 [self logWithFormat:@"init with URL: %@", lurl];
136 return [self initWithNSURL:lurl];
139 - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port
144 s = [NSString stringWithFormat:@"http%s://%@:%i/",
145 _flag ? "s" : "", _hostName,
146 _port == 0 ? (_flag?443:80) : _port];
147 return [self initWithURL:s];
149 - (id)initWithHost:(NSString *)_hostName onPort:(unsigned int)_port {
150 return [self initWithHost:_hostName onPort:_port secure:NO];
153 return [self initWithHost:@"localhost" onPort:80 secure:NO];
157 [self _unregisterNotification];
158 [self->lastException release];
161 [self->socket release];
168 - (NSException *)lastException {
169 return self->lastException;
172 - (BOOL)isDebuggingEnabled {
173 return doDebug ? YES : NO;
175 - (NSString *)loggingPrefix {
176 /* improve perf ... */
178 return [NSString stringWithFormat:@"WOHTTP[0x%08X]<%@>",
179 self, [self->url absoluteString]];
182 return [NSString stringWithFormat:@"WOHTTP[0x%08X]", self];
187 - (NSString *)hostName {
188 return [self->url host];
194 id<NGSocketAddress> address;
199 NSAssert(self->socket == nil, @"socket still available after disconnect");
200 NSAssert(self->io == nil, @"IO stream still available after disconnect");
204 if (SSLSocketClass == Nil) {
205 /* no SSL support is available */
206 static BOOL didLog = NO;
209 NSLog(@"NOTE: SSL support is not available !");
215 if (self->useProxy) {
218 purl = [[self class] proxyServerURL];
219 address = [purl socketAddressForURL];
222 address = [self->url socketAddressForURL];
224 if (address == nil) {
225 [self debugWithFormat:@"got no address for connect .."];
230 self->socket = self->useSSL
231 ? [SSLSocketClass socketConnectedToAddress:address]
232 : [NGActiveSocket socketConnectedToAddress:address];
236 fprintf(stderr, "couldn't create socket: %s\n",
237 [[localException description] cString]);
239 ASSIGN(self->lastException, localException);
244 if (self->socket == nil) {
245 [self debugWithFormat:@"socket is not setup: %@", [self lastException]];
249 if (![self->socket isConnected]) {
251 [self debugWithFormat:@"socket is not connected .."];
255 self->socket = [self->socket retain];
257 [(NGActiveSocket *)self->socket setSendTimeout:[self sendTimeout]];
258 [(NGActiveSocket *)self->socket setReceiveTimeout:[self receiveTimeout]];
263 bStr = [[NGBufferedStream alloc] initWithSource:self->socket];
265 self->log = [[WORecordRequestStream alloc] initWithSource:bStr];
270 [[NGCTextStream alloc] initWithSource:(id)(self->log?self->log:bStr)];
271 [bStr release]; bStr = nil;
276 - (void)_disconnect {
277 [self->log release]; self->log = nil;
278 [self->io release]; self->io = nil;
281 (void)[self->socket shutdown];
285 [self->socket release]; self->socket = nil;
288 /* runloop based IO */
290 - (NSNotificationCenter *)notificationCenter {
291 return [NSNotificationCenter defaultCenter];
293 - (NSRunLoop *)runLoop {
294 return [NSRunLoop currentRunLoop];
296 - (NSString *)runLoopMode {
297 return NSDefaultRunLoopMode;
300 - (void)_socketActivated:(NSNotification *)_n {
301 if ([_n object] != self->socket)
305 [self debugWithFormat:@"socket activated ..."];
308 [[self notificationCenter]
309 postNotificationName:WOHTTPConnectionCanReadResponse
313 - (void)_registerForNotification {
316 if (self->didRegisterForNotification)
319 [[self notificationCenter]
320 addObserver:self selector:@selector(_socketActivated:)
321 name:NSFileObjectBecameActiveNotificationName
322 object:self->socket];
325 [rl addFileObject:self->socket
326 activities:(NSPosixReadableActivity|NSPosixExceptionalActivity)
327 forMode:[self runLoopMode]];
329 - (void)_unregisterNotification {
330 if (!self->didRegisterForNotification)
333 [[self notificationCenter] removeObserver:self];
335 [[self runLoop] removeFileObject:self->socket
336 forMode:[self runLoopMode]];
341 - (void)logRequest:(WORequest *)_response data:(NSData *)_data {
342 if (_data == nil) return;
345 NSLog(@"request is\n");
347 fwrite([_data bytes], 1, [_data length], stderr);
349 fprintf(stderr,"\n");
353 - (void)logResponse:(WOResponse *)_response data:(NSData *)_data {
354 if (_data == nil) return;
357 NSLog(@"response is\n");
359 fwrite([_data bytes], 1, [_data length], stderr);
361 fprintf(stderr,"\n");
366 /* sending/receiving HTTP */
368 - (BOOL)sendRequest:(WORequest *)_request {
373 [self debugWithFormat:@"send request: %@", _request];
375 if (![self->socket isConnected]) {
376 if (![self _connect]) {
377 /* could not connect */
379 [self debugWithFormat:@" could not connect socket"];
385 content = [_request content];
387 /* write request line (eg 'GET / HTTP/1.0') */
389 [self debugWithFormat:@" method: %@", [_request method]];
391 if (isok) isok = [self->io writeString:[_request method]];
392 if (isok) isok = [self->io writeString:@" "];
394 if (self->useProxy) {
396 // TODO: check whether this produces a '//' (may need to strip uri)
397 isok = [self->io writeString:[self->url absoluteString]];
398 [self debugWithFormat:@" wrote proxy start ..."];
400 if (isok) isok = [self->io writeString:[_request uri]];
402 if (isok) isok = [self->io writeString:@" "];
403 if (isok) isok = [self->io writeString:[_request httpVersion]];
404 if (isok) isok = [self->io writeString:@"\r\n"];
406 /* set content-length header */
408 if ([content length] > 0) {
409 [_request setHeader:[NSString stringWithFormat:@"%d", [content length]]
410 forKey:@"content-length"];
413 if ([[self->url scheme] hasPrefix:@"http"]) {
416 if (isok) isok = [self->io writeString:@"Host: "];
417 if (isok) isok = [self->io writeString:[self hostName]];
418 if (isok) isok = [self->io writeString:@"\r\n"];
419 [self debugWithFormat:@" wrote host header: %@", [self hostName]];
422 /* write request headers */
425 NSEnumerator *fields;
429 fields = [[_request headerKeys] objectEnumerator];
431 while (isok && (fieldName = [fields nextObject])) {
432 NSEnumerator *values;
435 if ([fieldName length] == 4) {
436 if ([fieldName isEqualToString:@"host"])
437 /* did already write host ... */
439 if ([fieldName isEqualToString:@"Host"])
440 /* did already write host ... */
444 values = [[_request headersForKey:fieldName] objectEnumerator];
446 while ((value = [values nextObject]) && isok) {
447 if (isok) isok = [self->io writeString:fieldName];
448 if (isok) isok = [self->io writeString:@": "];
449 if (isok) isok = [self->io writeString:value];
450 if (isok) isok = [self->io writeString:@"\r\n"];
454 [self debugWithFormat:@" wrote %i request headers ...", cnt];
457 /* write some required headers */
459 if ([_request headerForKey:@"accept"] == nil) {
460 if (isok) isok = [self->io writeString:@"Accept: */*\r\n"];
461 [self debugWithFormat:@" wrote accept header ..."];
463 if ([_request headerForKey:@"user-agent"] == nil) {
464 if (isok) isok = [self->io writeString:@"User-Agent: SOPE/4.2\r\n"];
465 [self debugWithFormat:@" wrote user-agent header ..."];
468 /* write cookie headers */
470 if ([[_request cookies] count] > 0 && isok) {
471 NSEnumerator *cookies;
476 [self->io writeString:@"set-cookie: "];
478 cookies = [[_request cookies] objectEnumerator];
480 while (isok && (cookie = [cookies nextObject])) {
481 if (isFirst) isFirst = NO;
482 else if (isok) isok = [self->io writeString:@"; "];
484 if (isok) isok = [self->io writeString:[cookie stringValue]];
487 if (isok) isok = [self->io writeString:@"\r\n"];
488 [self debugWithFormat:@" wrote %i cookies ...", cnt];
491 /* flush request header on socket */
493 if (isok) isok = [self->io writeString:@"\r\n"];
494 if (isok) isok = [self->io flush];
495 [self debugWithFormat:@" flushed HTTP header."];
499 if ([content length] > 0) {
500 [self debugWithFormat:@" writing HTTP entity (length=%i).",
503 if ([content isKindOfClass:[NSString class]]) {
504 if (isok) isok = [self->io writeString:(NSString *)content];
506 else if ([content isKindOfClass:[NSData class]]) {
507 if (isok) isok = [[self->io source]
508 safeWriteBytes:[content bytes]
509 count:[content length]];
512 if (isok) isok = [self->io writeString:[content description]];
514 if (isok) isok = [self->io flush];
517 [self debugWithFormat:@" no HTTP entity to write ..."];
521 [self logRequest:_request data:[self->log writeLog]];
522 [self->log resetWriteLog];
524 [self debugWithFormat:@"=> finished:\n url: %@\n sock: %@",
525 self->url, self->socket];
527 ASSIGN(self->lastException, [self->socket lastException]);
528 [self->socket shutdown];
532 if (![self->socket isConnected])
535 [self _registerForNotification];
540 - (NSException *)handleResponseParsingError:(NSException *)_exception {
541 fprintf(stderr, "%s: caught: %s\n",
543 [[_exception description] cString]);
547 - (WOResponse *)readResponse {
548 /* TODO: split up method */
549 WOResponse *response;
552 [self _unregisterNotification];
554 if (self->socket == nil) {
555 [self debugWithFormat:@"no socket available for reading response ..."];
559 [self debugWithFormat:@"parsing response from socket: %@", self->socket];
561 if (useSimpleParser) {
562 WOSimpleHTTPParser *parser;
564 [self debugWithFormat:@" using simple HTTP parser ..."];
566 parser = [[WOSimpleHTTPParser alloc] initWithStream:[self->io source]];
569 parser = [parser autorelease];
571 if ((response = [parser parseResponse]) == nil) {
573 [self debugWithFormat:@"parsing failed: %@", [parser lastException]];
577 NGHttpMessageParser *parser;
578 NGHttpResponse *mresponse;
584 if ((parser = [[[NGHttpMessageParser alloc] init] autorelease]) == nil)
587 [self debugWithFormat:@" using MIME HTTP parser (complex parser) ..."];
590 [parser setDelegate:self];
591 mresponse = [parser parseResponseFromStream:self->socket];
594 [[self handleResponseParsingError:localException] raise];
597 [self debugWithFormat:@"finished parsing response: %@", mresponse];
599 /* transform parsed MIME response to WOResponse */
601 body = [mresponse body];
602 if (body == nil) body = [NSData data];
604 response = [[[WOResponse alloc] init] autorelease];
605 [response setHTTPVersion:[mresponse httpVersion]];
606 [response setStatus:[mresponse statusCode]];
607 [response setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys:
608 self, @"NGHTTPConnection",
609 mresponse, @"NGMimeResponse",
613 { /* check content-type */
616 value = [[mresponse valuesOfHeaderFieldWithName:@"content-type"]
621 ctype = [NGMimeType mimeType:[value stringValue]];
622 charset = [[ctype valueOfParameter:@"charset"] lowercaseString];
624 if ([charset length] == 0) {
625 /* autodetect charset ... */
627 if ([[ctype type] isEqualToString:@"text"]) {
628 if ([[ctype subType] isEqualToString:@"xml"]) {
629 /* default XML encoding is UTF-8 */
630 [response setContentEncoding:NSUTF8StringEncoding];
635 NSStringEncoding enc;
637 enc = [NGMimeType stringEncodingForCharset:charset];
638 [response setContentEncoding:enc];
641 [response setHeader:[ctype stringValue] forKey:@"content-type"];
645 ctype = [NGMimeType mimeType:@"application/octet-stream"];
651 if ([body isKindOfClass:[NSData class]]) {
652 [response setContent:body];
654 else if ([body isKindOfClass:[NSString class]]) {
657 data = [body dataUsingEncoding:[response contentEncoding]];
659 [response setContent:data];
662 /* generate data from structured body .. */
663 NGMimeBodyGenerator *gen;
666 gen = [[[NGMimeBodyGenerator alloc] init] autorelease];
667 data = [gen generateBodyOfPart:body
668 additionalHeaders:nil
670 [response setContent:data];
673 { /* transfer headers */
677 names = [mresponse headerFieldNames];
678 while ((name = [names nextObject])) {
679 NSEnumerator *values;
682 if ([name isEqualToString:@"content-type"])
684 if ([name isEqualToString:@"set-cookie"])
687 values = [mresponse valuesOfHeaderFieldWithName:name];
688 while ((value = [values nextObject])) {
689 value = [value stringValue];
690 [response appendHeader:value forKey:name];
695 { /* transfer cookies */
696 NSEnumerator *cookies;
697 NGHttpCookie *mcookie;
699 cookies = [mresponse valuesOfHeaderFieldWithName:@"set-cookie"];
701 while ((mcookie = [cookies nextObject])) {
704 if (![mcookie isKindOfClass:[NGHttpCookie class]]) {
706 woCookie = [WOCookie cookieWithString:[mcookie stringValue]];
709 woCookie = [WOCookie cookieWithName:[mcookie cookieName]
710 value:[mcookie value]
712 domain:[mcookie domainName]
713 expires:[mcookie expireDate]
714 isSecure:[mcookie needsSecureChannel]];
716 if (woCookie == nil) {
718 @"Couldn't create WOCookie from NGHttp cookie: %@",
720 // could not create cookie
724 [self debugWithFormat:@"adding cookie: %@", woCookie];
726 [response addCookie:woCookie];
732 [self logResponse:response data:[self->log readLog]];
733 [self->log resetReadLog];
736 [self debugWithFormat:@"processed response: %@", response];
738 /* check keep-alive */
742 conn = [response headerForKey:@"connection"];
743 conn = [conn lowercaseString];
745 if ([conn isEqualToString:@"close"]) {
746 [self setKeepAliveEnabled:NO];
749 else if ([conn isEqualToString:@"keep-alive"]) {
750 [self setKeepAliveEnabled:YES];
753 [self setKeepAliveEnabled:NO];
761 - (void)setKeepAliveEnabled:(BOOL)_flag {
762 self->keepAlive = _flag;
764 - (BOOL)keepAliveEnabled {
765 return self->keepAlive;
770 - (void)setConnectTimeout:(int)_seconds {
771 self->connectTimeout = _seconds;
773 - (int)connectTimeout {
774 return self->connectTimeout;
777 - (void)setReceiveTimeout:(int)_seconds {
778 self->receiveTimeout = _seconds;
780 - (int)receiveTimeout {
781 return self->receiveTimeout;
784 - (void)setSendTimeout:(int)_seconds {
785 self->sendTimeout = _seconds;
788 return self->sendTimeout;
793 - (NSString *)description {
794 NSMutableString *str;
796 str = [NSMutableString stringWithCapacity:128];
797 [str appendFormat:@"<%@[0x%08X]:", NSStringFromClass([self class]), self];
799 if (self->url) [str appendFormat:@" url=%@", self->url];
800 if (self->useProxy) [str appendString:@" proxy"];
801 if (self->useSSL) [str appendString:@" SSL"];
803 if (self->socket) [str appendFormat:@" socket=%@", self->socket];
805 [str appendString:@">"];
809 @end /* WOHTTPConnection */
811 @implementation NSURL(SocketAddress)
813 - (id)socketAddressForURL {
818 if ([s isEqualToString:@"http"]) {
822 if ([s length] == 0) s = @"localhost";
823 p = [[self port] intValue];
825 return [NGInternetSocketAddress addressWithPort:p == 0 ? 80 : p onHost:s];
827 else if ([s isEqualToString:@"https"]) {
831 if ([s length] == 0) s = @"localhost";
832 p = [[self port] intValue];
834 return [NGInternetSocketAddress addressWithPort:p == 0 ? 443 : p onHost:s];
836 else if ([s isEqualToString:@"unix"] || [s isEqualToString:@"file"]) {
837 return [NGLocalSocketAddress addressWithPath:[self path]];
842 - (BOOL)shouldUseWOProxyServer {
843 if ([[self scheme] hasPrefix:@"http"]) {
846 if ((h = [self host]) == nil)
849 if ([h isEqualToString:@"127.0.0.1"])
851 if ([h isEqualToString:@"localhost"])
854 if ([[WOHTTPConnection proxyServer] length] > 0) {
860 e = [[WOHTTPConnection noProxySuffixes] objectEnumerator];
861 while ((suffix = [e nextObject])) {
862 if ([h hasSuffix:suffix]) {
873 @end /* NSURL(SocketAddress) */