/***************************************************************************

    supportware
    Copyright (C) 2001-2008 by c.holzheuer
    c.holzheuer@sourceworx.org

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

***************************************************************************/


#include <ddwebserver.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <ddwebsite.h>
#include <ddthread.h>
#include <sys/stat.h>
#include <strings.h>
#include <stdlib.h>
#include <string.h>

using namespace std;


/**
	An einem Port lauschen.
	@param port der port.
	@param backlog backlog für den syscall 'listen'
*/

void ddwebsocket::listen( int port, int backlog )
{
	
	_fd = ::socket(AF_INET, SOCK_STREAM, 0 );
	::fcntl( _fd, F_SETFD, FD_CLOEXEC );
	if( _fd == -1 )
		log_fatal( "ddwebsocket::listen ::socket" );
	int xx = 1;
	if( ::setsockopt( _fd, SOL_SOCKET, SO_REUSEADDR, &xx, sizeof xx ) == -1 )
		log_fatal( "ddwebsocket::listen ::setsockopt: cannot set re-use flag" );

	// bind
	_sa_local.sin_family = AF_INET;
	_sa_local.sin_port = htons(port);
	_sa_local.sin_addr.s_addr = INADDR_ANY;
	bzero(&(_sa_local.sin_zero), 8);

	if( ::bind( _fd, (struct sockaddr *)&_sa_local, sizeof(struct sockaddr)) == -1 )
		log_fatal( "ddwebsocket::listen ::bind" );

	// listen
	if( ::listen( _fd, backlog ) == -1 )
		log_fatal( "ddwebsocket::listen ::listen" );

}


/**
	Schließt das socket.
*/

void ddwebsocket::close()
{
	::close( _fd );
}


/***************************************************************************

    supportware
    Copyright (C) 2001-2008 by c.holzheuer
    c.holzheuer@sourceworx.org

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

***************************************************************************/


/**
	Internal setup.
*/

ddwebconfig::ddwebconfig()
{
	add_option( "port", "-p", "--port", "8080" );
	add_option( "user", "-u", "--user" );
	add_option( "chroot", "-c", "--chroot" );
	add_option( "chdir", "-C", "--chdir" );
	add_option( "docroot", "-d", "--docroot", "docs" );
	add_option( "daemon", "-D", "--daemon", "true" );
	add_option( "watchdog", "-w", "--watchdog", "false" );
	add_option( "mail", "-m", "--mail", "c.holzheuer@sourceworx.org" );
}


/***************************************************************************

    supportware
    Copyright (C) 2001-2008 by c.holzheuer
    c.holzheuer@sourceworx.org

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

***************************************************************************/


/**
	Default constructor
*/

ddwebserver::ddwebserver()
:	_maxconnections( 0 ), _timeout( 180 ), _pollfds( 0 ), _halted( false )
{

}


/**
	Copy constructor
*/

ddwebserver::ddwebserver( const ddwebserver& src )
: _halted(false)
{
	*this = src;
}


/**
	Destructor.	Killt alle connections.
*/

ddwebserver::~ddwebserver()
{
	delete[] _pollfds;
	for( size_t i=0; i < _allconnections.size(); ++i  )
		delete _allconnections[i];
}


/**
	Assignment. Verboten.
*/

ddwebserver& ddwebserver::operator=( const ddwebserver& src )
{
	if( this == &src )
		return *this;
	throw runtime_error( "ddwebserver::operator=: not done yet!");
	return *this;
}


/**
	Ein Dämon werden: Den Server-Prozess in den Hintergrund schicken.
*/

void ddwebserver::change_daemon()
{
	if( !_config["main"].get_bool( "daemon", true ) )
		return;
	if ( ::daemon( 1, 0 ) < 0 )
		throw runtime_error( "ddwebserver::change_daemon() failed" );
}


/**
	Die User-ID des Server-Prozesses ändern. GGf. ein chroot durchführen.
*/

void ddwebserver::change_user_root( const ddstring& user, const ddstring& newroot )
{
  cout <<  "change_user_root( const ddstring& user, const ddstring& newroot ) ABGESCHALTET!" << endl;
	return;
	
	uid_t uid = ::getuid();
  bool change_uid = false;

  // If we're root, start becoming someone else.
  if( !user.empty() && uid == 0 )
  {
		change_uid = true;
		struct passwd* pwd = ::getpwnam( user.c_str() );
   	if( pwd == (struct passwd*) 0 )
   		log_fatal( "ddwebserver::change_user failed: unknown user." );

   	// Set aux groups to null.
   	if( ::setgroups( 0, (gid_t*) 0 ) < 0 )
   		log_fatal( "ddwebserver::change_user failed: set aux groups failed." );

   	// Set primary group.
   	if( ::setgid( pwd->pw_gid ) < 0 )
   		log_fatal( "ddwebserver::change_user failed: set gid failed." );

   	// Try setting aux groups correctly - not critical if this fails.
   	if( ::initgroups( user.c_str(), pwd->pw_gid ) < 0 )
   		log_fatal("initgroups failed" );

   	// Save the new uid for setting after we chroot().
   	uid = pwd->pw_uid;
  }
  if( !newroot.empty() )
  {
  	if( ::chroot( newroot.c_str() ) < 0)
  		log_fatal( "ddwebserver::change_root failed for path: " + newroot );
  	if( ::chdir( "/" ) < 0)
  		log_fatal( "ddwebserver::change_dir failed for path: " + newroot );
 	}

  // finally Set uid.
  if( change_uid && ::setuid( uid ) < 0 )
		log_fatal( "ddwebserver::change_user failed: set uid failed." );

}



/**
	Einen signal-handler installieren.
*/

int ddwebserver::sig_init( int sig, ddfncall function )
{  
	struct sigaction act;
	// wild
	act.sa_handler = function;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	return sigaction(sig, &act, 0);
}


/**
	Standard signal handler. Wird nicht benutzt.
*/

void ddwebserver::sig_handler( int sig )
{
	ddstring signame = "(nu?)";
	
	switch( sig )
	{

		case SIGCHLD:
			
			signame = "SIGCHLD:";
			break;
	
		case SIGTERM:
		case SIGINT:
			
			signame = "SIGTERM SIGINT:";
			break;
	
		case SIGHUP:
		
			signame = "SIGHUP:";
			break;
	
		case SIGUSR1:
		
			signame = "SIGUSR1:";
			break;
	
		case SIGUSR2:
			
			signame = "SIGUSR2:";
			break;
	
		case SIGQUIT:
	
			signame = "SIGQUIT:";			
				break;
	
		case SIGSEGV:
	
			signame = "SIGSEGV:";				
			break;
	
		case SIGBUS:
	
			signame = "SIGBUS:";				
				break;
		
		case SIGFPE:
	
			signame = "SIGFPE:";				
			break;
		
		case SIGABRT:
	
			signame = "SIGABRT:";				
			break;
		
		case 	SIGALRM:
	
			signame = "SIGALRM:";
		
	}
	//ddlogdevice::log( " --- server crash by SIG: " + signame );
	::exit( EXIT_FAILURE );
	
}


/**
	Eine virtuelle Domain hinzufügen.
*/

void ddwebserver::add_site( const ddstring& key, ddwebsite* site, const ddstring& title )
{
	_sites.add_by_key( key, site );
	site->set_name( key );
	site->set_title( title );
}



/**
	halt-flag setzen, dann werden nur noch "Bitte später kommen"-Messages angezeigt.
*/

void ddwebserver::halt()
{
	_halted = true;
}

/**
	init & start server
*/

void ddwebserver::run( int argc, char** argv, const ddstring& configfile )
{
 
	try
	{
		/*
		// catch crashes
		sig_init( SIGSEGV, &ddwebserver::sig_handler );
		sig_init( SIGBUS,  &ddwebserver::sig_handler );
		sig_init( SIGABRT, &ddwebserver::sig_handler );
		sig_init( SIGFPE,  &ddwebserver::sig_handler );
		sig_init( SIGHUP,  &ddwebserver::sig_handler );
		sig_init( SIGTERM, &ddwebserver::sig_handler );
		sig_init( SIGQUIT, &ddwebserver::sig_handler );
		sig_init( SIGINT,  &ddwebserver::sig_handler );
		*/
		
		// setup start time
		_starttime = ddbasedate::now().to_string();
		
		log_error( "server ' " + ddstring(DDVERSION) + "' start at: " + _starttime );
		log_debug( "server ' " + ddstring(DDVERSION) + "' start at: " + _starttime );

		// init configoptions
		_configoptions.create( argc, argv );
		//_configoptions.dump();
	
		// the try to find the config ...
		if( !configfile.empty() )
		{
			if( !_config.load( configfile ) )
				throw runtime_error( "server init failed: config load error!" );
			_configoptions.sync( _config, "main" );
		}
	
		//_config.dump();
	
		// daemonize, if option is selected
		change_daemon();
	
		// throws...
		_mimemap.init();
	
		// listen to mastersocket on port 80
		init_connection_pool( _config["main"] );
	
		// after listening, we chroot etc.
		ddstring& user = _config["main"].get_entry( "user" );
		ddstring& home = _config["main"].get_entry( "chroot" );
		// change use_pathr/change root, if option is selected
		change_user_root( user, home );

		
		// throws...
		_usermanager.init_data( _config );
		
		ddstring& path2 = _config["main"].get_entry("default_templates", "sn_templates" );
		// init sites
		for( int i=0; i<_sites.size(); ++i )
		{
			// test if form contains this field
			ddwebsite& site = *_sites[i];
			ddstring aliases;
			site.init_site( this, path2, aliases );
			// on success, add alias keys.
			ddstringrow aliasvec( aliases, ";" );
			for( size_t x=0; x<aliasvec.size(); ++x )
				_sites.add_alias( site.get_name(), aliasvec[x] );
	
		}
	}
	catch( exception& error )
	{
		log_error( ddstring(" --- server init crash ") + error.what() );
		exit(-1);
	}
	
	
	// finally: The magic mainloop.
	time_t start_time = ::time(0);
	struct tm tm_now;
	::localtime_r( &start_time, &tm_now );
  int start_day = tm_now.tm_yday;
	
	log_error( "server ' " + ddstring(DDVERSION) + "' running at: " + _starttime );
	log_debug( "server ' " + ddstring(DDVERSION) + "' running at: " + _starttime );
	
	while( true )
  {
  
		
		
		// die watchlist füllen...
		prepare_connections();
	
		// ...ereignisse abholen...
		poll_connections();
	
		// handle connection io
		process_connections();


		// check for timer events
		time_t now = ::time(0);
		if( now-start_time > DDTIMERINTERVALL )
		{
			start_time += DDTIMERINTERVALL;
			// Notify all domains.
			for( int x=0; x<_sites.size(); ++x )
				_sites[x]->get_session_manager().cleanup_sessions();
		}
		
		// check for log rotation
		::localtime_r( &now, &tm_now );
		if( tm_now.tm_yday != start_day )
		{
			rotate_log_streams( start_time );
			start_time = now;
			start_day = tm_now.tm_yday;				
		}
		
  } // while
}


/**
	Die Tägliche Log-Rotation: Logs werden mit Datum versehen in xxx.old umbenannt und
	später per skript weg-gemoved. Ferner werden die Hit-Counter zurückgesetzt. 
*/

void ddwebserver::rotate_log_streams( time_t lasttime )
{
	for( int i=0; i<_sites.size(); ++i )
		_sites[i]->reset_request_counter();
	thelog.rotate_log_streams( lasttime );	
}


/**
	Nur der server kann entscheiden, wer einen request bearbeitet.
	die connection ist locked und später per call wieder frei-
	bzw. auf schreiben geschaltet.
	Das ist hier übrigens alles noch single threaded (serialized)
*/

int ddwebserver::webserver_handle_request( ddwebconnection& connection )
{
	try
  {
		
		if( _halted )
	  	return connection.connection_create_error( DDHINTHALTED, ddwebresponse::ret_503_service_unavailable );
		
		// wenn es die site gibt ...
		ddstring& hostname =  connection.get_request().get_host_name();
    int i = _sites.index_of( hostname );
    if( i<0 )
    	return connection.connection_create_error( "domain not found: " + hostname, ddwebresponse::ret_404_not_found );
		
		// Böse IPs für diese Site raus ...
		if(	get_blocked_ips().contains_key( connection.get_host_ip() ) )
			return connection.connection_create_error( "<h1>service temporarily unavailable</h1>", ddwebresponse::ret_503_service_unavailable );
		
		// ...soll die den request übernehmen ...
		// ...es sei denn, es ist ein file: 
		if( !_sites.get_entry(i).website_handle_request( connection ) )
			
			// FIX! hier muss entschieden werden, ob wir
			// aus dem mmap cache (TODO) oder per sendfile liefern.

   		return ddwebconnection::write_sendfile;
   		
   	return ddwebconnection::write_wait;

  }
  catch( exception& errorexcecption )
  {
     return connection.connection_create_error( ddstring("internal server exception: ") + errorexcecption.what() );
  }
	
}


/**
	Den connectionpool einrichten.
*/
	  
void ddwebserver::init_connection_pool( ddconfigsection& config )
{
 	_maxconnections = config.get_int( "max_connections", 256 ) + 2;
	_timeout = config.get_int( "timeout", 180 );
	
	_socket.listen( config.get_int( "port", 8088 ) );
	
	if( _pollfds )
		delete[] _pollfds;
  
	_pollfds	= new struct pollfd[ _maxconnections ];
		
	// hack
	_tmpconnections.push_back( 0 );
	_tmpconnections.push_back( 0 );
	 
	 // install mastersocket
	_pollfds[0].events = POLLIN;
	_pollfds[0].fd		 = _socket.get_fd();

  // Eine FIFO anlegen und daran lauschen als deppen-brute-force ipc.
	// ( Wenn ein seiten-renderer aka pagelet fertig ist, sagt er
	// über die fifo Bescheid )
  
  if( ::mkfifo( "dope.fifo", 0666 ) < 0 )
  {
		if( errno != EEXIST )
			throw runtime_error( "ddwebserver::init_fifo():mkfifo() failed!" );
	}
	_fifo = ::open( "dope.fifo", O_RDONLY|O_NONBLOCK );
  if( _fifo < 0 )
		throw runtime_error( "ddwebserver::init_fifo(): open failed" );
	
	// install fifo listener
	_pollfds[1].events = POLLIN;
	_pollfds[1].fd		 = _fifo;
	
}


/**
	Sucht eine freie Connection.
	@see ddwebconnection
*/

ddwebconnection* ddwebserver::find_connection()
{
	// first, try to find a free connection
	size_t i=0;
	for( ; i<_allconnections.size(); ++i )
	{
		// free conn found -> ok
		if( _allconnections[i]->is_free() )
			return _allconnections[i];
	}
	// no free conn found
	ddwebconnection* cfree = 0;
	// try to create a new conn...
	if( (int) i < _maxconnections )
	{
		cfree = new ddwebconnection( this, i );
		_allconnections.push_back( cfree );
		// also resize connidx
		_tmpconnections.push_back( 0 );
	}
	return cfree;
}


/**
	Füllt die Watchlist aktiver Connections und schliesst dabei
	"alte" Connections.
*/

void ddwebserver::prepare_connections()
{	
	
	_cfree = 0;
 	_fdno = 2;
	time_t now = ::time(0);

	// über alle existierenden connections ...
	for( size_t i = 0; i < _allconnections.size(); ++i  )
	{
		ddwebconnection* connection = _allconnections[i];		
					
		// check timeout
		size_t age = now - connection->get_last_action();
		if( !connection->is_free() && age > _timeout )
		{
			// hängende connections:
			// !locked heisst dann lesend, schreibend etc.
			// locked: hängen im generator
			if( !connection->is_locked() )
				connection->connection_handle_close();
			//else
			//	log_error( "ddwebserver::prepare_connections(): INNER timeout: " + connection->to_string() );		
		}
		
		// register all active connections ( reading, writing, waiting )
		if( connection->is_active() )
		{			
			_pollfds[_fdno].events 		= connection->is_writing() ? POLLOUT : POLLIN ;
			_pollfds[_fdno].fd     		= connection->get_fd();
			_tmpconnections[_fdno++]  = connection;
		}
		else if( connection->is_free() && !_cfree )
		{
			// store free connection to avoid lookup		
			_cfree = connection;
		}
	}
	_activeconnections = _fdno;
	
}


/**
	Polls all registered file descriptors (@see man 2 poll() ) and processs new connections
*/

void ddwebserver::poll_connections()
{
	// FIX: man epoll
	// #include <sys/epoll.h>
  // By contrast, when used as a level-triggered interface, epoll is simplay a faster poll(2), and can be used wherever the latter is used
  // since it shares the same semantics.


	// fehler ist hier noch nie vorgekommen 
	// und eh nicht behandelbar.
	if( ::poll( _pollfds, _fdno, DDPOLLTIMEOUT ) < 0 )
		return;
	
	// DAS ist ziemlicher bullshit: hier wurde nur reingeschrieben,
	// um den poll 'aufzuwecken'

	if( _pollfds[1].revents & POLLIN )
		::read( _pollfds[1].fd, _dummybuf, ::strlen(DDFIFONOTIFIER) );
	
	// test for new connections
	if( _pollfds[0].revents & POLLIN )
	{
		if( !_cfree )
			_cfree = find_connection();
		if( !_cfree )
			return;
		if( _cfree->connection_handle_accept( _socket.get_fd() ) < 0 )
			log_error("ddwebserver::accept_connection(): master socket accept failed" );	
	}

}


/**
	Behandelt die IO-Events nach dem ::poll()
*/

void ddwebserver::process_connections()
{
	
	// test for change in existing connections
	for( int i =2; i < _fdno; ++i  )
	{
		short event = _pollfds[i].revents;
		// kaputt ?
		if( event & ( POLLERR | POLLHUP ) )
		{
      _tmpconnections[i]->connection_handle_close();
			continue;
		}
		
		/*
		// neu: auch 'hängende' connections, in denen noch pipelined requests warten,
  	// behandeln.
  	
  	if( _tmpconnections[i]->is_pending() )
  		_tmpconnections[i]->handle_test_buffer();
  	*/
  	
  	// es tut sich was: die behandlung von det janze findet
		// in der klasse ddwebconnection statt.
  	
  	if( event & ( POLLIN | POLLOUT ) )
  		_tmpconnections[i]->connection_handle_state();
  	
  
  } // for
		
}

//--



