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