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