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