]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WOSimpleHTTPParser.m
define some default defaults for the simple http parser (when the Defaults.plist...
[sope] / sope-appserver / NGObjWeb / WOSimpleHTTPParser.m
1 /*
2   Copyright (C) 2000-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "WOSimpleHTTPParser.h"
23 #include <NGObjWeb/WOResponse.h>
24 #include <NGObjWeb/WORequest.h>
25 #include "common.h"
26
27 @implementation WOSimpleHTTPParser
28
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;
34
35 + (int)version {
36   return 1;
37 }
38 + (void)initialize {
39   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
40   
41   debugOn        = [ud boolForKey:@"WOSimpleHTTPParserDebugEnabled"];
42   heavyDebugOn   = [ud boolForKey:@"WOSimpleHTTPParserHeavyDebugEnabled"];
43   fileIOBoundary = [ud integerForKey:@"WOSimpleHTTPParserFileIOBoundary"];
44   maxUploadSize  = [ud integerForKey:@"WOSimpleHTTPParserMaxUploadSizeInKB"];
45   
46   if (maxUploadSize == 0)
47     maxUploadSize = 256 * 1024; /* 256MB */
48   if (fileIOBoundary == 0)
49     fileIOBoundary = 16384;
50   
51   if (debugOn) {
52     NSLog(@"WOSimpleHTTPParser: max-upload-size:  %dKB", maxUploadSize);
53     NSLog(@"WOSimpleHTTPParser: file-IO boundary: %d",   fileIOBoundary);
54   }
55 }
56
57 - (id)initWithStream:(id<NGStream>)_stream {
58   if (NSStringClass == Nil) NSStringClass = [NSString class];
59   
60   if ((self = [super init])) {
61     if ((self->io = [_stream retain]) == nil) {
62       [self release];
63       return nil;
64     }
65     
66     self->readBytes = (void *)
67       [(NSObject *)self->io methodForSelector:@selector(readBytes:count:)];
68     if (self->readBytes == NULL) {
69       NSLog(@"WARNING(%s): got invalid stream object: %@", __PRETTY_FUNCTION__,
70             self->io);
71       [self release];
72       return nil;
73     }
74   }
75   return self;
76 }
77 - (void)dealloc {
78   [self reset];
79   [self->io release];
80   [super dealloc];
81 }
82
83 /* transient state */
84
85 - (void)reset {
86   self->clen = -1;
87   
88   [self->content       release]; self->content     = nil;
89   [self->lastException release]; self->lastException = nil;
90   [self->httpVersion   release]; self->httpVersion   = nil;
91   [self->headers removeAllObjects];
92
93   if (self->lineBuffer) {
94     free(self->lineBuffer);
95     self->lineBuffer = NULL;
96   }
97   self->lineBufSize = 0;
98 }
99
100 /* low-level reading */
101
102 - (unsigned int)defaultLineSize {
103   return 512;
104 }
105
106 - (NSException *)readNextLine {
107   unsigned i;
108   
109   if (self->lineBuffer == NULL) {
110     self->lineBufSize = [self defaultLineSize];
111     self->lineBuffer  = malloc(self->lineBufSize + 10);
112   }
113   
114   for (i = 0; YES; i++) {
115     register unsigned rc;
116     unsigned char c;
117     
118     rc = self->readBytes(self->io, @selector(readBytes:count:), &c, 1);
119     if (rc != 1) {
120       if (debugOn) {
121         [self debugWithFormat:@"got result %u, exception: %@", 
122                 rc, [self->io lastException]];
123       }
124       return [self->io lastException];
125     }
126     
127     /* check buffer capacity */
128     if ((i + 2) > self->lineBufSize) {
129       static int reallocCount = 0;
130       reallocCount++;
131       if (reallocCount > 1000) {
132         static BOOL didLog = NO;
133         if (!didLog) {
134           didLog = YES;
135           NSLog(@"WARNING(%s): reallocated the HTTP line buffer %i times, "
136                 @"consider increasing the default line buffer size!",
137                 __PRETTY_FUNCTION__, reallocCount);
138         }
139       }
140       
141       if (self->lineBufSize > (56 * 1024)) {
142         /* to avoid DOS attacks ... */
143         return [NSException exceptionWithName:@"HTTPParserHeaderSizeExceeded"
144                             reason:
145                               @"got a HTTP line of 100KB+ (DoS attack?)!"
146                             userInfo:nil];
147       }
148       
149       self->lineBufSize *= 2;
150       self->lineBuffer = realloc(self->lineBuffer, self->lineBufSize + 10);
151     }
152     
153     if (c == '\n') {
154       /* found EOL */
155       break;
156     }
157     else if (c == '\r') {
158       /* skip CR */
159       i--;
160       continue;
161     }
162     else {
163       /* store byte */
164       self->lineBuffer[i] = c;
165     }
166   }
167   self->lineBuffer[i] = 0; /* 0-terminate buffer */
168   
169   return nil /* nil means: everything OK */;
170 }
171
172 /* common HTTP parsing */
173
174 static NSString *ContentLengthHeaderName = @"content-length";
175
176 static NSString *stringForHeaderName(unsigned char *p) {
177   /* 
178      process header name
179      
180      we try to be smart to avoid creation of NSString objects ...
181   */
182   register unsigned len;
183   register unsigned char c1;
184   
185   if ((len = strlen(p)) == 0)
186     return @"";
187   c1 = *p;
188
189   switch (len) {
190   case 0:
191   case 1:
192     break;
193   case 2:
194     if (strcasecmp(p, "te") == 0) return @"te";
195     if (strcasecmp(p, "if") == 0) return @"if";
196     break;
197   case 3:
198     if (strcasecmp(p, "via") == 0)   return @"via";
199     if (strcasecmp(p, "age") == 0)   return @"age";
200     if (strcasecmp(p, "p3p") == 0)   return @"p3p";
201     break;
202   case 4: 
203     switch (c1) {
204     case 'd': case 'D':
205       if (strcasecmp(p, "date") == 0) return @"date";
206       break;
207     case 'e': case 'E':
208       if (strcasecmp(p, "etag") == 0) return @"etag";
209       break;
210     case 'f': case 'F':
211       if (strcasecmp(p, "from") == 0) return @"from";
212       break;
213     case 'h': case 'H':
214       if (strcasecmp(p, "host") == 0) return @"host";
215       break;
216     case 'v': case 'V':
217       if (strcasecmp(p, "vary") == 0) return @"vary";
218       break;
219     }
220     break;
221   case 5:
222     if (strcasecmp(p, "allow") == 0) return @"allow";
223     if (strcasecmp(p, "brief") == 0) return @"brief";
224     if (strcasecmp(p, "range") == 0) return @"range";
225     if (strcasecmp(p, "depth") == 0) return @"depth";
226     if (strcasecmp(p, "ua-os") == 0) return @"ua-os"; /* Entourage */
227     break;
228   case 6:
229     switch (c1) {
230     case 'a': case 'A':
231       if (strcasecmp(p, "accept") == 0) return @"accept";
232       break;
233     case 'c': case 'C':
234       if (strcasecmp(p, "cookie") == 0) return @"cookie";
235       break;
236     case 'e': case 'E':
237       if (strcasecmp(p, "expect") == 0) return @"expect";
238       break;
239     case 'p': case 'P':
240       if (strcasecmp(p, "pragma") == 0) return @"pragma";
241       break;
242     case 's': case 'S':
243       if (strcasecmp(p, "server") == 0) return @"server";
244       break;
245     case 'u': case 'U':
246       if (strcasecmp(p, "ua-cpu") == 0) return @"ua-cpu"; /* Entourage */
247       break;
248     }
249     break;
250
251   default:
252     switch (c1) {
253     case 'a': case 'A': 
254       if (len > 10) {
255         if (p[6] == '-') {
256           if (strcasecmp(p, "accept-charset")  == 0) return @"accept-charset";
257           if (strcasecmp(p, "accept-encoding") == 0) return @"accept-encoding";
258           if (strcasecmp(p, "accept-language") == 0) return @"accept-language";
259           if (strcasecmp(p, "accept-ranges")   == 0) return @"accept-ranges";
260         }
261         else if (strcasecmp(p, "authorization") == 0)
262           return @"authorization";
263       }
264       break;
265       
266     case 'c': case 'C':
267       if (len > 8) {
268         if (p[7] == '-') {
269           if (strcasecmp(p, "content-length") == 0)  
270             return ContentLengthHeaderName;
271           
272           if (strcasecmp(p, "content-type") == 0)    return @"content-type";
273           if (strcasecmp(p, "content-md5") == 0)     return @"content-md5";
274           if (strcasecmp(p, "content-range") == 0)   return @"content-range";
275           
276           if (strcasecmp(p, "content-encoding") == 0)
277             return @"content-encoding";
278           if (strcasecmp(p, "content-language") == 0)
279             return @"content-language";
280
281           if (strcasecmp(p, "content-location") == 0)
282             return @"content-location";
283           if (strcasecmp(p, "content-class") == 0) /* Entourage */
284             return @"content-class";
285         }
286         else if (strcasecmp(p, "call-back") == 0)
287           return @"call-back";
288       }
289       
290       if (strcasecmp(p, "connection") == 0)    return @"connection";
291       if (strcasecmp(p, "cache-control") == 0) return @"cache-control";
292       
293       break;
294
295     case 'd': case 'D':
296       if (strcasecmp(p, "destination") == 0) return @"destination";
297       if (strcasecmp(p, "destroy")     == 0) return @"destroy";
298       break;
299
300     case 'e': case 'E':
301       if (strcasecmp(p, "expires")   == 0) return @"expires";
302       if (strcasecmp(p, "extension") == 0) return @"extension"; /* Entourage */
303       break;
304
305     case 'i': case 'I':
306       if (strcasecmp(p, "if-modified-since") == 0) 
307         return @"if-modified-since";
308       if (strcasecmp(p, "if-none-match") == 0) /* Entourage */
309         return @"if-none-match";
310       if (strcasecmp(p, "if-match") == 0) 
311         return @"if-match";
312       break;
313
314     case 'k': case 'K':
315       if (strcasecmp(p, "keep-alive") == 0) return @"keep-alive";
316       break;
317       
318     case 'l': case 'L':
319       if (strcasecmp(p, "last-modified") == 0) return @"last-modified";
320       if (strcasecmp(p, "location")      == 0) return @"location";
321       if (strcasecmp(p, "lock-token")    == 0) return @"lock-token";
322       break;
323
324     case 'm': case 'M':
325       if (strcasecmp(p, "ms-webstorage") == 0) return @"ms-webstorage";
326       if (strcasecmp(p, "max-forwards")  == 0) return @"max-forwards";
327       break;
328       
329     case 'n': case 'N':
330       if (len > 16) {
331         if (p[12] == '-') {
332           if (strcasecmp(p, "notification-delay") == 0)
333             return @"notification-delay";
334           if (strcasecmp(p, "notification-type") == 0)
335             return @"notification-type";
336         }
337       }
338       break;
339
340     case 'o': case 'O':
341       if (len == 9) {
342         if (strcasecmp(p, "overwrite") == 0) 
343           return @"overwrite";
344       }
345       break;
346       
347     case 'p': case 'P':
348       if (len == 16) {
349         if (strcasecmp(p, "proxy-connection") == 0) 
350           return @"proxy-connection";
351       }
352       break;
353       
354     case 'r': case 'R':
355       if (len == 7) {
356         if (strcasecmp(p, "referer") == 0) return @"referer";
357       }
358       break;
359       
360     case 's': case 'S':
361       switch (len) {
362       case 21:
363         if (strcasecmp(p, "subscription-lifetime") == 0)
364           return @"subscription-lifetime";
365         break;
366       case 15:
367         if (strcasecmp(p, "subscription-id") == 0)
368           return @"subscription-id";
369         break;
370       case 10:
371         if (strcasecmp(p, "set-cookie") == 0)
372           return @"set-cookie";
373         break;
374       }
375       break;
376       
377     case 't': case 'T':
378       if (strcasecmp(p, "transfer-encoding") == 0) return @"transfer-encoding";
379       if (strcasecmp(p, "translate") == 0)         return @"translate";
380       if (strcasecmp(p, "trailer") == 0)           return @"trailer";
381       if (strcasecmp(p, "timeout") == 0)           return @"timeout";
382       break;
383       
384     case 'u': case 'U':
385       if (strcasecmp(p, "user-agent") == 0) return @"user-agent";
386       break;
387       
388     case 'w': case 'W':
389       if (strcasecmp(p, "www-authenticate") == 0) return @"www-authenticate";
390       if (strcasecmp(p, "warning") == 0)          return @"warning";
391       break;
392       
393     case 'x': case 'X':
394       if ((p[2] == 'w') && (len > 22)) {
395         if (strstr(p, "x-webobjects-") == (void *)p) {
396           p += 13; /* skip x-webobjects- */
397           if (strcmp(p, "server-protocol") == 0)
398             return @"x-webobjects-server-protocol";
399           else if (strcmp(p, "server-protocol") == 0)
400             return @"x-webobjects-server-protocol";
401           else if (strcmp(p, "remote-addr") == 0)
402             return @"x-webobjects-remote-addr";
403           else if (strcmp(p, "remote-host") == 0)
404             return @"x-webobjects-remote-host";
405           else if (strcmp(p, "server-name") == 0)
406             return @"x-webobjects-server-name";
407           else if (strcmp(p, "server-port") == 0)
408             return @"x-webobjects-server-port";
409           else if (strcmp(p, "server-url") == 0)
410             return @"x-webobjects-server-url";
411         }
412       }
413       if (len == 7) {
414         if (strcasecmp(p, "x-cache") == 0)
415           return @"x-cache";
416       }
417       else if (len == 12) {
418         if (strcasecmp(p, "x-powered-by") == 0)
419           return @"x-powered-by";
420       }
421       if (strcasecmp(p, "x-zidestore-name") == 0)
422         return @"x-zidestore-name"; 
423       if (strcasecmp(p, "x-forwarded-for") == 0)
424         return @"x-forwarded-for";
425       if (strcasecmp(p, "x-forwarded-host") == 0)
426         return @"x-forwarded-host";
427       if (strcasecmp(p, "x-forwarded-server") == 0)
428         return @"x-forwarded-server";
429       break;
430     }
431   }
432   
433   if (debugOn)
434     NSLog(@"making custom header name '%s'!", p);
435   
436   /* make name lowercase (we own the buffer, so we can work on it) */
437   {
438     unsigned char *t;
439     
440     for (t = p; *t != '\0'; t++)
441       *t = tolower(*t);
442   }
443   return [[NSString alloc] initWithCString:p];
444 }
445
446 - (NSException *)parseHeader {
447   NSException *e = nil;
448   
449   while ((e = [self readNextLine]) == nil) {
450     unsigned char *p, *v;
451     unsigned int  idx;
452     NSString *headerName;
453     NSString *headerValue;
454     
455     if (heavyDebugOn)
456       printf("read header line: '%s'\n", self->lineBuffer);
457     
458     if (strlen(self->lineBuffer) == 0) {
459       /* found end of header */
460       break;
461     }
462     
463     p = self->lineBuffer;
464     
465     if (*p == ' ' || *p == '\t') {
466       // TODO: implement folding (remember last header-key, add string)
467       [self logWithFormat:
468               @"ERROR(%s): got a folded HTTP header line, cannot process!",
469               __PRETTY_FUNCTION__];
470       continue;
471     }
472     
473     /* find key/value separator */
474     if ((v = index(p, ':')) == NULL) {
475       [self logWithFormat:@"WARNING: got malformed header line: '%s'",
476               self->lineBuffer];
477       continue;
478     }
479     
480     *v = '\0'; v++; /* now 'p' points to name and 'v' to value */
481     
482     /* skip leading spaces */
483     while (*v != '\0' && (*v == ' ' || *v == '\t'))
484       v++;
485
486     if (*v != '\0') {
487       /* trim trailing spaces */
488       for (idx = strlen(v) - 1; idx >= 0; idx--) {
489         if ((v[idx] != ' ' && v[idx] != '\t'))
490           break;
491         
492         v[idx] = '\0';
493       }
494     }
495     
496     headerName  = stringForHeaderName(p);
497     headerValue = [[NSStringClass alloc] initWithCString:v];
498     
499     if (headerName == ContentLengthHeaderName)
500       self->clen = atoi(v);
501     
502     if (headerName != nil || headerValue != nil) {
503       if (self->headers == nil)
504         self->headers = [[NSMutableDictionary alloc] initWithCapacity:32];
505       
506       [self->headers setObject:headerValue forKey:headerName];
507     }
508     
509     [headerValue release];
510     [headerName  release];
511   }
512   
513   return e;
514 }
515
516 - (NSException *)parseEntityOfMethod:(NSString *)_method {
517   /*
518     TODO: several cases are caught:
519     a) content-length = 0   => empty data
520     b) content-length small => read into memory
521     c) content-length large => streamed into the filesystem to safe RAM
522     d) content-length unknown => ??
523   */
524   
525   if (self->clen == 0) {
526     /* nothing to do */
527   }
528   else if (self->clen < 0) {
529     /* I think HTTP/1.1 requires a content-length header to be present ? */
530     
531     if ([self->httpVersion isEqualToString:@"HTTP/1.0"] ||
532         [self->httpVersion isEqualToString:@"HTTP/0.9"]) {
533       /* content-length unknown, read till EOF */
534       BOOL readToEOF = YES;
535
536       if ([_method isEqualToString:@"HEAD"])
537         readToEOF = NO;
538       else if ([_method isEqualToString:@"GET"])
539         readToEOF = NO;
540       else if ([_method isEqualToString:@"DELETE"])
541         readToEOF = NO;
542       
543       if (readToEOF) {
544         [self logWithFormat:
545                 @"WARNING: not processing entity of request "
546                 @"without contentlen!"];
547       }
548     }
549   }
550   else if (self->clen > maxUploadSize*1024) {
551     /* entity is too large */
552     NSString *s;
553
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];
558   }
559   else if (self->clen > fileIOBoundary) {
560     /* we are streaming the content to a file and use a memory mapped data */
561     unsigned toGo;
562     NSString *fn;
563     char buf[4096];
564     BOOL ok = YES;
565     int  writeError = 0;
566     FILE *t;
567     
568     [self debugWithFormat:@"streaming %i bytes into file ...", self->clen];
569     
570     fn = [[NSProcessInfo processInfo] temporaryFileName];
571     
572     if ((t = fopen([fn cString], "w")) == NULL) {
573       [self logWithFormat:@"ERROR: could not open temporary file '%@'!", fn];
574       
575       /* read into memory as a fallback ... */
576       
577       self->content =
578         [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
579       if (self->content == nil)
580         return [self->io lastException];
581       return nil;
582     }
583     
584     for (toGo = self->clen; toGo > 0; ) {
585       unsigned readCount, writeCount;
586       
587       /* read from socket */
588       readCount = [self->io readBytes:buf count:sizeof(buf)];
589       if (readCount == NGStreamError) {
590         /* an error */
591         ok = NO;
592         break;
593       }
594       toGo -= readCount;
595       
596       /* write to file */
597       if ((writeCount = fwrite(buf, readCount, 1, t)) != 1) {
598         /* an error */
599         ok = NO;
600         writeError = ferror(t);
601         break;
602       }
603     }
604     fclose(t);
605     
606     if (!ok) {
607       unlink([fn cString]); /* delete temporary file */
608       
609       if (writeError == 0) {
610         return [NSException exceptionWithName:@"SystemWriteError"
611                             reason:@"failed to write data to upload file"
612                             userInfo:nil];
613       }
614       
615       return [self->io lastException];
616     }
617     
618     self->content = [[NSData alloc] initWithContentsOfMappedFile:fn];
619     unlink([fn cString]); /* if the mmap disappears, the storage is freed */
620   }
621   else {
622     /* content-length known and small */
623     //[self logWithFormat:@"reading %i bytes of the entity", self->clen];
624     
625     self->content =
626       [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
627     if (self->content == nil)
628       return [self->io lastException];
629     
630     //[self logWithFormat:@"read %i bytes.", [self->content length]];
631   }
632   
633   return nil;
634 }
635
636 /* handling expectations */
637
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"
646     "\r\n";
647   static unsigned char *failStatLine = 
648     "HTTP/1.0 417 Expectation Failed\r\n"
649     "content-length: 0\r\n"
650     "\r\n";
651   unsigned char *respline = NULL;
652   BOOL ok = YES;
653   
654   [self debugWithFormat:@"process 100 continue on IO: %@", self->io];
655   
656   if (self->clen > 0 && (self->clen > (maxUploadSize * 1024))) {
657     // TODO: return a 417 expectation failed
658     ok = NO;
659     respline = failStatLine;
660   }
661   else {
662     ok = YES;
663     respline = contStatLine;
664   }
665   
666   if (![self->io safeWriteBytes:respline count:strlen(respline)]) {
667     ASSIGN(self->lastException, [self->io lastException]);
668     return NO;
669   }
670   if (![self->io flush]) {
671     ASSIGN(self->lastException, [self->io lastException]);
672     return NO;
673   }
674   
675   return ok;
676 }
677
678 /* parsing */
679
680 - (WORequest *)parseRequest {
681   NSException *e = nil;
682   WORequest   *r = nil;
683   NSString    *uri    = @"/";
684   NSString    *method = @"GET";
685   NSString    *expect;
686   
687   [self reset];
688   if (heavyDebugOn)
689     [self logWithFormat:@"HeavyDebug: parsing response ..."];
690   
691   /* process request line */
692   
693   if ((e = [self readNextLine])) {
694     ASSIGN(self->lastException, e);
695     return nil;
696   }
697   if (heavyDebugOn)
698     printf("read request line: '%s'\n", self->lineBuffer);
699   
700   {
701     /* sample line: "GET / HTTP/1.0" */
702     unsigned char *p, *t;
703     
704     /* parse method */
705     
706     p = self->lineBuffer;
707     if ((t = index(p, ' ')) == NULL) {
708       [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
709       return nil;
710     }
711     *t = '\0';
712     
713     switch (*p) {
714       /* intended fall-throughs ! */
715     case 'b': case 'B':
716       if (strcasecmp(p, "BPROPFIND")  == 0) { method = @"BPROPFIND";  break; }
717       if (strcasecmp(p, "BPROPPATCH") == 0) { method = @"BPROPPATCH"; break; }
718     case 'c': case 'C':
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; }
722     case 'd': case 'D':
723       if (strcasecmp(p, "DELETE")  == 0) { method = @"DELETE"; break; }
724     case 'h': case 'H':
725       if (strcasecmp(p, "HEAD")    == 0) { method = @"HEAD";   break; }
726     case 'l': case 'L':
727       if (strcasecmp(p, "LOCK")    == 0) { method = @"LOCK";   break; }
728     case 'g': case 'G':
729       if (strcasecmp(p, "GET")     == 0) { method = @"GET";    break; }
730     case 'm': case 'M':
731       if (strcasecmp(p, "MKCOL")   == 0) { method = @"MKCOL";  break; }
732       if (strcasecmp(p, "MOVE")    == 0) { method = @"MOVE";   break; }
733     case 'n': case 'N':
734       if (strcasecmp(p, "NOTIFY")  == 0) { method = @"NOTIFY"; break; }
735     case 'o': case 'O':
736       if (strcasecmp(p, "OPTIONS") == 0) { method = @"OPTIONS"; break; }
737     case 'p': case 'P':
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; }
743     case 'r': case 'R':
744       if (strcasecmp(p, "REPORT")    == 0) { method = @"REPORT";    break; }
745     case 's': case 'S':
746       if (strcasecmp(p, "SEARCH")    == 0) { method = @"SEARCH";    break; }
747       if (strcasecmp(p, "SUBSCRIBE") == 0) { method = @"SUBSCRIBE"; break; }
748     case 'u': case 'U':
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; }
752     case 'v': case 'V':
753       if (strcasecmp(p, "VERSION-CONTROL") == 0) { 
754         method = @"VERSION-CONTROL";      
755         break; 
756       }
757       
758     default:
759       if (debugOn)
760         [self debugWithFormat:@"making custom HTTP method name: '%s'", p];
761       method = [NSString stringWithCString:p];
762       break;
763     }
764     
765     /* parse URI */
766     
767     p = t + 1; /* skip space */
768     while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
769       p++;
770     
771     if (*p == '\0') {
772       [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
773       return nil;
774     }
775     
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];
781     }
782     else {
783       *t = '\0';
784       uri = [NSString stringWithCString:p];
785
786       /* parse version */
787       
788       p = t + 1; /* skip space */
789       while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
790         p++;
791       
792       if (*p == '\0')
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";
798       else {
799         /* TODO: strip trailing spaces */
800         self->httpVersion = [[NSString alloc] initWithCString:p];
801       }
802     }
803   }
804   
805   /* process header */
806   
807   if ((e = [self parseHeader])) {
808     ASSIGN(self->lastException, e);
809     return nil;
810   }
811   if (heavyDebugOn)
812     [self logWithFormat:@"parsed header: %@", self->headers];
813   
814   /* check for expectations */
815   
816   if ((expect = [self->headers objectForKey:@"expect"])) {
817     if ([expect rangeOfString:@"100-continue" 
818                 options:NSCaseInsensitiveSearch].length > 0) {
819       if (![self processContinueExpectation])
820         return nil;
821     }
822   }
823   
824   /* process body */
825   
826   if (clen != 0) {
827     if ((e = [self parseEntityOfMethod:method])) {
828       ASSIGN(self->lastException, e);
829       return nil;
830     }
831   }
832   
833   if (heavyDebugOn)
834     [self logWithFormat:@"HeavyDebug: got all .."];
835   
836   r = [[WORequest alloc] initWithMethod:method
837                          uri:uri
838                          httpVersion:self->httpVersion
839                          headers:self->headers
840                          content:self->content
841                          userInfo:nil];
842   [self reset];
843   
844   if (heavyDebugOn)
845     [self logWithFormat:@"HeavyDebug: request: %@", r];
846   
847   return [r autorelease];
848 }
849
850 - (WOResponse *)parseResponse {
851   NSException *e           = nil;
852   int         code         = 200;
853   WOResponse  *r = nil;
854   
855   [self reset];
856   if (heavyDebugOn)
857     [self logWithFormat:@"HeavyDebug: parsing response ..."];
858   
859   /* process response line */
860   
861   if ((e = [self readNextLine])) {
862     ASSIGN(self->lastException, e);
863     return nil;
864   }
865   if (heavyDebugOn)
866     printf("read response line: '%s'\n", self->lineBuffer);
867   
868   {
869     /* sample line: "HTTP/1.0 200 OK" */
870     unsigned char *p, *t;
871     
872     /* version */
873     
874     p = self->lineBuffer;
875     if ((t = index(p, ' ')) == NULL) {
876       [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
877       return nil;
878     }
879     
880     *t = '\0';
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";
885     else
886       self->httpVersion = [[NSString alloc] initWithCString:p];
887     
888     /* code */
889     
890     p = t + 1; /* skip space */
891     while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
892       p++;
893     if (*p == '\0') {
894       [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
895       return nil;
896     }
897     code = atoi(p);
898     
899     /* we don't need to parse a reason ... */
900   }
901   
902   /* process header */
903   
904   if ((e = [self parseHeader])) {
905     ASSIGN(self->lastException, e);
906     return nil;
907   }
908   if (heavyDebugOn)
909     [self logWithFormat:@"parsed header: %@", self->headers];
910   
911   /* process body */
912   
913   if (clen != 0) {
914     if ((e = [self parseEntityOfMethod:nil /* parsing a response */])) {
915       ASSIGN(self->lastException, e);
916       return nil;
917     }
918   }
919   
920   if (heavyDebugOn)
921     [self logWithFormat:@"HeavyDebug: got all .."];
922   
923   r = [[[WOResponse alloc] init] autorelease];
924   [r setStatus:code];
925   [r setHTTPVersion:self->httpVersion];
926   [r setHeaders:self->headers];
927   [r setContent:self->content];
928   
929   [self reset];
930   
931   if (heavyDebugOn)
932     [self logWithFormat:@"HeavyDebug: response: %@", r];
933   
934   return r;
935 }
936
937 - (NSException *)lastException {
938   return self->lastException;
939 }
940
941 /* debugging */
942
943 - (BOOL)isDebuggingEnabled {
944   return debugOn;
945 }
946
947 @end /* WOSimpleHTTPParser */