/*
  Copyright (C) 2000-2005 SKYRIX Software AG

  This file is part of SOPE.

  SOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include "WOSimpleHTTPParser.h"
#include <NGObjWeb/WOResponse.h>
#include <NGObjWeb/WORequest.h>
#include "common.h"

@implementation WOSimpleHTTPParser

static Class NSStringClass  = Nil;
static BOOL  debugOn        = NO;
static BOOL  heavyDebugOn   = NO;
static int   fileIOBoundary = 16384;
static int   maxUploadSize  = 256 * 1024; /* 256MB */

+ (int)version {
  return 1;
}
+ (void)initialize {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  
  debugOn        = [ud boolForKey:@"WOSimpleHTTPParserDebugEnabled"];
  heavyDebugOn   = [ud boolForKey:@"WOSimpleHTTPParserHeavyDebugEnabled"];
  fileIOBoundary = [ud integerForKey:@"WOSimpleHTTPParserFileIOBoundary"];
  maxUploadSize  = [ud integerForKey:@"WOSimpleHTTPParserMaxUploadSizeInKB"];

  if (debugOn) {
    NSLog(@"WOSimpleHTTPParser: max-upload-size:  %dKB", maxUploadSize);
    NSLog(@"WOSimpleHTTPParser: file-IO boundary: %d",   fileIOBoundary);
  }
}

- (id)initWithStream:(id<NGStream>)_stream {
  if (NSStringClass == Nil) NSStringClass = [NSString class];
  
  if ((self = [super init])) {
    if ((self->io = [_stream retain]) == nil) {
      [self release];
      return nil;
    }
    
    self->readBytes = (void *)
      [(NSObject *)self->io methodForSelector:@selector(readBytes:count:)];
    if (self->readBytes == NULL) {
      NSLog(@"WARNING(%s): got invalid stream object: %@", __PRETTY_FUNCTION__,
	    self->io);
      [self release];
      return nil;
    }
  }
  return self;
}
- (void)dealloc {
  [self reset];
  [self->io release];
  [super dealloc];
}

/* transient state */

- (void)reset {
  self->clen = -1;
  
  [self->content       release]; self->content     = nil;
  [self->lastException release]; self->lastException = nil;
  [self->httpVersion   release]; self->httpVersion   = nil;
  [self->headers removeAllObjects];

  if (self->lineBuffer) {
    free(self->lineBuffer);
    self->lineBuffer = NULL;
  }
  self->lineBufSize = 0;
}

/* low-level reading */

- (unsigned int)defaultLineSize {
  return 512;
}

- (NSException *)readNextLine {
  unsigned i;
  
  if (self->lineBuffer == NULL) {
    self->lineBufSize = [self defaultLineSize];
    self->lineBuffer  = malloc(self->lineBufSize + 10);
  }
  
  for (i = 0; YES; i++) {
    register unsigned rc;
    unsigned char c;
    
    rc = self->readBytes(self->io, @selector(readBytes:count:), &c, 1);
    if (rc != 1) {
      if (debugOn) {
	[self debugWithFormat:@"got result %u, exception: %@", 
	        rc, [self->io lastException]];
      }
      return [self->io lastException];
    }
    
    /* check buffer capacity */
    if ((i + 2) > self->lineBufSize) {
      static int reallocCount = 0;
      reallocCount++;
      if (reallocCount > 1000) {
	static BOOL didLog = NO;
	if (!didLog) {
	  didLog = YES;
	  NSLog(@"WARNING(%s): reallocated the HTTP line buffer %i times, "
		@"consider increasing the default line buffer size!",
		__PRETTY_FUNCTION__, reallocCount);
	}
      }
      
      if (self->lineBufSize > (56 * 1024)) {
	/* to avoid DOS attacks ... */
	return [NSException exceptionWithName:@"HTTPParserHeaderSizeExceeded"
			    reason:
			      @"got a HTTP line of 100KB+ (DoS attack?)!"
			    userInfo:nil];
      }
      
      self->lineBufSize *= 2;
      self->lineBuffer = realloc(self->lineBuffer, self->lineBufSize + 10);
    }
    
    if (c == '\n') {
      /* found EOL */
      break;
    }
    else if (c == '\r') {
      /* skip CR */
      i--;
      continue;
    }
    else {
      /* store byte */
      self->lineBuffer[i] = c;
    }
  }
  self->lineBuffer[i] = 0; /* 0-terminate buffer */
  
  return nil /* nil means: everything OK */;
}

/* common HTTP parsing */

static NSString *ContentLengthHeaderName = @"content-length";

static NSString *stringForHeaderName(unsigned char *p) {
  /* 
     process header name
     
     we try to be smart to avoid creation of NSString objects ...
  */
  register unsigned len;
  register unsigned char c1;
  
  if ((len = strlen(p)) == 0)
    return @"";
  c1 = *p;

  switch (len) {
  case 0:
  case 1:
    break;
  case 2:
    if (strcasecmp(p, "te") == 0) return @"te";
    if (strcasecmp(p, "if") == 0) return @"if";
    break;
  case 3:
    if (strcasecmp(p, "via") == 0)   return @"via";
    if (strcasecmp(p, "age") == 0)   return @"age";
    if (strcasecmp(p, "p3p") == 0)   return @"p3p";
    break;
  case 4: 
    switch (c1) {
    case 'd': case 'D':
      if (strcasecmp(p, "date") == 0) return @"date";
      break;
    case 'e': case 'E':
      if (strcasecmp(p, "etag") == 0) return @"etag";
      break;
    case 'f': case 'F':
      if (strcasecmp(p, "from") == 0) return @"from";
      break;
    case 'h': case 'H':
      if (strcasecmp(p, "host") == 0) return @"host";
      break;
    case 'v': case 'V':
      if (strcasecmp(p, "vary") == 0) return @"vary";
      break;
    }
    break;
  case 5:
    if (strcasecmp(p, "allow") == 0) return @"allow";
    if (strcasecmp(p, "brief") == 0) return @"brief";
    if (strcasecmp(p, "range") == 0) return @"range";
    if (strcasecmp(p, "depth") == 0) return @"depth";
    if (strcasecmp(p, "ua-os") == 0) return @"ua-os"; /* Entourage */
    break;
  case 6:
    switch (c1) {
    case 'a': case 'A':
      if (strcasecmp(p, "accept") == 0)	return @"accept";
      break;
    case 'c': case 'C':
      if (strcasecmp(p, "cookie") == 0)	return @"cookie";
      break;
    case 'e': case 'E':
      if (strcasecmp(p, "expect") == 0) return @"expect";
      break;
    case 'p': case 'P':
      if (strcasecmp(p, "pragma") == 0)	return @"pragma";
      break;
    case 's': case 'S':
      if (strcasecmp(p, "server") == 0)	return @"server";
      break;
    case 'u': case 'U':
      if (strcasecmp(p, "ua-cpu") == 0)	return @"ua-cpu"; /* Entourage */
      break;
    }
    break;

  default:
    switch (c1) {
    case 'a': case 'A': 
      if (len > 10) {
	if (p[6] == '-') {
	  if (strcasecmp(p, "accept-charset")  == 0) return @"accept-charset";
	  if (strcasecmp(p, "accept-encoding") == 0) return @"accept-encoding";
	  if (strcasecmp(p, "accept-language") == 0) return @"accept-language";
	  if (strcasecmp(p, "accept-ranges")   == 0) return @"accept-ranges";
	}
	else if (strcasecmp(p, "authorization") == 0)
	  return @"authorization";
      }
      break;
      
    case 'c': case 'C':
      if (len > 8) {
	if (p[7] == '-') {
	  if (strcasecmp(p, "content-length") == 0)  
	    return ContentLengthHeaderName;
	  
	  if (strcasecmp(p, "content-type") == 0)    return @"content-type";
	  if (strcasecmp(p, "content-md5") == 0)     return @"content-md5";
	  if (strcasecmp(p, "content-range") == 0)   return @"content-range";
	  
	  if (strcasecmp(p, "content-encoding") == 0)
	    return @"content-encoding";
	  if (strcasecmp(p, "content-language") == 0)
	    return @"content-language";

	  if (strcasecmp(p, "content-location") == 0)
	    return @"content-location";
	  if (strcasecmp(p, "content-class") == 0) /* Entourage */
	    return @"content-class";
	}
	else if (strcasecmp(p, "call-back") == 0)
	  return @"call-back";
      }
      
      if (strcasecmp(p, "connection") == 0)    return @"connection";
      if (strcasecmp(p, "cache-control") == 0) return @"cache-control";
      
      break;

    case 'd': case 'D':
      if (strcasecmp(p, "destination") == 0) return @"destination";
      if (strcasecmp(p, "destroy")     == 0) return @"destroy";
      break;

    case 'e': case 'E':
      if (strcasecmp(p, "expires")   == 0) return @"expires";
      if (strcasecmp(p, "extension") == 0) return @"extension"; /* Entourage */
      break;

    case 'i': case 'I':
      if (strcasecmp(p, "if-modified-since") == 0) 
        return @"if-modified-since";
      if (strcasecmp(p, "if-none-match") == 0) /* Entourage */
        return @"if-none-match";
      if (strcasecmp(p, "if-match") == 0) 
        return @"if-match";
      break;

    case 'k': case 'K':
      if (strcasecmp(p, "keep-alive") == 0) return @"keep-alive";
      break;
      
    case 'l': case 'L':
      if (strcasecmp(p, "last-modified") == 0) return @"last-modified";
      if (strcasecmp(p, "location")      == 0) return @"location";
      if (strcasecmp(p, "lock-token")    == 0) return @"lock-token";
      break;

    case 'm': case 'M':
      if (strcasecmp(p, "ms-webstorage") == 0) return @"ms-webstorage";
      if (strcasecmp(p, "max-forwards")  == 0) return @"max-forwards";
      break;
      
    case 'n': case 'N':
      if (len > 16) {
	if (p[12] == '-') {
	  if (strcasecmp(p, "notification-delay") == 0)
	    return @"notification-delay";
	  if (strcasecmp(p, "notification-type") == 0)
	    return @"notification-type";
	}
      }
      break;

    case 'o': case 'O':
      if (len == 9) {
	if (strcasecmp(p, "overwrite") == 0) 
	  return @"overwrite";
      }
      break;
      
    case 'p': case 'P':
      if (len == 16) {
	if (strcasecmp(p, "proxy-connection") == 0) 
	  return @"proxy-connection";
      }
      break;
      
    case 'r': case 'R':
      if (len == 7) {
	if (strcasecmp(p, "referer") == 0) return @"referer";
      }
      break;
      
    case 's': case 'S':
      switch (len) {
      case 21:
	if (strcasecmp(p, "subscription-lifetime") == 0)
	  return @"subscription-lifetime";
        break;
      case 15:
	if (strcasecmp(p, "subscription-id") == 0)
	  return @"subscription-id";
        break;
      case 10:
	if (strcasecmp(p, "set-cookie") == 0)
	  return @"set-cookie";
        break;
      }
      break;
      
    case 't': case 'T':
      if (strcasecmp(p, "transfer-encoding") == 0) return @"transfer-encoding";
      if (strcasecmp(p, "translate") == 0)         return @"translate";
      if (strcasecmp(p, "trailer") == 0)           return @"trailer";
      if (strcasecmp(p, "timeout") == 0)           return @"timeout";
      break;
      
    case 'u': case 'U':
      if (strcasecmp(p, "user-agent") == 0) return @"user-agent";
      break;
      
    case 'w': case 'W':
      if (strcasecmp(p, "www-authenticate") == 0) return @"www-authenticate";
      if (strcasecmp(p, "warning") == 0)          return @"warning";
      break;
      
    case 'x': case 'X':
      if ((p[2] == 'w') && (len > 22)) {
	if (strstr(p, "x-webobjects-") == (void *)p) {
	  p += 13; /* skip x-webobjects- */
	  if (strcmp(p, "server-protocol") == 0)
	    return @"x-webobjects-server-protocol";
	  else if (strcmp(p, "server-protocol") == 0)
	    return @"x-webobjects-server-protocol";
	  else if (strcmp(p, "remote-addr") == 0)
	    return @"x-webobjects-remote-addr";
	  else if (strcmp(p, "remote-host") == 0)
	    return @"x-webobjects-remote-host";
	  else if (strcmp(p, "server-name") == 0)
	    return @"x-webobjects-server-name";
	  else if (strcmp(p, "server-port") == 0)
	    return @"x-webobjects-server-port";
	  else if (strcmp(p, "server-url") == 0)
	    return @"x-webobjects-server-url";
	}
      }
      if (len == 7) {
	if (strcasecmp(p, "x-cache") == 0)
	  return @"x-cache";
      }
      else if (len == 12) {
	if (strcasecmp(p, "x-powered-by") == 0)
	  return @"x-powered-by";
      }
      if (strcasecmp(p, "x-zidestore-name") == 0)
	return @"x-zidestore-name"; 
      if (strcasecmp(p, "x-forwarded-for") == 0)
	return @"x-forwarded-for";
      if (strcasecmp(p, "x-forwarded-host") == 0)
	return @"x-forwarded-host";
      if (strcasecmp(p, "x-forwarded-server") == 0)
	return @"x-forwarded-server";
      break;
    }
  }
  
  if (debugOn)
    NSLog(@"making custom header name '%s'!", p);
  
  /* make name lowercase (we own the buffer, so we can work on it) */
  {
    unsigned char *t;
    
    for (t = p; *t != '\0'; t++)
      *t = tolower(*t);
  }
  return [[NSString alloc] initWithCString:p];
}

- (NSException *)parseHeader {
  NSException *e = nil;
  
  while ((e = [self readNextLine]) == nil) {
    unsigned char *p, *v;
    unsigned int  idx;
    NSString *headerName;
    NSString *headerValue;
    
    if (heavyDebugOn)
      printf("read header line: '%s'\n", self->lineBuffer);
    
    if (strlen(self->lineBuffer) == 0) {
      /* found end of header */
      break;
    }
    
    p = self->lineBuffer;
    
    if (*p == ' ' || *p == '\t') {
      // TODO: implement folding (remember last header-key, add string)
      [self logWithFormat:
              @"ERROR(%s): got a folded HTTP header line, cannot process!",
	      __PRETTY_FUNCTION__];
      continue;
    }
    
    /* find key/value separator */
    if ((v = index(p, ':')) == NULL) {
      [self logWithFormat:@"WARNING: got malformed header line: '%s'",
	      self->lineBuffer];
      continue;
    }
    
    *v = '\0'; v++; /* now 'p' points to name and 'v' to value */
    
    /* skip leading spaces */
    while (*v != '\0' && (*v == ' ' || *v == '\t'))
      v++;

    if (*v != '\0') {
      /* trim trailing spaces */
      for (idx = strlen(v) - 1; idx >= 0; idx--) {
        if ((v[idx] != ' ' && v[idx] != '\t'))
          break;
        
        v[idx] = '\0';
      }
    }
    
    headerName  = stringForHeaderName(p);
    headerValue = [[NSStringClass alloc] initWithCString:v];
    
    if (headerName == ContentLengthHeaderName)
      self->clen = atoi(v);
    
    if (headerName != nil || headerValue != nil) {
      if (self->headers == nil)
	self->headers = [[NSMutableDictionary alloc] initWithCapacity:32];
      
      [self->headers setObject:headerValue forKey:headerName];
    }
    
    [headerValue release];
    [headerName  release];
  }
  
  return e;
}

- (NSException *)parseEntityOfMethod:(NSString *)_method {
  /*
    TODO: several cases are caught:
    a) content-length = 0   => empty data
    b) content-length small => read into memory
    c) content-length large => streamed into the filesystem to safe RAM
    d) content-length unknown => ??
  */
  
  if (self->clen == 0) {
    /* nothing to do */
  }
  else if (self->clen < 0) {
    /* I think HTTP/1.1 requires a content-length header to be present ? */
    
    if ([self->httpVersion isEqualToString:@"HTTP/1.0"] ||
	[self->httpVersion isEqualToString:@"HTTP/0.9"]) {
      /* content-length unknown, read till EOF */
      BOOL readToEOF = YES;

      if ([_method isEqualToString:@"HEAD"])
	readToEOF = NO;
      else if ([_method isEqualToString:@"GET"])
	readToEOF = NO;
      else if ([_method isEqualToString:@"DELETE"])
	readToEOF = NO;
      
      if (readToEOF) {
	[self logWithFormat:
		@"WARNING: not processing entity of request "
	        @"without contentlen!"];
      }
    }
  }
  else if (self->clen > maxUploadSize*1024) {
    /* entity is too large */
    return [NSException exceptionWithName:@"LimitException"
			reason:
			  @"the maximum HTTP transaction size was exceeded"
			userInfo:nil];
  }
  else if (self->clen > fileIOBoundary) {
    /* we are streaming the content to a file and use a memory mapped data */
    unsigned toGo;
    NSString *fn;
    char buf[4096];
    BOOL ok = YES;
    int  writeError = 0;
    FILE *t;
    
    [self debugWithFormat:@"streaming %i bytes into file ...", self->clen];
    
    fn = [[NSProcessInfo processInfo] temporaryFileName];
    
    if ((t = fopen([fn cString], "w")) == NULL) {
      [self logWithFormat:@"ERROR: could not open temporary file '%@'!", fn];
      
      /* read into memory as a fallback ... */
      
      self->content =
	[[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
      if (self->content == nil)
	return [self->io lastException];
      return nil;
    }
    
    for (toGo = self->clen; toGo > 0; ) {
      unsigned readCount, writeCount;
      
      /* read from socket */
      readCount = [self->io readBytes:buf count:sizeof(buf)];
      if (readCount == NGStreamError) {
	/* an error */
	ok = NO;
	break;
      }
      toGo -= readCount;
      
      /* write to file */
      if ((writeCount = fwrite(buf, readCount, 1, t)) != 1) {
	/* an error */
	ok = NO;
	writeError = ferror(t);
	break;
      }
    }
    fclose(t);
    
    if (!ok) {
      unlink([fn cString]); /* delete temporary file */
      
      if (writeError == 0) {
	return [NSException exceptionWithName:@"SystemWriteError"
			    reason:@"failed to write data to upload file"
			    userInfo:nil];
      }
      
      return [self->io lastException];
    }
    
    self->content = [[NSData alloc] initWithContentsOfMappedFile:fn];
    unlink([fn cString]); /* if the mmap disappears, the storage is freed */
  }
  else {
    /* content-length known and small */
    //[self logWithFormat:@"reading %i bytes of the entity", self->clen];
    
    self->content =
      [[(NGStream *)self->io safeReadDataOfLength:self->clen] retain];
    if (self->content == nil)
      return [self->io lastException];
    
    //[self logWithFormat:@"read %i bytes.", [self->content length]];
  }
  
  return nil;
}

/* handling expectations */

- (BOOL)processContinueExpectation {
  // TODO: this should check the credentials of a request before accepting the
  //       body. The current implementation is far from optimal and only added
  //       for Mono compatibility (and actually produces the same behaviour
  //       like with HTTP/1.0 ...)
  static unsigned char *contStatLine = 
    "HTTP/1.0 100 Continue\r\n"
    "content-length: 0\r\n"
    "\r\n";
  static unsigned char *failStatLine = 
    "HTTP/1.0 417 Expectation Failed\r\n"
    "content-length: 0\r\n"
    "\r\n";
  unsigned char *respline = NULL;
  BOOL ok = YES;
  
  [self debugWithFormat:@"process 100 continue on IO: %@", self->io];
  
  if (self->clen > 0 && (self->clen > (maxUploadSize * 1024))) {
    // TODO: return a 417 expectation failed
    ok = NO;
    respline = failStatLine;
  }
  else {
    ok = YES;
    respline = contStatLine;
  }
  
  if (![self->io safeWriteBytes:respline count:strlen(respline)]) {
    ASSIGN(self->lastException, [self->io lastException]);
    return NO;
  }
  if (![self->io flush]) {
    ASSIGN(self->lastException, [self->io lastException]);
    return NO;
  }
  
  return ok;
}

/* parsing */

- (WORequest *)parseRequest {
  NSException *e = nil;
  WORequest   *r = nil;
  NSString    *uri    = @"/";
  NSString    *method = @"GET";
  NSString    *expect;
  
  [self reset];
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: parsing response ..."];
  
  /* process request line */
  
  if ((e = [self readNextLine])) {
    ASSIGN(self->lastException, e);
    return nil;
  }
  if (heavyDebugOn)
    printf("read request line: '%s'\n", self->lineBuffer);
  
  {
    /* sample line: "GET / HTTP/1.0" */
    unsigned char *p, *t;
    
    /* parse method */
    
    p = self->lineBuffer;
    if ((t = index(p, ' ')) == NULL) {
      [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
      return nil;
    }
    *t = '\0';
    
    switch (*p) {
      /* intended fall-throughs ! */
    case 'b': case 'B':
      if (strcasecmp(p, "BPROPFIND")  == 0) { method = @"BPROPFIND";  break; }
      if (strcasecmp(p, "BPROPPATCH") == 0) { method = @"BPROPPATCH"; break; }
    case 'c': case 'C':
      if (strcasecmp(p, "COPY")     == 0) { method = @"COPY";     break; }
      if (strcasecmp(p, "CHECKOUT") == 0) { method = @"CHECKOUT"; break; }
      if (strcasecmp(p, "CHECKIN")  == 0) { method = @"CHECKIN";  break; }
    case 'd': case 'D':
      if (strcasecmp(p, "DELETE")  == 0) { method = @"DELETE"; break; }
    case 'h': case 'H':
      if (strcasecmp(p, "HEAD")    == 0) { method = @"HEAD";   break; }
    case 'l': case 'L':
      if (strcasecmp(p, "LOCK")    == 0) { method = @"LOCK";   break; }
    case 'g': case 'G':
      if (strcasecmp(p, "GET")     == 0) { method = @"GET";    break; }
    case 'm': case 'M':
      if (strcasecmp(p, "MKCOL")   == 0) { method = @"MKCOL";  break; }
      if (strcasecmp(p, "MOVE")    == 0) { method = @"MOVE";   break; }
    case 'n': case 'N':
      if (strcasecmp(p, "NOTIFY")  == 0) { method = @"NOTIFY"; break; }
    case 'o': case 'O':
      if (strcasecmp(p, "OPTIONS") == 0) { method = @"OPTIONS"; break; }
    case 'p': case 'P':
      if (strcasecmp(p, "PUT")       == 0) { method = @"PUT";       break; }
      if (strcasecmp(p, "POST")      == 0) { method = @"POST";      break; }
      if (strcasecmp(p, "PROPFIND")  == 0) { method = @"PROPFIND";  break; }
      if (strcasecmp(p, "PROPPATCH") == 0) { method = @"PROPPATCH"; break; }
      if (strcasecmp(p, "POLL")      == 0) { method = @"POLL";      break; }
    case 'r': case 'R':
      if (strcasecmp(p, "REPORT")    == 0) { method = @"REPORT";    break; }
    case 's': case 'S':
      if (strcasecmp(p, "SEARCH")    == 0) { method = @"SEARCH";    break; }
      if (strcasecmp(p, "SUBSCRIBE") == 0) { method = @"SUBSCRIBE"; break; }
    case 'u': case 'U':
      if (strcasecmp(p, "UNLOCK")     == 0) { method = @"UNLOCK";      break; }
      if (strcasecmp(p, "UNSUBSCRIBE")== 0) { method = @"UNSUBSCRIBE"; break; }
      if (strcasecmp(p, "UNCHECKOUT") == 0) { method = @"UNCHECKOUT";  break; }
    case 'v': case 'V':
      if (strcasecmp(p, "VERSION-CONTROL") == 0) { 
        method = @"VERSION-CONTROL";      
        break; 
      }
      
    default:
      if (debugOn)
        [self debugWithFormat:@"making custom HTTP method name: '%s'", p];
      method = [NSString stringWithCString:p];
      break;
    }
    
    /* parse URI */
    
    p = t + 1; /* skip space */
    while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
      p++;
    
    if (*p == '\0') {
      [self logWithFormat:@"got broken request line '%s'", self->lineBuffer];
      return nil;
    }
    
    if ((t = index(p, ' ')) == NULL) {
      /* the URI isn't followed by a HTTP version */
      self->httpVersion = @"HTTP/0.9";
      /* TODO: strip trailing spaces for better compliance */
      uri = [NSString stringWithCString:p];
    }
    else {
      *t = '\0';
      uri = [NSString stringWithCString:p];

      /* parse version */
      
      p = t + 1; /* skip space */
      while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
	p++;
      
      if (*p == '\0')
	self->httpVersion = @"HTTP/0.9";
      else if (strcasecmp(p, "http/1.0") == 0)
	self->httpVersion = @"HTTP/1.0";
      else if (strcasecmp(p, "http/1.1") == 0)
	self->httpVersion = @"HTTP/1.1";
      else {
	/* TODO: strip trailing spaces */
	self->httpVersion = [[NSString alloc] initWithCString:p];
      }
    }
  }
  
  /* process header */
  
  if ((e = [self parseHeader])) {
    ASSIGN(self->lastException, e);
    return nil;
  }
  if (heavyDebugOn)
    [self logWithFormat:@"parsed header: %@", self->headers];
  
  /* check for expectations */
  
  if ((expect = [self->headers objectForKey:@"expect"])) {
    if ([expect rangeOfString:@"100-continue" 
                options:NSCaseInsensitiveSearch].length > 0) {
      if (![self processContinueExpectation])
        return nil;
    }
  }
  
  /* process body */
  
  if (clen != 0) {
    if ((e = [self parseEntityOfMethod:method])) {
      ASSIGN(self->lastException, e);
      return nil;
    }
  }
  
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: got all .."];
  
  r = [[WORequest alloc] initWithMethod:method
			 uri:uri
			 httpVersion:self->httpVersion
			 headers:self->headers
			 content:self->content
			 userInfo:nil];
  [self reset];
  
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: request: %@", r];
  
  return [r autorelease];
}

- (WOResponse *)parseResponse {
  NSException *e           = nil;
  int         code         = 200;
  WOResponse  *r = nil;
  
  [self reset];
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: parsing response ..."];
  
  /* process response line */
  
  if ((e = [self readNextLine])) {
    ASSIGN(self->lastException, e);
    return nil;
  }
  if (heavyDebugOn)
    printf("read response line: '%s'\n", self->lineBuffer);
  
  {
    /* sample line: "HTTP/1.0 200 OK" */
    unsigned char *p, *t;
    
    /* version */
    
    p = self->lineBuffer;
    if ((t = index(p, ' ')) == NULL) {
      [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
      return nil;
    }
    
    *t = '\0';
    if (strcasecmp(p, "http/1.0") == 0)
      self->httpVersion = @"HTTP/1.0";
    else if (strcasecmp(p, "http/1.1") == 0)
      self->httpVersion = @"HTTP/1.1";
    else
      self->httpVersion = [[NSString alloc] initWithCString:p];
    
    /* code */
    
    p = t + 1; /* skip space */
    while (*p != '\0' && (*p == ' ' || *p == '\t')) /* skip spaces */
      p++;
    if (*p == '\0') {
      [self logWithFormat:@"got broken response line '%s'", self->lineBuffer];
      return nil;
    }
    code = atoi(p);
    
    /* we don't need to parse a reason ... */
  }
  
  /* process header */
  
  if ((e = [self parseHeader])) {
    ASSIGN(self->lastException, e);
    return nil;
  }
  if (heavyDebugOn)
    [self logWithFormat:@"parsed header: %@", self->headers];
  
  /* process body */
  
  if (clen != 0) {
    if ((e = [self parseEntityOfMethod:nil /* parsing a response */])) {
      ASSIGN(self->lastException, e);
      return nil;
    }
  }
  
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: got all .."];
  
  r = [[[WOResponse alloc] init] autorelease];
  [r setStatus:code];
  [r setHTTPVersion:self->httpVersion];
  [r setHeaders:self->headers];
  [r setContent:self->content];
  
  [self reset];
  
  if (heavyDebugOn)
    [self logWithFormat:@"HeavyDebug: response: %@", r];
  
  return r;
}

- (NSException *)lastException {
  return self->lastException;
}

/* debugging */

- (BOOL)isDebuggingEnabled {
  return debugOn;
}

@end /* WOSimpleHTTPParser */
