/*
  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 <NGObjWeb/WOAdaptor.h>
#include <NGObjWeb/WOCoreApplication.h>
#include <NGObjWeb/WORequest.h>
#include <NGObjWeb/WOResponse.h>
#include <NGObjWeb/WOCookie.h>

#include "common.h"
#include "WORunLoop.h"
#include "NGHttp+WO.h"

#if LIB_FOUNDATION_LIBRARY
#  import <Foundation/UnixSignalHandler.h>
#else
#  include "UnixSignalHandler.h"
#endif

//#define USE_POOLS 1

#if USE_POOLS
#  warning extensive pools are enabled ...
#endif

static BOOL HTTP_PERFLOG = NO;

#include "WOHttpAdaptor.h"
#include "WORecordRequestStream.h"
#include "WOHttpTransaction.h"

@interface WOHttpAdaptor(Server)

/* accessors */

- (id<NGPassiveSocket>)socket;
- (id<NGSocketAddress>)serverAddress;

- (void)setSendTimeout:(NSTimeInterval)_timeout;
- (NSTimeInterval)sendTimeout;
- (void)setReceiveTimeout:(NSTimeInterval)_timeout;
- (NSTimeInterval)receiveTimeout;

@end /* WOHttpAdaptor */

@interface WOHttpAdaptor(PrivateMethods2)

- (void)logWithFormat:(NSString *)_format, ...;

@end

@interface WOCoreApplication(Port)
- (NSNumber *)port;
@end

@implementation WOHttpAdaptor

static BOOL     WOHttpAdaptor_LogStream      = NO;
static BOOL     WOContactSNS                 = NO;
static BOOL     WOCoreOnHTTPAdaptorException = NO;
static NSString *WOPort      = nil;
static int      WOHttpAdaptorSendTimeout    = 10;
static int      WOHttpAdaptorReceiveTimeout = 10;
static id       allow   = nil;
static BOOL     debugOn = NO;

+ (BOOL)optionLogStream {
  return WOHttpAdaptor_LogStream;
}
+ (BOOL)optionLogPerf {
  return HTTP_PERFLOG;
}

+ (int)version {
  return [super version] + 1 /* v2 */;
}
+ (void)initialize {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  static BOOL didInit = NO;
  if (didInit) return;
  didInit = YES;
  
  NSAssert2([super version] == 1,
            @"invalid superclass (%@) version %i !",
            NSStringFromClass([self superclass]), [super version]);
  
  WOHttpAdaptor_LogStream = [ud boolForKey:@"WOHttpAdaptor_LogStream"];
  HTTP_PERFLOG = [[ud objectForKey:@"WOProfileHttpAdaptor"] boolValue];

  // TODO: those two should be queried on demand to allow different defaults
  WOPort       = [[ud stringForKey:@"WOPort"] copy];
  WOContactSNS = [[ud objectForKey:@"WOContactSNS"] boolValue];

  WOCoreOnHTTPAdaptorException = 
    [[ud objectForKey:@"WOCoreOnHTTPAdaptorException"] boolValue] ? 1 : 0;
  
  WOHttpAdaptorSendTimeout    = 
    [ud integerForKey:@"WOHttpAdaptorSendTimeout"];
  WOHttpAdaptorReceiveTimeout = 
    [ud integerForKey:@"WOHttpAdaptorReceiveTimeout"];
  
  if (allow == nil) {
    allow = [ud objectForKey:@"WOHttpAllowHost"];
    if (allow == nil) {
      allow = [NSArray arrayWithObjects:
                         @"localhost", @"localhost.localdomain", nil];
    }
      
    if (![allow isKindOfClass:[NSArray class]])
      allow = [NSArray arrayWithObject:allow];
      
    allow = [allow copy];
  }
  
  if (WOCoreOnHTTPAdaptorException)
    NSLog(@"WARNING: will dump core on HTTP adaptor exception!");
}

- (id)autoBindAddress {
  NGInternetSocketAddress *addr;
  addr = [[NGInternetSocketAddress alloc] initWithPort:0 onHost:@"127.0.0.1"];
  return [addr autorelease];
}

- (id)initWithName:(NSString *)_name
  arguments:(NSDictionary *)_args
  application:(WOCoreApplication *)_application
{
  if ((self = [super initWithName:_name
                     arguments:_args
                     application:_application])) {
    id arg = nil;
    
    if ([[_application recordingPath] length] > 0)
      WOHttpAdaptor_LogStream = YES;
    
#if !defined(__MINGW32__)
    {
      UnixSignalHandler *us = [UnixSignalHandler sharedHandler];
      
      [us addObserver:self selector:@selector(handleSIGPIPE:)
          forSignal:SIGPIPE
          immediatelyNotifyOnSignal:NO];
    }
#endif
    
    if ([_args count] < 1) {
      const char *cstr;
      
      self->address = nil;
      
      if ([WOPort isEqualToString:@"auto"])
        self->address = [self autoBindAddress];
      
      if ((self->address == nil) && ((cstr = [WOPort cString]) != NULL)) {
        if (isdigit(*cstr)) {
          NSNumber *p;
          
          p = [(WOCoreApplication *)[_application class] port];
          if (p == nil) p = (id)WOPort;
          
          self->address =
            [NGInternetSocketAddress wildcardAddressWithPort:[p intValue]];
        }
      }
      if (self->address == nil)
        self->address = NGSocketAddressFromString(WOPort);
    }
    else {
      NSString *arg = nil;
      
      if ((arg = [_args objectForKey:@"-p"])) {
        self->address =
          [NGInternetSocketAddress wildcardAddressWithPort:[arg intValue]];
      }
      else if ((arg = [_args objectForKey:@"-WOPort"])) {
        const char *cstr;
        
        self->address = nil;
        
        if ([arg isEqualToString:@"auto"])
          self->address = [self autoBindAddress];
        
        if ((self->address == nil) && (cstr = [arg cString])) {
          if (isdigit(*cstr)) {
            self->address =
              [NGInternetSocketAddress wildcardAddressWithPort:[arg intValue]];
          }
        }
        if (self->address == nil)
          self->address = NGSocketAddressFromString(arg);
      }
    }
    self->address = [self->address retain];
    
    if (self->address == nil) {
      [_application logWithFormat:
                      @"got no address for HTTP server (using arg '%@')", arg];
      [self release];
      return nil;
    }
    
    if (!WOContactSNS) {
      [_application logWithFormat:@"%@ listening on address %@",
                      NSStringFromClass([self class]),
                      [(id)self->address stringValue]];
    }
    
    self->lock = [[NSRecursiveLock alloc] init];
    
    self->maxThreadCount = [[WOCoreApplication workerThreadCount] intValue];
    
    [self setSendTimeout:WOHttpAdaptorSendTimeout];
    [self setReceiveTimeout:WOHttpAdaptorReceiveTimeout];
  }
  return self;
}

- (void)dealloc {
  [[UnixSignalHandler    sharedHandler] removeObserver:self];
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [self->lock    release];
  [self->socket  release];
  [self->address release];
  [super dealloc];
}

/* accessors */

- (id<NGSocketAddress>)socketAddress {
  /* used by sns */
  return self->address;
}

/* events */

- (void)handleSIGPIPE:(int)_signal {
  [self logWithFormat:@"caught SIGPIPE !"];
}

- (void)registerForEvents {
  int backlog;
  
  backlog = [[WOCoreApplication listenQueueSize] intValue];
  
  if (backlog == 0)
    backlog = 5;
  
  [self->socket release]; self->socket = nil;
  
  self->socket =
    [[NGPassiveSocket alloc] initWithDomain:[self->address domain]];
  
  [self->socket bindToAddress:self->address];
  
  if ([[self->address domain] isEqual:[NGInternetSocketDomain domain]]) {
    if ([(NGInternetSocketAddress *)self->address port] == 0) {
      /* let the kernel choose an IP address */
      
      [self debugWithFormat:@"bound to wildcard: %@", self->address];
      [self debugWithFormat:@"got local: %@", [self->socket localAddress]];
      
      self->address = [[self->socket localAddress] retain];
      
      [self logWithFormat:@"bound to kernel assigned address %@: %@",
              self->address, self->socket];
    }
  }
  
  [self->socket listenWithBacklog:backlog];
  
  [[NSNotificationCenter defaultCenter]
                         addObserver:self selector:@selector(acceptConnection:)
                         name:NSFileObjectBecameActiveNotificationName
                         object:self->socket];
  [(WORunLoop *)[WORunLoop currentRunLoop]
              addFileObject:self->socket
              activities:NSPosixReadableActivity
              forMode:NSDefaultRunLoopMode];
}
- (void)unregisterForEvents {
  [(WORunLoop *)[WORunLoop currentRunLoop]
              removeFileObject:self->socket forMode:NSDefaultRunLoopMode];
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  
  [self->lock   release]; self->lock   = nil;
  [self->socket release]; self->socket = nil;
}

/* debugging */

- (BOOL)isDebuggingEnabled {
  return debugOn;
}

/* description */

- (NSString *)description {
  return [NSString stringWithFormat:@"<%@[0x%08X]: address=%@>",
                     NSStringFromClass([self class]), self,
                     self->address];
}

/* Server */

/* accessors */

- (id<NGPassiveSocket>)socket {
  return self->socket;
}
- (id<NGSocketAddress>)serverAddress {
  return [self->socket localAddress];
}

- (void)setSendTimeout:(NSTimeInterval)_timeout {
  self->sendTimeout = _timeout;
}
- (NSTimeInterval)sendTimeout {
  return self->sendTimeout;
}

- (void)setReceiveTimeout:(NSTimeInterval)_timeout {
  self->receiveTimeout = _timeout;
}
- (NSTimeInterval)receiveTimeout {
  return self->receiveTimeout;
}

- (void)setMaxThreadCount:(int)_count {
  self->maxThreadCount = _count;
}
- (int)maxThreadCount {
  return self->maxThreadCount;
}

/* logging */

- (void)logWithFormat:(NSString *)_format, ... {
  NSString *value = nil;
  va_list  ap;

  va_start(ap, _format);
  value = [[NSString alloc] initWithFormat:_format arguments:ap];
  va_end(ap);

  NSLog(@"WOHttpAdaptor: %@", value);
  [value release]; value = nil;
}

/* run-loop */

- (void)_serverCatched:(NSException *)_exc {
  [self logWithFormat:@"http server caught: %@", _exc];
  if (WOCoreOnHTTPAdaptorException) abort();
}

- (BOOL)runConnection:(id<NGActiveSocket>)_socket {
  WOHttpTransaction *tx;
  
  if (_socket == nil) {
    [self logWithFormat:@"got no socket for transaction ??"];
    return NO;
  }
  
  tx = [[WOHttpTransaction alloc] initWithSocket:_socket
                                  application:self->application];
  
  if (![tx run])
    [self _serverCatched:[tx lastException]];
  
  [tx release];
  
  if ([self->application isTerminating])
    self->isTerminated = YES;
  
  return YES;
}

- (void)_handleAcceptedConnection:(NGActiveSocket *)_connection {
#if USE_POOLS
  NSAutoreleasePool *pool = nil;
#endif
  NSTimeInterval t;
  
  if (HTTP_PERFLOG)
    *(&t) = [[NSDate date] timeIntervalSince1970];
  
  [self->lock lock];
  self->activeThreadCount++;
  [self->lock unlock];
  
#if USE_POOLS
  pool = [[NSAutoreleasePool alloc] init];
#endif
  {
    [*(&_connection) autorelease];
    
    NS_DURING {
      [_connection setReceiveTimeout:self->receiveTimeout];
      [_connection setSendTimeout:self->sendTimeout];
      
      [self runConnection:_connection];
    }
    NS_HANDLER {
      [self _serverCatched:localException];
    }
    NS_ENDHANDLER;
  }
#if USE_POOLS
  [pool release]; pool = nil;
#endif

  [self->lock lock];
  self->activeThreadCount--;
  [self->lock unlock];

  if (HTTP_PERFLOG) {
    t = [[NSDate date] timeIntervalSince1970] - t;
    [self logWithFormat:@"handling of request took %4.3fs.",
            t < 0.0 ? -1.0 : t];
  }
}

- (NSArray *)allowedHostNames {
  return allow;
}

- (id<NGActiveSocket>)_checkAccessOnConnection:(id<NGActiveSocket>)_connection{
  static NGInternetSocketDomain *ipDomain = nil;
  id<NGSocketAddress> remote;

  if ((remote = [_connection remoteAddress]) == nil) {
    [self logWithFormat:
            @"missing remote address for connection: %@",
            _connection];
    return nil;
  }

  if (ipDomain == nil)
    ipDomain = [[NGInternetSocketDomain domain] retain];
  
  if ([[remote domain] isEqual:ipDomain]) {
    /* check access */
    NGInternetSocketAddress *ipAddr = (id)remote;
    NSArray *allow = nil;
    unsigned i, count;
    NSString *rh, *ra;
    
    allow = [self allowedHostNames];
    
    rh = [ipAddr hostName];
    ra = [ipAddr address];
    
    /* first check address */
    
    for (i = 0, count = [allow count]; i < count; i++) {
      NSString *h;
      
      h = [[allow objectAtIndex:i] stringValue];
      if ([h isEqualToString:ra])
        return _connection;
    }
    
    /* now check DNS names */
    
    for (i = 0, count = [allow count]; i < count; i++) {
      NSString *h;
      
      h = [[allow objectAtIndex:i] stringValue];
      if ([h isEqualToString:rh])
        return _connection;
    }
    
    [self logWithFormat:@"ACCESS DENIED: %@", ipAddr];
    _connection = nil;
  }
  
  return _connection;
}

- (void)acceptConnection:(id)_notification {
#if USE_POOLS
  NSAutoreleasePool *pool;
  *(&pool) = [[NSAutoreleasePool alloc] init];
#endif
  {
    NGActiveSocket *connection;
    
    NS_DURING {
      *(&connection) = (NGActiveSocket *)[self->socket accept];
      if (connection == nil)
        [self _serverCatched:[self->socket lastException]];
      else
	[self debugWithFormat:@"accepted connection: %@", connection];
    }
    NS_HANDLER {
      connection = nil;
      [self _serverCatched:localException];
    }
    NS_ENDHANDLER;
    
    connection = (NGActiveSocket *)[self _checkAccessOnConnection:connection];
    
    if (connection != nil) {
      if (self->maxThreadCount <= 1) {
        NS_DURING
          [self _handleAcceptedConnection:[connection retain]];
        NS_HANDLER
          [self _serverCatched:localException];
        NS_ENDHANDLER;
      }
      else {
        [NSThread detachNewThreadSelector:@selector(_handleAcceptedConnection:)
                  toTarget:self
                  withObject:[connection retain]];
        [self logWithFormat:@"detached new thread for request."];
        //[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
      }
      connection = nil;
    }
  }
#if USE_POOLS
  [pool release]; pool = nil;
#endif
  
  if (self->isTerminated) {
    if (self->socket) {
      [[NSNotificationCenter defaultCenter]
                             removeObserver:self
                             name:NSFileObjectBecameActiveNotificationName
                             object:self->socket];
      [self->socket close];
      [self->socket release]; self->socket = nil;
    }
    NSLog(@"adaptor stops application: %@ ...", self->application);
    exit(0);
  }
}

@end /* WOHttpAdaptor */
