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