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 "WOSimpleHTTPParser.h"
23 #include <NGObjWeb/WOResponse.h>
24 #include <NGObjWeb/WORequest.h>
27 @implementation WOSimpleHTTPParser
29 static Class NSStringClass = Nil;
30 static BOOL debugOn = NO;
31 static BOOL heavyDebugOn = NO;
32 static int fileIOBoundary = 0;
33 static int maxUploadSize = 0;
39 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
41 debugOn = [ud boolForKey:@"WOSimpleHTTPParserDebugEnabled"];
42 heavyDebugOn = [ud boolForKey:@"WOSimpleHTTPParserHeavyDebugEnabled"];
43 fileIOBoundary = [ud integerForKey:@"WOSimpleHTTPParserFileIOBoundary"];
44 maxUploadSize = [ud integerForKey:@"WOSimpleHTTPParserMaxUploadSizeInKB"];
46 if (maxUploadSize == 0)
47 maxUploadSize = 256 * 1024; /* 256MB */
48 if (fileIOBoundary == 0)
49 fileIOBoundary = 16384;
52 NSLog(@"WOSimpleHTTPParser: max-upload-size: %dKB", maxUploadSize);
53 NSLog(@"WOSimpleHTTPParser: file-IO boundary: %d", fileIOBoundary);
57 - (id)initWithStream:(id<NGStream>)_stream {
58 if (NSStringClass == Nil) NSStringClass = [NSString class];
60 if ((self = [super init])) {
61 if ((self->io = [_stream retain]) == nil) {
66 self->readBytes = (void *)
67 [(NSObject *)self->io methodForSelector:@selector(readBytes:count:)];
68 if (self->readBytes == NULL) {
69 [self warnWithFormat:@"(%s): got invalid stream object: %@",
89 [self->content release]; self->content = nil;
90 [self->lastException release]; self->lastException = nil;
91 [self->httpVersion release]; self->httpVersion = nil;
92 [self->headers removeAllObjects];
94 if (self->lineBuffer) {
95 free(self->lineBuffer);
96 self->lineBuffer = NULL;
98 self->lineBufSize = 0;
101 /* low-level reading */
103 - (unsigned int)defaultLineSize {
107 - (NSException *)readNextLine {
110 if (self->lineBuffer == NULL) {
111 self->lineBufSize = [self defaultLineSize];
112 self->lineBuffer = malloc(self->lineBufSize + 10);
115 for (i = 0; YES; i++) {
116 register unsigned rc;
119 rc = self->readBytes(self->io, @selector(readBytes:count:), &c, 1);
122 [self debugWithFormat:@"got result %u, exception: %@",
123 rc, [self->io lastException]];
125 return [self->io lastException];
128 /* check buffer capacity */
129 if ((i + 2) > self->lineBufSize) {
130 static int reallocCount = 0;
132 if (reallocCount > 1000) {
133 static BOOL didLog = NO;
136 [self warnWithFormat:@"(%s): reallocated the HTTP line buffer %i times, "
137 @"consider increasing the default line buffer size!",
138 __PRETTY_FUNCTION__, reallocCount];
142 if (self->lineBufSize > (56 * 1024)) {
143 /* to avoid DOS attacks ... */
144 return [NSException exceptionWithName:@"HTTPParserHeaderSizeExceeded"
146 @"got a HTTP line of 100KB+ (DoS attack?)!"
150 self->lineBufSize *= 2;
151 self->lineBuffer = realloc(self->lineBuffer, self->lineBufSize + 10);
158 else if (c == '\r') {
165 self->lineBuffer[i] = c;
168 self->lineBuffer[i] = 0; /* 0-terminate buffer */
170 return nil /* nil means: everything OK */;
173 /* common HTTP parsing */
175 static NSString *ContentLengthHeaderName = @"content-length";
177 static NSString *stringForHeaderName(unsigned char *p) {
181 we try to be smart to avoid creation of NSString objects ...
183 register unsigned len;
184 register unsigned char c1;
186 if ((len = strlen(p)) == 0)
195 if (strcasecmp(p, "te") == 0) return @"te";
196 if (strcasecmp(p, "if") == 0) return @"if";
199 if (strcasecmp(p, "via") == 0) return @"via";
200 if (strcasecmp(p, "age") == 0) return @"age";
201 if (strcasecmp(p, "p3p") == 0) return @"p3p";
206 if (strcasecmp(p, "date") == 0) return @"date";
209 if (strcasecmp(p, "etag") == 0) return @"etag";
212 if (strcasecmp(p, "from") == 0) return @"from";
215 if (strcasecmp(p, "host") == 0) return @"host";
218 if (strcasecmp(p, "vary") == 0) return @"vary";
223 if (strcasecmp(p, "allow") == 0) return @"allow";
224 if (strcasecmp(p, "brief") == 0) return @"brief";
225 if (strcasecmp(p, "range") == 0) return @"range";
226 if (strcasecmp(p, "depth") == 0) return @"depth";
227 if (strcasecmp(p, "ua-os") == 0) return @"ua-os"; /* Entourage */
232 if (strcasecmp(p, "accept") == 0) return @"accept";
235 if (strcasecmp(p, "cookie") == 0) return @"cookie";
238 if (strcasecmp(p, "expect") == 0) return @"expect";
241 if (strcasecmp(p, "pragma") == 0) return @"pragma";
244 if (strcasecmp(p, "server") == 0) return @"server";
247 if (strcasecmp(p, "ua-cpu") == 0) return @"ua-cpu"; /* Entourage */
257 if (strcasecmp(p, "accept-charset") == 0) return @"accept-charset";
258 if (strcasecmp(p, "accept-encoding") == 0) return @"accept-encoding";
259 if (strcasecmp(p, "accept-language") == 0) return @"accept-language";
260 if (strcasecmp(p, "accept-ranges") == 0) return @"accept-ranges";
262 else if (strcasecmp(p, "authorization") == 0)
263 return @"authorization";
270 if (strcasecmp(p, "content-length") == 0)
271 return ContentLengthHeaderName;
273 if (strcasecmp(p, "content-type") == 0) return @"content-type";
274 if (strcasecmp(p, "content-md5") == 0) return @"content-md5";
275 if (strcasecmp(p, "content-range") == 0) return @"content-range";
277 if (strcasecmp(p, "content-encoding") == 0)
278 return @"content-encoding";
279 if (strcasecmp(p, "content-language") == 0)
280 return @"content-language";
282 if (strcasecmp(p, "content-location") == 0)
283 return @"content-location";
284 if (strcasecmp(p, "content-class") == 0) /* Entourage */
285 return @"content-class";
287 else if (strcasecmp(p, "call-back") == 0)
291 if (strcasecmp(p, "connection") == 0) return @"connection";
292 if (strcasecmp(p, "cache-control") == 0) return @"cache-control";
297 if (strcasecmp(p, "destination") == 0) return @"destination";
298 if (strcasecmp(p, "destroy") == 0) return @"destroy";
302 if (strcasecmp(p, "expires") == 0) return @"expires";
303 if (strcasecmp(p, "extension") == 0) return @"extension"; /* Entourage */
307 if (strcasecmp(p, "if-modified-since") == 0)
308 return @"if-modified-since";
309 if (strcasecmp(p, "if-none-match") == 0) /* Entourage */
310 return @"if-none-match";
311 if (strcasecmp(p, "if-match") == 0)
316 if (strcasecmp(p, "keep-alive") == 0) return @"keep-alive";
320 if (strcasecmp(p, "last-modified") == 0) return @"last-modified";
321 if (strcasecmp(p, "location") == 0) return @"location";
322 if (strcasecmp(p, "lock-token") == 0) return @"lock-token";
326 if (strcasecmp(p, "ms-webstorage") == 0) return @"ms-webstorage";
327 if (strcasecmp(p, "max-forwards") == 0) return @"max-forwards";
333 if (strcasecmp(p, "notification-delay") == 0)
334 return @"notification-delay";
335 if (strcasecmp(p, "notification-type") == 0)
336 return @"notification-type";
343 if (strcasecmp(p, "overwrite") == 0)
350 if (strcasecmp(p, "proxy-connection") == 0)
351 return @"proxy-connection";
357 if (strcasecmp(p, "referer") == 0) return @"referer";
364 if (strcasecmp(p, "subscription-lifetime") == 0)
365 return @"subscription-lifetime";
368 if (strcasecmp(p, "subscription-id") == 0)
369 return @"subscription-id";
372 if (strcasecmp(p, "set-cookie") == 0)
373 return @"set-cookie";
379 if (strcasecmp(p, "transfer-encoding") == 0) return @"transfer-encoding";
380 if (strcasecmp(p, "translate") == 0) return @"translate";
381 if (strcasecmp(p, "trailer") == 0) return @"trailer";
382 if (strcasecmp(p, "timeout") == 0) return @"timeout";
386 if (strcasecmp(p, "user-agent") == 0) return @"user-agent";
390 if (strcasecmp(p, "www-authenticate") == 0) return @"www-authenticate";
391 if (strcasecmp(p, "warning") == 0) return @"warning";
395 if ((p[2] == 'w') && (len > 22)) {
396 if (strstr(p, "x-webobjects-") == (void *)p) {
397 p += 13; /* skip x-webobjects- */
398 if (strcmp(p, "server-protocol") == 0)
399 return @"x-webobjects-server-protocol";
400 else if (strcmp(p, "server-protocol") == 0)
401 return @"x-webobjects-server-protocol";
402 else if (strcmp(p, "remote-addr") == 0)
403 return @"x-webobjects-remote-addr";
404 else if (strcmp(p, "remote-host") == 0)
405 return @"x-webobjects-remote-host";
406 else if (strcmp(p, "server-name") == 0)
407 return @"x-webobjects-server-name";
408 else if (strcmp(p, "server-port") == 0)
409 return @"x-webobjects-server-port";
410 else if (strcmp(p, "server-url") == 0)
411 return @"x-webobjects-server-url";
415 if (strcasecmp(p, "x-cache") == 0)
418 else if (len == 12) {
419 if (strcasecmp(p, "x-powered-by") == 0)
420 return @"x-powered-by";
422 if (strcasecmp(p, "x-zidestore-name") == 0)
423 return @"x-zidestore-name";
424 if (strcasecmp(p, "x-forwarded-for") == 0)
425 return @"x-forwarded-for";
426 if (strcasecmp(p, "x-forwarded-host") == 0)
427 return @"x-forwarded-host";
428 if (strcasecmp(p, "x-forwarded-server") == 0)
429 return @"x-forwarded-server";
435 NSLog(@"making custom header name '%s'!", p);
437 /* make name lowercase (we own the buffer, so we can work on it) */
441 for (t = p; *t != '\0'; t++)
444 return [[NSString alloc] initWithCString:p];
447 - (NSException *)parseHeader {
448 NSException *e = nil;
450 while ((e = [self readNextLine]) == nil) {
451 unsigned char *p, *v;
453 NSString *headerName;
454 NSString *headerValue;
457 printf("read header line: '%s'\n", self->lineBuffer);
459 if (strlen(self->lineBuffer) == 0) {
460 /* found end of header */
464 p = self->lineBuffer;
466 if (*p == ' ' || *p == '\t') {
467 // TODO: implement folding (remember last header-key, add string)
468 [self errorWithFormat:
469 @"(%s): got a folded HTTP header line, cannot process!",
470 __PRETTY_FUNCTION__];
474 /* find key/value separator */
475 if ((v = index(p, ':')) == NULL) {
476 [self warnWithFormat:@"got malformed header line: '%s'",
481 *v = '\0'; v++; /* now 'p' points to name and 'v' to value */
483 /* skip leading spaces */
484 while (*v != '\0' && (*v == ' ' || *v == '\t'))
488 /* trim trailing spaces */
489 for (idx = strlen(v) - 1; idx >= 0; idx--) {
490 if ((v[idx] != ' ' && v[idx] != '\t'))
497 headerName = stringForHeaderName(p);
498 headerValue = [[NSStringClass alloc] initWithCString:v];
500 if (headerName == ContentLengthHeaderName)
501 self->clen = atoi(v);
503 if (headerName != nil || headerValue != nil) {
504 if (self->headers == nil)
505 self->headers = [[NSMutableDictionary alloc] initWithCapacity:32];
507 [self->headers setObject:headerValue forKey:headerName];
510 [headerValue release];
511 [headerName release];
517 - (NSException *)parseEntityOfMethod:(NSString *)_method {
519 TODO: several cases are caught:
520 a) content-length = 0 => empty data
521 b) content-length small => read into memory
522 c) content-length large => streamed into the filesystem to safe RAM
523 d) content-length unknown => ??
526 if (self->clen == 0) {
529 else if (self->clen < 0) {
530 /* I think HTTP/1.1 requires a content-length header to be present ? */
532 if ([self->httpVersion isEqualToString:@"HTTP/1.0"] ||
533 [self->httpVersion isEqualToString:@"HTTP/0.9"]) {
534 /* content-length unknown, read till EOF */
535 BOOL readToEOF = YES;
537 if ([_method isEqualToString:@"HEAD"])
539 else if ([_method isEqualToString:@"GET"])
541 else if ([_method isEqualToString:@"DELETE"])
545 [self warnWithFormat:
546 @"not processing entity of request without contentlen!"];
550 else if (self->clen > maxUploadSize*1024) {
551 /* entity is too large */
554 s = [NSString stringWithFormat:@"The maximum HTTP transaction size was "
555 @"exceeded (%d vs %d)", self->clen, maxUploadSize * 1024];
556 return [NSException exceptionWithName:@"LimitException"
557 reason:s userInfo:nil];
559 else if (self->clen > fileIOBoundary) {
560 /* we are streaming the content to a file and use a memory mapped data */
568 [self debugWithFormat:@"streaming %i bytes into file ...", self->clen];
570 fn = [[NSProcessInfo processInfo] temporaryFileName];
572 if ((t = fopen([fn cString], "w")) == NULL) {
573 [self errorWithFormat:@"could not open temporary file '%@'!", fn];
575 /* read into memory as a fallback ... */
578 [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
579 if (self->content == nil)
580 return [self->io lastException];
584 for (toGo = self->clen; toGo > 0; ) {
585 unsigned readCount, writeCount;
587 /* read from socket */
588 readCount = [self->io readBytes:buf count:sizeof(buf)];
589 if (readCount == NGStreamError) {
597 if ((writeCount = fwrite(buf, readCount, 1, t)) != 1) {
600 writeError = ferror(t);
607 unlink([fn cString]); /* delete temporary file */
609 if (writeError == 0) {
610 return [NSException exceptionWithName:@"SystemWriteError"
611 reason:@"failed to write data to upload file"
615 return [self->io lastException];
618 self->content = [[NSData alloc] initWithContentsOfMappedFile:fn];
619 unlink([fn cString]); /* if the mmap disappears, the storage is freed */
622 /* content-length known and small */
623 //[self logWithFormat:@"reading %i bytes of the entity", self->clen];
626 [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
627 if (self->content == nil)
628 return [self->io lastException];
630 //[self logWithFormat:@"read %i bytes.", [self->content length]];
636 /* handling expectations */
638 - (BOOL)processContinueExpectation {
639 // TODO: this should check the credentials of a request before accepting the
640 // body. The current implementation is far from optimal and only added
641 // for Mono compatibility (and actually produces the same behaviour
642 // like with HTTP/1.0 ...)
643 static unsigned char *contStatLine =
644 "HTTP/1.0 100 Continue\r\n"
645 "content-length: 0\r\n"
647 static unsigned char *failStatLine =
648 "HTTP/1.0 417 Expectation Failed\r\n"
649 "content-length: 0\r\n"
651 unsigned char *respline = NULL;
654 [self debugWithFormat:@"process 100 continue on IO: %@", self->io];
656 if (self->clen > 0 && (self->clen > (maxUploadSize * 1024))) {
657 // TODO: return a 417 expectation failed
659 respline = failStatLine;
663 respline = contStatLine;
666 if (![self->io safeWriteBytes:respline count:strlen(respline)]) {
667 ASSIGN(self->lastException, [self->io lastException]);
670 if (![self->io flush]) {
671 ASSIGN(self->lastException, [self->io lastException]);
680 - (WORequest *)parseRequest {
681 NSException *e = nil;
683 NSString *uri = @"/";
684 NSString *method = @"GET";
689 [self logWithFormat:@"HeavyDebug: parsing response ..."];
691 /* process request line */
693 if ((e = [self readNextLine])) {
694 ASSIGN(self->lastException, e);
698 printf("read request line: '%s'\n", self->lineBuffer);
701 /* sample line: "GET / HTTP/1.0" */
702 unsigned char *p, *t;
706 p = self->lineBuffer;
707 if ((t = index(p, ' ')) == NULL) {
708 [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
714 /* intended fall-throughs ! */
716 if (strcasecmp(p, "BPROPFIND") == 0) { method = @"BPROPFIND"; break; }
717 if (strcasecmp(p, "BPROPPATCH") == 0) { method = @"BPROPPATCH"; break; }
719 if (strcasecmp(p, "COPY") == 0) { method = @"COPY"; break; }
720 if (strcasecmp(p, "CHECKOUT") == 0) { method = @"CHECKOUT"; break; }
721 if (strcasecmp(p, "CHECKIN") == 0) { method = @"CHECKIN"; break; }
723 if (strcasecmp(p, "DELETE") == 0) { method = @"DELETE"; break; }
725 if (strcasecmp(p, "HEAD") == 0) { method = @"HEAD"; break; }
727 if (strcasecmp(p, "LOCK") == 0) { method = @"LOCK"; break; }
729 if (strcasecmp(p, "GET") == 0) { method = @"GET"; break; }
731 if (strcasecmp(p, "MKCOL") == 0) { method = @"MKCOL"; break; }
732 if (strcasecmp(p, "MOVE") == 0) { method = @"MOVE"; break; }
734 if (strcasecmp(p, "NOTIFY") == 0) { method = @"NOTIFY"; break; }
736 if (strcasecmp(p, "OPTIONS") == 0) { method = @"OPTIONS"; break; }
738 if (strcasecmp(p, "PUT") == 0) { method = @"PUT"; break; }
739 if (strcasecmp(p, "POST") == 0) { method = @"POST"; break; }
740 if (strcasecmp(p, "PROPFIND") == 0) { method = @"PROPFIND"; break; }
741 if (strcasecmp(p, "PROPPATCH") == 0) { method = @"PROPPATCH"; break; }
742 if (strcasecmp(p, "POLL") == 0) { method = @"POLL"; break; }
744 if (strcasecmp(p, "REPORT") == 0) { method = @"REPORT"; break; }
746 if (strcasecmp(p, "SEARCH") == 0) { method = @"SEARCH"; break; }
747 if (strcasecmp(p, "SUBSCRIBE") == 0) { method = @"SUBSCRIBE"; break; }
749 if (strcasecmp(p, "UNLOCK") == 0) { method = @"UNLOCK"; break; }
750 if (strcasecmp(p, "UNSUBSCRIBE")== 0) { method = @"UNSUBSCRIBE"; break; }
751 if (strcasecmp(p, "UNCHECKOUT") == 0) { method = @"UNCHECKOUT"; break; }
753 if (strcasecmp(p, "VERSION-CONTROL") == 0) {
754 method = @"VERSION-CONTROL";
760 [self debugWithFormat:@"making custom HTTP method name: '%s'", p];
761 method = [NSString stringWithCString:p];
767 p = t + 1; /* skip space */
768 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
772 [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
776 if ((t = index(p, ' ')) == NULL) {
777 /* the URI isn't followed by a HTTP version */
778 self->httpVersion = @"HTTP/0.9";
779 /* TODO: strip trailing spaces for better compliance */
780 uri = [NSString stringWithCString:p];
784 uri = [NSString stringWithCString:p];
788 p = t + 1; /* skip space */
789 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
793 self->httpVersion = @"HTTP/0.9";
794 else if (strcasecmp(p, "http/1.0") == 0)
795 self->httpVersion = @"HTTP/1.0";
796 else if (strcasecmp(p, "http/1.1") == 0)
797 self->httpVersion = @"HTTP/1.1";
799 /* TODO: strip trailing spaces */
800 self->httpVersion = [[NSString alloc] initWithCString:p];
807 if ((e = [self parseHeader])) {
808 ASSIGN(self->lastException, e);
812 [self logWithFormat:@"parsed header: %@", self->headers];
814 /* check for expectations */
816 if ((expect = [self->headers objectForKey:@"expect"])) {
817 if ([expect rangeOfString:@"100-continue"
818 options:NSCaseInsensitiveSearch].length > 0) {
819 if (![self processContinueExpectation])
827 if ((e = [self parseEntityOfMethod:method])) {
828 ASSIGN(self->lastException, e);
834 [self logWithFormat:@"HeavyDebug: got all .."];
836 r = [[WORequest alloc] initWithMethod:method
838 httpVersion:self->httpVersion
839 headers:self->headers
840 content:self->content
845 [self logWithFormat:@"HeavyDebug: request: %@", r];
847 return [r autorelease];
850 - (WOResponse *)parseResponse {
851 NSException *e = nil;
857 [self logWithFormat:@"HeavyDebug: parsing response ..."];
859 /* process response line */
861 if ((e = [self readNextLine])) {
862 ASSIGN(self->lastException, e);
866 printf("read response line: '%s'\n", self->lineBuffer);
869 /* sample line: "HTTP/1.0 200 OK" */
870 unsigned char *p, *t;
874 p = self->lineBuffer;
875 if ((t = index(p, ' ')) == NULL) {
876 [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
881 if (strcasecmp(p, "http/1.0") == 0)
882 self->httpVersion = @"HTTP/1.0";
883 else if (strcasecmp(p, "http/1.1") == 0)
884 self->httpVersion = @"HTTP/1.1";
886 self->httpVersion = [[NSString alloc] initWithCString:p];
890 p = t + 1; /* skip space */
891 while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
894 [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
899 /* we don't need to parse a reason ... */
904 if ((e = [self parseHeader])) {
905 ASSIGN(self->lastException, e);
909 [self logWithFormat:@"parsed header: %@", self->headers];
914 if ((e = [self parseEntityOfMethod:nil /* parsing a response */])) {
915 ASSIGN(self->lastException, e);
921 [self logWithFormat:@"HeavyDebug: got all .."];
923 r = [[[WOResponse alloc] init] autorelease];
925 [r setHTTPVersion:self->httpVersion];
926 [r setHeaders:self->headers];
927 [r setContent:self->content];
932 [self logWithFormat:@"HeavyDebug: response: %@", r];
937 - (NSException *)lastException {
938 return self->lastException;
943 - (BOOL)isDebuggingEnabled {
947 @end /* WOSimpleHTTPParser */