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