#!/usr/bin/perl -w

#$Header: /home2/cvsroot/LogTrend/StorageServer/StorageServer,v 1.21.2.2 2002/02/20 17:11:34 lsimonneau Exp $
##******************************************************************************
## StorageServer.pl
##  Description  : The server to store informations from agents
##  Project      : LogTrend 1.0.0.0 - Atrid Systemes
##  Author       : Sylvain Lhullier s.lhullier@atrid.fr (01.03.2001)
##  Author       : David Mascle d.mascle@atrid.fr
##******************************************************************************
#$Log: StorageServer,v $
#Revision 1.21.2.2  2002/02/20 17:11:34  lsimonneau
#*** empty log message ***
#
#Revision 1.21.2.1  2002/02/14 15:43:30  lsimonneau
#Minor features enhancement
#Add a Sender attribut in the Mail tag of agents config file.
#Use SMTP Server when available or /usr/lib/sendmail otherwise.
#Add the -s <smtp_server> option to MailBridge.pl.
#
#Revision 1.21  2002/01/16 15:13:03  slhullier
#sendmail -f$self->{ADMINMAIL}
#
#Revision 1.20  2002/01/08 12:42:56  lsimonneau
#Major bugfixes : Correct signature crash in daemon mode and use /usr/lib/sendmail -t -i
#to send mail insetead of MIME::Entity->smtpsend
#
#Revision 1.19  2001/11/19 10:41:02  lsimonneau
#Critical bugfixes.
#
#Revision 1.18  2001/11/16 10:25:18  lsimonneau
#Major feature enhancement : Implementation of authentification with RSA signature.
#
#IMPORTANT : This StorageServer is incompatible with older version.
#
#Revision 1.17  2001/09/27 16:15:23  slhullier
#Config files in /etc/LogTrend
#
#Revision 1.16  2001/07/24 14:38:41  slhullier
#
#Scripting for Visu
#
#Revision 1.15  2001/07/10 12:55:17  slhullier
#*** empty log message ***
#
#Revision 1.14  2001/06/08 07:42:12  slhullier
#
#Maj __WARN__ et __DIE__
#
#Revision 1.13  2001/06/07 14:25:41  slhullier
#
#Passage de  unshift @INC,'..';  aux packages Logtrend::....
#
#Revision 1.12  2001/05/29 12:43:30  fdesar
#
#Correction typo : deamon -> daemon
#
#Revision 1.11  2001/03/30 12:58:07  slhullier
#
#Le serveur ne fait plus de messages sur la console  +  gzip : qq bugs
#Mise en place d'un nv StorageServer sur serveur
#
#Revision 1.10  2001/03/22 13:32:24  slhullier
#
#Petit bug du serveur du au parseur
#
#Revision 1.9  2001/03/19 15:27:04  slhullier
#
#Ajout d'un fichier de conf pour le serveur
#A tester en situation reelle.
#
#Revision 1.8  2001/03/14 09:10:46  slhullier
#
#Die moins stupides
#
#Revision 1.7  2001/03/12 14:02:34  slhullier
#
#petite erreur d'affichage reparee
#
#Revision 1.6  2001/03/07 15:15:30  slhullier
#
#LogDie mis en place sur le serveur et l'agent Linux
#
#Revision 1.5  2001/03/06 17:31:59  slhullier
#
#Proc d'install revue + debut de syslog
#
#Revision 1.4  2001/03/01 17:21:37  slhullier
#
#Commentaires
#
#Revision 1.3  2001/03/01 16:40:37  slhullier
#
#Fautes
#
#Revision 1.2  2001/03/01 16:28:50  slhullier
#
#Premier parcours des fichiers
#
#Revision 1.1.1.1  2001/02/06 09:48:28  fdubuy
#
#First CVS version : all work done by D.Mascle
#
#

use strict;
use IO::Socket;
use Sys::Hostname;
use POSIX;
use XML::DOM;
use Error qw(:try);
use MIME::Entity;
use Mail::Mailer;


use LogTrend::Common::LogDie;
use LogTrend::Crypto::KeyManager;
use LogTrend::StorageServer::PostgreSQLDataAlarms;
use LogTrend::StorageServer::StorageProtocol;
use LogTrend::StorageServer::ErrorDeclaration;

my $name = "StorageServer";
my $version = "1.0.0.0";
my $serverinformation = "$name version $version";
my $server_listen_default = 5;
my $default_conf = "/etc/LogTrend/$name.conf";

##******************************************************************************
## Function display_help_and_exit
##******************************************************************************
sub display_help_and_exit
{
   my $status = shift;

   print "\n";
   print "Usage : $name -f [<conf-file>]\n";
   print "        $name [-D] -s <serverport> [-l <nbmax>] \\\n";
   print "                      -d <database> [-H <host>] [-P <port>] \\\n";
   print "                      -u <username> -p <password> \n";
   print "        $name --version\n";
   print "\n";
   print "Options :\n";
   print "   -f : run the serveur with config file (default=$default_conf).\n";
   print "   -D : run as daemon (detach form termial, syslog...).\n";
   print "   -s : LogTrend storage server's port.\n";
   print "   -l : maximun number of simultaneous connections (optional, default=$server_listen_default).\n";
   print "   -d : database name.\n";
   print "   -H : host name of the database server (optional).\n";
   print "   -P : port number of the database server (optional).\n";
   print "   -u : database user.\n";
   print "   -p : database user's password.\n";
   print "\n";
   exit( $status );
}

##******************************************************************************
## Function parse_xml
##******************************************************************************
sub parse_xml
{
   my ($file, $local_server_port, $server_listen, $database_name,
       $database_host, $database_port, $database_username, $database_password,
       $daemon_mode) = @_;

   ##===========================================================================
   ## Tag 'Configuration'
   ##===========================================================================
   my $parser = new XML::DOM::Parser() || die("XML::DOM::Parser: $!");
   open( FILE, "$file" ) || die("$file: $!");
   close(FILE);
   my $doc = $parser->parsefile( $file ) || die("$file :$!");

   my $rootlist = $doc->getElementsByTagName("Configuration") ||
                                        die("$file :No \"Configuration\" tag.");
   my $rootnode = $rootlist->item(0) || die("$file :No \"Configuration\" tag.");

   ##===========================================================================
   ## Tag 'Server'
   ##===========================================================================
   my $list = $rootnode->getElementsByTagName("Server") || die("$file: No \"Server\" tag.");
   my $node = $list->item(0) || die("No \"Server\" tag.");
   my $attributes = $node->getAttributes() || die("$file: Error in \"Server\" tag.");

   ##---------------------------------------------------------------------------
   my $attrnode = $attributes->getNamedItem("Port") ||
                   die("$file: No 'Port' field in \"Server\" tag.");
   $$local_server_port = $attrnode->getValue();

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("SimultaneousConnections");
   if( defined( $attrnode ) ) { $$server_listen = $attrnode->getValue(); }

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("Daemon") ||
                   die("$file: No 'Daemon' field in \"Server\" tag.");
   my $val = $attrnode->getValue();
   if(   $val eq "yes" ) { $$daemon_mode = 1; }
   elsif( $val eq "no" ) { $$daemon_mode = 0; }
   else { die("$file: \"Server\" tag: 'Daemon' field: no such value."); }

   ##===========================================================================
   ## Tag 'DataBase'
   ##===========================================================================
   $list = $rootnode->getElementsByTagName("DataBase") ||
                                             die("$file: No \"DataBase\" tag.");
   $node = $list->item(0) || die("No \"DataBase\" tag.");
   $attributes = $node->getAttributes() || die("$file: Error in \"DataBase\" tag.");

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("Name") ||
                   die("$file: No 'Name' field in \"DataBase\" tag.");
   $$database_name = $attrnode->getValue();

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("Host");
   if( defined( $attrnode ) ) { $$database_host = $attrnode->getValue(); }

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("Port");
   if( defined( $attrnode ) ) { $$database_port = $attrnode->getValue(); }

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("User") ||
                   die("$file: No 'User' field in \"DataBase\" tag.");
   $$database_username = $attrnode->getValue();

   ##---------------------------------------------------------------------------
   $attrnode = $attributes->getNamedItem("Password") ||
                   die("$file: No 'Password' field in \"DataBase\" tag.");
   $$database_password = $attrnode->getValue();

}

##******************************************************************************
## Function daemon_mode
##******************************************************************************
sub daemon_mode
{
   OpenLog( $name, "daemon" );
   chdir '/' || die( "chdir / :$!" );
   open( STDIN, "</dev/null");
   open( STDOUT, ">/dev/null");
   open( STDERR, ">/dev/null");
   if( ( my $x = fork() ) > 0 )
   { exit(0); }
   elsif( $x == -1 )
   { die("fork: $!"); }

   die("setsid: $!") if( setsid() == -1 );

   open( FILE, ">/var/run/$name.pid" ) || return;
   printf FILE "$$\n";
   close FILE;
}

##******************************************************************************
## Function child_work
##******************************************************************************
sub child_work
{
   my( $database_name, $database_host, $database_port, $database_username,
       $database_password, $new_sock,
       $authentication_interface) = @_;

   #fcntl($new_sock, F_SETFL(), O_NONBLOCK());

   my $storage_interface;
   my $storage_protocol;
   try {
       ##===========================================================================
       ## Creation of a storage interface
       ##===========================================================================
        $storage_interface = 
	  LogTrend::StorageServer::PostgreSQLDataAlarms->new($database_name, $database_host,
							     $database_port,
							     $database_username,
							     $database_password);

       ##===========================================================================
       ## Creation of a storage protocol
       ##===========================================================================
       $storage_protocol = 
	 LogTrend::StorageServer::StorageProtocol->new($authentication_interface,
						       $storage_interface);

   }

   ## Catch Error::DB_Connection
   catch Error::DB_Connection with {
       my $error = shift;
       
       print $new_sock "ERROR DB_CONNECT $error->{-text}\n.\n";
       close $new_sock;
       exit(0);
   };

   my $peerhost = $new_sock->peerhost();
   my $peerport = $new_sock->peerport();
   Message("a new agent has been connected via ".$peerhost.":".$peerport );

   my $message = "";
   my $communicationterminated = 0;
   my $nb_prot_error = 0;
   while ((defined (my $buf = <$new_sock>)) && (!$communicationterminated)) {
       $buf =~ s/\r\n/\n/gsm;

       my $chompedbuf = $buf;
       chomp($chompedbuf);       

       if ($chompedbuf ne ".") # the message is complete
       {
	   $message = $message.$buf;
	   next;
       }
       
       # processing of the client's message
       my $response;
       try {
	   my $answer = $storage_protocol->ProcessInput($message);
	   $communicationterminated = 1 if $answer == 1;

	   $response = "OK";
       }
       
       ## Error::SP_XML error while parsing the xml representation of data
       ## Break the connection
       catch Error::SP_XML with {
	   my $error = shift;

	   $response =  "ERROR XML $error->{-text}";
	   $communicationterminated = 1;
       }
       
       ## Error::SP_Internal Internal error, break the connection.
       catch Error::SP_Internal with {
	   my $error = shift;

	   $response =  "ERROR INTERNAL $error->{-text}";
	   $communicationterminated = 1;
       }
       
       ## Error::SP_Protocol protocol error, print a command usage information.
       ## Break the connection after 3 protocol error and a message to SysLog.
       catch Error::SP_Protocol with {
	   my $error = shift;

	   $response =  "ERROR PROTOCOL $error->{-text}";

	   $nb_prot_error++;
	   
	   if($nb_prot_error == 3) {
	       $communicationterminated = 1;
	       SysLog("Too much protocol error from host $peerhost");
	   }
       }
       
       ## Error::DB_Request Database request error.
       ## Break the connection
       catch Error::DB_Request with {
	   my $error = shift;

	   $response =  "ERROR DB_REQUEST $error->{-text}";
	   $communicationterminated = 1;
       }
       
       ## Error::DB_AgentDescription Database Agent Description error.
       ## Break the connection
       catch Error::DB_AgentDescription with {
	   my $error = shift;

	   $response =  "ERROR DB_AGENTDESCRIPTION $error->{-text}";
	   $communicationterminated = 1;
       }

       ## Error::Not_A_Signature Authentication error, the signature is not valid.
       ## Break the connection and log the error with SysLog
       catch Error::Not_A_Signature with {
	   my $error = shift;

	   $response = "ERROR NOT_A_SIGNATURE";
	   $communicationterminated = 1;
	   
	   SysLog("Authentication error for host $peerhost (Invalid Signature)");
       }

       ## Error::Authentication error, the signature is valid but is not authenticated.
       ## Break the connection and log the error with SysLog
       catch Error::Authentication with {
	   my $error = shift;

	   $response = "ERROR AUTHENTICATION";
	   $communicationterminated = 1;
	   
	   SysLog("Authentication error for host $peerhost (Can't authenticate the source)");
       }
  
       ## Error::Authentication error, the signature is valid but is not authenticated.
       ## Break the connection and log the error with SysLog
       catch Error::Permission with {
	   my $error = shift;

	   $response = "ERROR PERMISSION $error->{-text}";
	   $communicationterminated = 1;
	   
	   SysLog("Permission error for host $peerhost ($error->{-text})");
       }
  
       ## Error::IO error, IO error while reloading keys from database
       ## die and send a mail to system admin
       catch Error::IO with {
	   my $error = shift;

	   print $new_sock "ERROR CONNECT\n.\n";	   
	   close $new_sock;
	   Die("The LogTrend StorageServer is dead : $error->{-text}.");
	   die("$error->{-text}.");
       };

       print $new_sock $response."\n.\n";

       $message = "";

   }
   Message("the agent connected via ".$peerhost.":".$peerport.
	   "has stopped its connection to the LogTrend's storage server.");
      
   exit(0);   # Child process exits when it is done.
}




##******************************************************************************
## Function main
##******************************************************************************
sub main
{
   # server variables
   my $local_server_host = hostname();
   my $local_server_port = "5432";
   my $config_file = "";
   my $option_f = 0;
   my $daemon_m = 0;
   my $server_listen;

   # database server variables
   my $database_name = "";
   my $database_host = "";
   my $database_port = "";
   my $database_username = "";
   my $database_password = "";

   display_help_and_exit(1) if( @ARGV == 0 );

   for (my $i = 0; $i < (@ARGV); $i++)
   {
      if ($ARGV[$i] eq "--help")
      { display_help_and_exit(2); }
      elsif ($ARGV[$i] eq "--help")
      { print "$serverinformation\n"; exit( 0 ); }
      elsif ($ARGV[$i] eq "-s")
      { $local_server_port = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-l")
      { $server_listen = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-d")
      { $database_name = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-H")
      { $database_host = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-P")
      { $database_port = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-u")
      { $database_username = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-p")
      { $database_password = $ARGV[$i + 1]; $i++; }
      elsif ($ARGV[$i] eq "-D")
      { $daemon_m = 1 }
      elsif ($ARGV[$i] eq "-f")
      { $option_f=1; $config_file = $ARGV[$i+1]; $i++; }
      else { display_help_and_exit(3); };
   }


   if( $option_f == 0 )
   {
      if( ($local_server_port eq "") || ($database_name     eq "") ||
          ($database_username eq "") || ($database_password eq "") )
      { display_help_and_exit(4); }
   }
   else
   {
      if( !defined($config_file) || $config_file eq "" )
      { $config_file = $default_conf; }

      if( ($database_name     ne "") || ($database_host     ne "") ||
          ($database_port     ne "") || ($database_username ne "") ||
          ($database_password ne "") || ($daemon_m          != 0) )
      { display_help_and_exit(5); }

      parse_xml( $config_file, \$local_server_port, \$server_listen, \$database_name,
                 \$database_host, \$database_port, \$database_username,
                 \$database_password, \$daemon_m );
      if( $server_listen eq "" )
      { $server_listen = $server_listen_default; }

   }

   $database_host = "localhost" unless $database_host ne "";
   $database_port = 5432 unless $database_port ne "";


   ##==========================================================================
   ## Main process
   ##==========================================================================

   $SIG{CHLD} = sub {wait ()};

   # creation of the server's main socket
   my $main_sock = new IO::Socket::INET( LocalHost => $local_server_host,
                          LocalPort => $local_server_port, Listen => $server_listen,
                          Proto => 'tcp', Reuse => 1) || die("IO::Socket::INET: $!");


   ##===========================================================================
   ## Creation of an authentication interface
   ##===========================================================================
   my $authentication_interface;
   try {
       $authentication_interface = 
	 LogTrend::Crypto::KeyManager->new($database_name, $database_host, $database_port,
					   $database_username, $database_password);   
       
   }
   ## Catch Error::IO thrown by LogTrend::Crypto::KeyManager 
   catch Error::IO with {
       my $error = shift;
       
       die "IO error in file $error->{-file} at line $error->{-line} : $error->{-text}";
   };   


   ##===========================================================================
   ## Connect signals
   ##===========================================================================
   if( $daemon_m == 1 )
   {
      daemon_mode();
      $SIG{__WARN__} = sub { LogTrend::Common::LogDie::SysLog($_[0]) };
   }
   
   $SIG{INT} = sub { undef $authentication_interface if defined $authentication_interface; exit(-1)};
   $SIG{TERM} = sub { undef $authentication_interface if defined $authentication_interface; exit(-1)};

   # we accept new connections
   Message("Ok, waiting connections");
   ACCEPT: while( my $new_sock = $main_sock->accept() )
   {
      # creation of a child of the main process
      my $pid = fork();
      if( !defined $pid )
      {
         SysLog("fork : $!");
         next ACCEPT;
      }

      if( $pid != 0 )
      { # else it's the parent process, which goes back to accept()
         next ACCEPT;
      }

      child_work( $database_name,     $database_host,     $database_port,
                  $database_username, $database_password, $new_sock,
		  $authentication_interface);
   }

   close( $main_sock );
}

##******************************************************************************
main();

