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 "WOSimpleHTTPParser.h"
24 #include <NGObjWeb/WOResponse.h>
25 #include <NGObjWeb/WORequest.h>
28 @implementation WOSimpleHTTPParser
30 static Class NSStringClass = Nil;
31 static BOOL debugOn = NO;
32 static BOOL heavyDebugOn = NO;
33 static int fileIOBoundary = 16384;
34 static int maxUploadSize = 256 * 1024; /* 256MB */
40 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
42 debugOn = [ud boolForKey:@"WOSimpleHTTPParserDebugEnabled"];
43 heavyDebugOn = [ud boolForKey:@"WOSimpleHTTPParserHeavyDebugEnabled"];
44 fileIOBoundary = [ud integerForKey:@"WOSimpleHTTPParserFileIOBoundary"];
45 maxUploadSize = [ud integerForKey:@"WOSimpleHTTPParserMaxUploadSizeInKB"];
48 NSLog(@"WOSimpleHTTPParser: max-upload-size: %dKB", maxUploadSize);
49 NSLog(@"WOSimpleHTTPParser: file-IO boundary: %d", fileIOBoundary);
53 - (id)initWithStream:(id<NGStream>)_stream {
54 if (NSStringClass == Nil) NSStringClass = [NSString class];
56 if ((self = [super init])) {
57 if ((self->io = [_stream retain]) == nil) {
62 self->readBytes = (void *)
63 [(NSObject *)self->io methodForSelector:@selector(readBytes:count:)];
64 if (self->readBytes == NULL) {
65 NSLog(@"WARNING(%s): got invalid stream object: %@", __PRETTY_FUNCTION__,
84 [self->content release]; self->content = nil;
85 [self->lastException release]; self->lastException = nil;
86 [self->httpVersion release]; self->httpVersion = nil;
87 [self->headers removeAllObjects];
89 if (self->lineBuffer) {
90 free(self->lineBuffer);
91 self->lineBuffer = NULL;
93 self->lineBufSize = 0;
96 /* low-level reading */
98 - (unsigned int)defaultLineSize {
102 - (NSException *)readNextLine {
105 if (self->lineBuffer == NULL) {
106 self->lineBufSize = [self defaultLineSize];
107 self->lineBuffer = malloc(self->lineBufSize + 10);
110 for (i = 0; YES; i++) {
111 register unsigned rc;
114 rc = self->readBytes(self->io, @selector(readBytes:count:), &c, 1);
117 [self debugWithFormat:@"got result %u, exception: %@",
118 rc, [self->io lastException]];
120 return [self->io lastException];
123 /* check buffer capacity */
124 if ((i + 2) > self->lineBufSize) {
125 static int reallocCount = 0;
127 if (reallocCount > 1000) {
128 static BOOL didLog = NO;
131 NSLog(@"WARNING(%s): reallocated the HTTP line buffer %i times, "
132 @"consider increasing the default line buffer size!",
133 __PRETTY_FUNCTION__, reallocCount);
137 if (self->lineBufSize > (56 * 1024)) {
138 /* to avoid DOS attacks ... */
139 return [NSException exceptionWithName:@"HTTPParserHeaderSizeExceeded"
141 @"got a HTTP line of 100KB+ (DoS attack?)!"
145 self->lineBufSize *= 2;
146 self->lineBuffer = realloc(self->lineBuffer, self->lineBufSize + 10);
153 else if (c == '\r') {
160 self->lineBuffer[i] = c;
163 self->lineBuffer[i] = 0; /* 0-terminate buffer */
165 return nil /* nil means: everything OK */;
168 /* common HTTP parsing */
170 static NSString *ContentLengthHeaderName = @"content-length";
172 static NSString *stringForHeaderName(unsigned char *p) {
176 we try to be smart to avoid creation of NSString objects ...
178 register unsigned len;
179 register unsigned char c1;
181 if ((len = strlen(p)) == 0)
190 if (strcasecmp(p, "te") == 0) return @"te";
191 if (strcasecmp(p, "if") == 0) return @"if";
194 if (strcasecmp(p, "via") == 0) return @"via";
195 if (strcasecmp(p, "age") == 0) return @"age";
196 if (strcasecmp(p, "p3p") == 0) return @"p3p";
201 if (strcasecmp(p, "date") == 0) return @"date";
204 if (strcasecmp(p, "etag") == 0) return @"etag";
207 if (strcasecmp(p, "from") == 0) return @"from";
210 if (strcasecmp(p, "host") == 0) return @"host";
213 if (strcasecmp(p, "vary") == 0) return @"vary";
218 if (strcasecmp(p, "allow") == 0) return @"allow";
219 if (strcasecmp(p, "brief") == 0) return @"brief";
220 if (strcasecmp(p, "range") == 0) return @"range";
221 if (strcasecmp(p, "depth") == 0) return @"depth";
222 if (strcasecmp(p, "ua-os") == 0) return @"ua-os"; /* Entourage */
227 if (strcasecmp(p, "accept") == 0) return @"accept";
230 if (strcasecmp(p, "cookie") == 0) return @"cookie";
233 if (strcasecmp(p, "expect") == 0) return @"expect";
236 if (strcasecmp(p, "pragma") == 0) return @"pragma";
239 if (strcasecmp(p, "server") == 0) return @"server";
242 if (strcasecmp(p, "ua-cpu") == 0) return @"ua-cpu"; /* Entourage */
252 if (strcasecmp(p, "accept-charset") == 0) return @"accept-charset";
253 if (strcasecmp(p, "accept-encoding") == 0) return @"accept-encoding";
254 if (strcasecmp(p, "accept-language") == 0) return @"accept-language";
255 if (strcasecmp(p, "accept-ranges") == 0) return @"accept-ranges";
257 else if (strcasecmp(p, "authorization") == 0)
258 return @"authorization";
265 if (strcasecmp(p, "content-length") == 0)
266 return ContentLengthHeaderName;
268 if (strcasecmp(p, "content-type") == 0) return @"content-type";
269 if (strcasecmp(p, "content-md5") == 0) return @"content-md5";
270 if (strcasecmp(p, "content-range") == 0) return @"content-range";
272 if (strcasecmp(p, "content-encoding") == 0)
273 return @"content-encoding";
274 if (strcasecmp(p, "content-language") == 0)
275 return @"content-language";
277 if (strcasecmp(p, "content-location") == 0)
278 return @"content-location";
279 if (strcasecmp(p, "content-class") == 0) /* Entourage */
280 return @"content-class";
282 else if (strcasecmp(p, "call-back") == 0)
286 if (strcasecmp(p, "connection") == 0) return @"connection";
287 if (strcasecmp(p, "cache-control") == 0) return @"cache-control";
292 if (strcasecmp(p, "destination") == 0) return @"destination";
293 if (strcasecmp(p, "destroy") == 0) return @"destroy";
297 if (strcasecmp(p, "expires") == 0) return @"expires";
298 if (strcasecmp(p, "extension") == 0) return @"extension"; /* Entourage */
302 if (strcasecmp(p, "if-modified-since") == 0)
303 return @"if-modified-since";
304 if (strcasecmp(p, "if-none-match") == 0) /* Entourage */
305 return @"if-none-match";
306 if (strcasecmp(p, "if-match") == 0)
311 if (strcasecmp(p, "keep-alive") == 0) return @"keep-alive";
315 if (strcasecmp(p, "last-modified") == 0) return @"last-modified";
316 if (strcasecmp(p, "location") == 0) return @"location";
317 if (strcasecmp(p, "lock-token") == 0) return @"lock-token";
321 if (strcasecmp(p, "ms-webstorage") == 0) return @"ms-webstorage";
322 if (strcasecmp(p, "max-forwards") == 0) return @"max-forwards";
328 if (strcasecmp(p, "notification-delay") == 0)
329 return @"notification-delay";
330 if (strcasecmp(p, "notification-type") == 0)
331 return @"notification-type";
338 if (strcasecmp(p, "overwrite") == 0)
345 if (strcasecmp(p, "proxy-connection") == 0)
346 return @"proxy-connection";
352 if (strcasecmp(p, "referer") == 0) return @"referer";
359 if (strcasecmp(p, "subscription-lifetime") == 0)
360 return @"subscription-lifetime";
363 if (strcasecmp(p, "subscription-id") == 0)
364 return @"subscription-id";
367 if (strcasecmp(p, "set-cookie") == 0)
368 return @"set-cookie";
374 if (strcasecmp(p, "transfer-encoding") == 0) return @"transfer-encoding";
375 if (strcasecmp(p, "translate") == 0) return @"translate";
376 if (strcasecmp(p, "trailer") == 0) return @"trailer";
377 if (strcasecmp(p, "timeout") == 0) return @"timeout";
381 if (strcasecmp(p, "user-agent") == 0) return @"user-agent";
385 if (strcasecmp(p, "www-authenticate") == 0) return @"www-authenticate";
386 if (strcasecmp(p, "warning") == 0) return @"warning";
390 if ((p[2] == 'w') && (len > 22)) {
391 if (strstr(p, "x-webobjects-") == (void *)p) {
392 p += 13; /* skip x-webobjects- */
393 if (strcmp(p, "server-protocol") == 0)
394 return @"x-webobjects-server-protocol";
395 else if (strcmp(p, "server-protocol") == 0)
396 return @"x-webobjects-server-protocol";
397 else if (strcmp(p, "remote-addr") == 0)
398 return @"x-webobjects-remote-addr";
399 else if (strcmp(p, "remote-host") == 0)
400 return @"x-webobjects-remote-host";
401 else if (strcmp(p, "server-name") == 0)
402 return @"x-webobjects-server-name";
403 else if (strcmp(p, "server-port") == 0)
404 return @"x-webobjects-server-port";
405 else if (strcmp(p, "server-url") == 0)
406 return @"x-webobjects-server-url";
410 if (strcasecmp(p, "x-cache") == 0)
413 else if (len == 12) {
414 if (strcasecmp(p, "x-powered-by") == 0)
415 return @"x-powered-by";
417 if (strcasecmp(p, "x-zidestore-name") == 0)
418 return @"x-zidestore-name";
419 if (strcasecmp(p, "x-forwarded-for") == 0)
420 return @"x-forwarded-for";
421 if (strcasecmp(p, "x-forwarded-host") == 0)
422 return @"x-forwarded-host";
423 if (strcasecmp(p, "x-forwarded-server") == 0)
424 return @"x-forwarded-server";
430 NSLog(@"making custom header name '%s'!", p);
432 /* make name lowercase (we own the buffer, so we can work on it) */
436 for (t = p; *t != '\0'; t++)
439 return [[NSString alloc] initWithCString:p];
442 - (NSException *)parseHeader {
443 NSException *e = nil;
445 while ((e = [self readNextLine]) == nil) {
446 unsigned char *p, *v;
448 NSString *headerName;
449 NSString *headerValue;
452 printf("read header line: '%s'\n", self->lineBuffer);
454 if (strlen(self->lineBuffer) == 0) {
455 /* found end of header */
459 p = self->lineBuffer;
461 if (*p == ' ' || *p == '\t') {
462 // TODO: implement folding (remember last header-key, add string)
464 @"ERROR(%s): got a folded HTTP header line, cannot process!",
465 __PRETTY_FUNCTION__];
469 /* find key/value separator */
470 if ((v = index(p, ':')) == NULL) {
471 [self logWithFormat:@"WARNING: got malformed header line: '%s'",
476 *v = '\0'; v++; /* now 'p' points to name and 'v' to value */
478 /* skip leading spaces */
479 while (*v != '\0' && (*v == ' ' || *v == '\t'))
483 /* trim trailing spaces */
484 for (idx = strlen(v) - 1; idx >= 0; idx--) {
485 if ((v[idx] != ' ' && v[idx] != '\t'))
492 headerName = stringForHeaderName(p);
493 headerValue = [[NSStringClass alloc] initWithCString:v];
495 if (headerName == ContentLengthHeaderName)
496 self->clen = atoi(v);
498 if (headerName != nil || headerValue != nil) {
499 if (self->headers == nil)
500 self->headers = [[NSMutableDictionary alloc] initWithCapacity:32];
502 [self->headers setObject:headerValue forKey:headerName];
505 [headerValue release];
506 [headerName release];
512 - (NSException *)parseEntityOfMethod:(NSString *)_method {
514 TODO: several cases are caught:
515 a) content-length = 0 => empty data
516 b) content-length small => read into memory
517 c) content-length large => streamed into the filesystem to safe RAM
518 d) content-length unknown => ??
521 if (self->clen == 0) {
524 else if (self->clen < 0) {
525 /* I think HTTP/1.1 requires a content-length header to be present ? */
527 if ([self->httpVersion isEqualToString:@"HTTP/1.0"] ||
528 [self->httpVersion isEqualToString:@"HTTP/0.9"]) {
529 /* content-length unknown, read till EOF */
530 BOOL readToEOF = YES;
532 if ([_method isEqualToString:@"HEAD"])
534 else if ([_method isEqualToString:@"GET"])
536 else if ([_method isEqualToString:@"DELETE"])
541 @"WARNING: not processing entity of request "
542 @"without contentlen!"];
546 else if (self->clen > maxUploadSize*1024) {
547 /* entity is too large */
548 return [NSException exceptionWithName:@"LimitException"
550 @"the maximum HTTP transaction size was exceeded"
553 else if (self->clen > fileIOBoundary) {
554 /* we are streaming the content to a file and use a memory mapped data */
562 [self debugWithFormat:@"streaming %i bytes into file ...", self->clen];
564 fn = [[NSProcessInfo processInfo] temporaryFileName];
566 if ((t = fopen([fn cString], "w")) == NULL) {
567 [self logWithFormat:@"ERROR: could not open temporary file '%@'!", fn];
569 /* read into memory as a fallback ... */
572 [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
573 if (self->content == nil)
574 return [self->io lastException];
578 for (toGo = self->clen; toGo > 0; ) {
579 unsigned readCount, writeCount;
581 /* read from socket */
582 readCount = [self->io readBytes:buf count:sizeof(buf)];
583 if (readCount == NGStreamError) {
591 if ((writeCount = fwrite(buf, readCount, 1, t)) != 1) {
594 writeError = ferror(t);
601 unlink([fn cString]); /* delete temporary file */
603 if (writeError == 0) {
604 return [NSException exceptionWithName:@"SystemWriteError"
605 reason:@"failed to write data to upload file"
609 return [self->io lastException];
612 self->content = [[NSData alloc] initWithContentsOfMappedFile:fn];
613 unlink([fn cString]); /* if the mmap disappears, the storage is freed */
616 /* content-length known and small */
617 //[self logWithFormat:@"reading %i bytes of the entity", self->clen];
620 [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
621 if (self->content == nil)
622 return [self->io lastException];
624 //[self logWithFormat:@"read %i bytes.", [self->content length]];
630 /* handling expectations */
632 - (BOOL)processContinueExpectation {
633 // TODO: this should check the credentials of a request before accepting the
634 // body. The current implementation is far from optimal and only added
635 // for Mono compatibility (and actually produces the same behaviour
636 // like with HTTP/1.0 ...)
637 static unsigned char *contStatLine =
638 "HTTP/1.0 100 Continue\r\n"
639 "content-length: 0\r\n"
641 static unsigned char *failStatLine =
642 "HTTP/1.0 417 Expectation Failed\r\n"
643 "content-length: 0\r\n"
645 unsigned char *respline = NULL;
648 [self debugWithFormat:@"process 100 continue on IO: %@", self->io];
650 if (self->clen > 0 && (self->clen > (maxUploadSize * 1024))) {
651 // TODO: return a 417 expectation failed
653 respline = failStatLine;
657 respline = contStatLine;
660 if (![self->io safeWriteBytes:respline count:strlen(respline)]) {
661 ASSIGN(self->lastException, [self->io lastException]);
664 if (![self->io flush]) {
665 ASSIGN(self->lastException, [self->io lastException]);
674 - (WORequest *)parseRequest {
675 NSException *e = nil;
677 NSString *uri = @"/";
678 NSString *method = @"GET";
683 [self logWithFormat:@"HeavyDebug: parsing response ..."];
685 /* process request line */
687 if ((e = [self readNextLine])) {
688 ASSIGN(self->lastException, e);
692 printf("read request line: '%s'\n", self->lineBuffer);
695 /* sample line: "GET / HTTP/1.0" */
696 unsigned char *p, *t;
700 p = self->lineBuffer;
701 if ((t = index(p, ' ')) == NULL) {
702 [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
708 /* intended fall-throughs ! */
710 if (strcasecmp(p, "BPROPFIND") == 0) { method = @"BPROPFIND"; break; }
711 if (strcasecmp(p, "BPROPPATCH") == 0) { method = @"BPROPPATCH"; break; }
713 if (strcasecmp(p, "COPY") == 0) { method = @"COPY"; break; }
714 if (strcasecmp(p, "CHECKOUT") == 0) { method = @"CHECKOUT"; break; }
715 if (strcasecmp(p, "CHECKIN") == 0) { method = @"CHECKIN"; break; }
717 if (strcasecmp(p, "DELETE") == 0) { method = @"DELETE"; break; }
719 if (strcasecmp(p, "HEAD") == 0) { method = @"HEAD"; break; }
721 if (strcasecmp(p, "LOCK") == 0) { method = @"LOCK"; break; }
723 if (strcasecmp(p, "GET") == 0) { method = @"GET"; break; }
725 if (strcasecmp(p, "MKCOL") == 0) { method = @"MKCOL"; break; }
726 if (strcasecmp(p, "MOVE") == 0) { method = @"MOVE"; break; }
728 if (strcasecmp(p, "NOTIFY") == 0) { method = @"NOTIFY"; break; }
730 if (strcasecmp(p, "OPTIONS") == 0) { method = @"OPTIONS"; break; }
732 if (strcasecmp(p, "PUT") == 0) { method = @"PUT"; break; }
733 if (strcasecmp(p, "POST") == 0) { method = @"POST"; break; }
734 if (strcasecmp(p, "PROPFIND") == 0) { method = @"PROPFIND"; break; }
735 if (strcasecmp(p, "PROPPATCH") == 0) { method = @"PROPPATCH"; break; }
736 if (strcasecmp(p, "POLL") == 0) { method = @"POLL"; break; }
738 if (strcasecmp(p, "REPORT") == 0) { method = @"REPORT"; break; }
740 if (strcasecmp(p, "SEARCH") == 0) { method = @"SEARCH"; break; }
741 if (strcasecmp(p, "SUBSCRIBE") == 0) { method = @"SUBSCRIBE"; break; }
743 if (strcasecmp(p, "UNLOCK") == 0) { method = @"UNLOCK"; break; }
744 if (strcasecmp(p, "UNSUBSCRIBE")== 0) { method = @"UNSUBSCRIBE"; break; }
745 if (strcasecmp(p, "UNCHECKOUT") == 0) { method = @"UNCHECKOUT"; break; }
747 if (strcasecmp(p, "VERSION-CONTROL") == 0) {
748 method = @"VERSION-CONTROL";
754 [self debugWithFormat:@"making custom HTTP method name: '%s'", p];
755 method = [NSString stringWithCString:p];
761 p = t + 1; /* skip space */
762 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
766 [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
770 if ((t = index(p, ' ')) == NULL) {
771 /* the URI isn't followed by a HTTP version */
772 self->httpVersion = @"HTTP/0.9";
773 /* TODO: strip trailing spaces for better compliance */
774 uri = [NSString stringWithCString:p];
778 uri = [NSString stringWithCString:p];
782 p = t + 1; /* skip space */
783 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
787 self->httpVersion = @"HTTP/0.9";
788 else if (strcasecmp(p, "http/1.0") == 0)
789 self->httpVersion = @"HTTP/1.0";
790 else if (strcasecmp(p, "http/1.1") == 0)
791 self->httpVersion = @"HTTP/1.1";
793 /* TODO: strip trailing spaces */
794 self->httpVersion = [[NSString alloc] initWithCString:p];
801 if ((e = [self parseHeader])) {
802 ASSIGN(self->lastException, e);
806 [self logWithFormat:@"parsed header: %@", self->headers];
808 /* check for expectations */
810 if ((expect = [self->headers objectForKey:@"expect"])) {
811 if ([expect rangeOfString:@"100-continue"
812 options:NSCaseInsensitiveSearch].length > 0) {
813 if (![self processContinueExpectation])
821 if ((e = [self parseEntityOfMethod:method])) {
822 ASSIGN(self->lastException, e);
828 [self logWithFormat:@"HeavyDebug: got all .."];
830 r = [[WORequest alloc] initWithMethod:method
832 httpVersion:self->httpVersion
833 headers:self->headers
834 content:self->content
839 [self logWithFormat:@"HeavyDebug: request: %@", r];
841 return [r autorelease];
844 - (WOResponse *)parseResponse {
845 NSException *e = nil;
851 [self logWithFormat:@"HeavyDebug: parsing response ..."];
853 /* process response line */
855 if ((e = [self readNextLine])) {
856 ASSIGN(self->lastException, e);
860 printf("read response line: '%s'\n", self->lineBuffer);
863 /* sample line: "HTTP/1.0 200 OK" */
864 unsigned char *p, *t;
868 p = self->lineBuffer;
869 if ((t = index(p, ' ')) == NULL) {
870 [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
875 if (strcasecmp(p, "http/1.0") == 0)
876 self->httpVersion = @"HTTP/1.0";
877 else if (strcasecmp(p, "http/1.1") == 0)
878 self->httpVersion = @"HTTP/1.1";
880 self->httpVersion = [[NSString alloc] initWithCString:p];
884 p = t + 1; /* skip space */
885 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
888 [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
893 /* we don't need to parse a reason ... */
898 if ((e = [self parseHeader])) {
899 ASSIGN(self->lastException, e);
903 [self logWithFormat:@"parsed header: %@", self->headers];
908 if ((e = [self parseEntityOfMethod:nil /* parsing a response */])) {
909 ASSIGN(self->lastException, e);
915 [self logWithFormat:@"HeavyDebug: got all .."];
917 r = [[[WOResponse alloc] init] autorelease];
919 [r setHTTPVersion:self->httpVersion];
920 [r setHeaders:self->headers];
921 [r setContent:self->content];
926 [self logWithFormat:@"HeavyDebug: response: %@", r];
931 - (NSException *)lastException {
932 return self->lastException;
937 - (BOOL)isDebuggingEnabled {
941 @end /* WOSimpleHTTPParser */