#!/usr/bin/perl -w

require 5.000;
use lib '/usr/local/bin', 'c:/sysconf/client';
# Verfügbar unter: http://www.loescher-online.de/progdata/slutil.pm
use slutil 2016.0111;
use FileHandle;
use English;
use RemoteServerSSL 2018.0912;

######################################################################
### Unterprogramm-Funktionen für interne Zwecke
######################################################################

sub debug;      # Ausgabe von Informationen
sub warning;    # Ausgabe von Warnungen
sub error;      # Ausgabe von Fehlern
sub myexit;     # Exit und Aufräumen
sub loginit;    # Schreibt einen kleinen Header ins LOG-file
sub logdie;     # Schreibt einen Text ins LOG-file und stirbt dann
sub logprint;   # Schreibt einen Text ins LOG-file

######################################################################
### Voreinstellungen
######################################################################

$version = '2022.0307';
$appname = 'SysConf-Client-SSL';

$rcfile = "\L$appname\Erc"; # Name des Konfigurationsfiles

# Changelog:
# 1.0.7:
# - Codebasis von SysConf-Client 1.0.7 übernommen
# 1.0.8:
# - Neue FILE Option: seccon (Security Context)
# 1.0.9:
# - zusätzliche Prüfung des commonName
# 1.0.10:
# - Englische Meldungen
# 1.0.11:
# - Anpassungen an Windows
# 2019.1204:
# - Optimierung bei AUTHORIZED_HOSTS
#

my $TempDir = '/tmp';
if ($osname eq 'MSWin32')
{
  $TempDir = 'c:\temp';
}

# Wohin soll das logging erfolgen?
# $logfile = ">>/tmp/\L$appname\E.log"; # Log in eine Datei
# $logfile = ">&STDERR"; # Log auf STDERR
$logfile = ReadConfigFile_Logfile();

# Exitcodes beim Beenden
$NormalExitCode = 0;
$ErrorExitCode  = 1;

######################################################################
### Log-File und Signal-Handler
######################################################################

# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
open(LOG, ">>$logfile") || die "Can not open logfile '$logfile' for writing!\n";
chmod(0600,$logfile);
select(LOG); $|=1; select(STDOUT); $|=1; # Buffer ausschalten
loginit;

# Signal-Handler installieren
$SIG{HUP}  = \&catch_signal;
$SIG{INT}  = \&catch_signal;
$SIG{QUIT} = \&catch_signal;
$SIG{ABRT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
if ($osname ne 'MSWin32')
{
  $SIG{USR1} = \&catch_signal_usr1;
}
$SIG{'__WARN__'} = \&catch_warning;

$debug = $FALSE;

while ( (defined $ARGV[0]) && ($ARGV[0] =~ /^-/) )
{
 OPTION:
  {
    if ($ARGV[0] =~ /^-d/)
    {
      $debug = $TRUE; shift @ARGV; last OPTION
    }
    # Sonst Hilfe ausgeben:
    Hilfe();
  }
}


######################################################################
### Hauptprogramm
######################################################################

my $ssl_ca_file          = '';
my $ssl_cert_file        = '';
my $ssl_key_file         = '';
my $ssl_peer_common_name = '';

ReadConfigFile();

debug "PORT:                 $port\n";
debug "AUTHORIZED_HOSTS:     ",join(' ',@authorized_hosts),"\n";
debug "SSL_CA_FILE:          $ssl_ca_file\n";
debug "SSL_CERT_FILE:        $ssl_cert_file\n";
debug "SSL_KEY_FILE:         $ssl_key_file\n";
debug "SSL_PEER_COMMON_NAME: $ssl_peer_common_name\n";

# Logfile-Unterfunktionen in das Remote-Modul einhängen
RemoteServerSSL::SetLoggingAndExitFunctions(
					    exit     => \&myexit,
					    logprint => \&logprint,
					    debug    => \&debug,
					    warning  => \&warning,
					    error    => \&error,
					    logdie   => \&logdie,
					    restart  => \&restart,
					   );

logprint "Server-protocol-version: ",$RemoteServerSSL::VERSION,"\n";
$server = RemoteServerSSL::new(
			       $port,
			       $ssl_ca_file,
			       $ssl_cert_file,
			       $ssl_key_file,
			       $ssl_peer_common_name,
			       @authorized_hosts
			      );

# STDIN, STDOUT und STDERR schliessen, da man sysconf-client-ssl sonst so
# starten müsste: sysconf-client-ssl </dev/null >/dev/null 2>/dev/null
# So gehts nicht:
#close(STDIN);
#close(STDOUT);
#close(STDERR);

# Unter Windows als Windows Service starten
if (
    ($osname eq "MSWin32") &&
    eval("use Win32::Daemon; 1;")
   )
{
  logprint "Windows: setup...\n";
  use if $^O eq 'MSWin32', Win32::Daemon;

  # Unter Windows funktioniert die Signalisierung für das
  # RESTART-AFTER-QUIT nicht.
  # Daher machen wir das per default:
  RemoteServerSSL::SetRestartAfterQuit();

  # Callbacks einrichten, speziell für das Starten und Stoppen!
  logprint "Windows: installing callbacks...\n";
  Win32::Daemon::RegisterCallbacks( {
				     start        => \&Callback_Start,
				     stop         => \&Callback_Stop,
				     running      => \&Callback_Running,
				     param_change => \&Callback_Param_Change,
				    } );
  %Context = (
	      last_state => SERVICE_STOPPED,
	      start_time => time(),
	     );
#  $pid = 0;

  sub Callback_Start
  {
    my ($Event, $Context) = @_;
    logprint "Windows: Callback_Start() invoked.\n";
    $Context->{last_state} = SERVICE_RUNNING;
    Win32::Daemon::State( SERVICE_RUNNING );
  }

  sub Callback_Running
  {
    my ($Event, $Context) = @_;
    debug "Windows: Callback_Running() invoked.\n";
    if( SERVICE_RUNNING == Win32::Daemon::State() )
    {
      logprint "Windows: Callback_Running() invoked.\n";
      logprint "Windows: Starting Run().\n";
#      $pid = fork();
#      if ($pid == 0)
#      {
#	# Kindprozess
	$server->Run();
#      }
    }
  }

  sub Callback_Stop
  {
    my ($Event, $Context) = @_;
    # Tell the OS that the service is terminating...
    logprint "Windows: Callback_Stop() invoked.\n";
    $Context->{last_state} = SERVICE_STOPPED;
    Win32::Daemon::State( SERVICE_STOPPED );
    logprint "Windows: StopService().\n";
    Win32::Daemon::StopService();
#    logprint "Windows: Kill child process $pid...\n";
#    kill(9, $pid);
    myexit;
  }

  logprint "Windows: StartService().\n";
  Win32::Daemon::StartService(\%Context, 5000);
}
else
{
  # unter Unix...
  $server->Run();
}

myexit;

######################################################################
### Unterprogramme
######################################################################

sub TesteDateiBesitzer
{
  # Parameter: Voller Pfad einer Datei
  # Kein Returnwert. (Bei Gefahr sofort Abbruch.)
  #
  # Es wird überprüft, ob eine Konfigurationsdatei nur für den Menschen
  # schreibbar ist, der auch sysconf ausführt.
  # Sonst könnte irgendjemand die Konfigurationsfiles verändern und Root
  # lässt das dann aufs System los.
  #
  my $file = shift;
  # FIXME: Wie prüft man unter Windows die Datei-Berechtigungen?
  if ($osname ne 'MSWin32')
  {
    my $fmode = (stat($file))[2] & 07777;
    if ( ($fmode & ~0644) > 0 ) # mehr Rechte als "-rw-r--r--"
    {
      logdie "Security-hole: the file '$file' is writable for other users!\n";
    }
  }
  unless (-o $file)
  {
    logdie "Security-hole: you are not owner of '$file'\n";
  }
}


sub ReadConfigFile
{
  # Setzen von globalen Parametern aus dem Konfigurationsfile
  # Parameter: -
  # Return:    -
  #
  my $file = '.' .$slash. $rcfile;
  my $auth_hosts    = '';
  my $homedir = GetHomeDir();
  $file = $homedir .$slash. '.' . $rcfile unless -r $file;
  $file = "/etc/$rcfile" unless -r $file;
  if ($osname eq 'MSWin32')
  {
    $file = "c:/sysconf/client/$rcfile" unless -r $file;
  }
  logdie "Can not read '.${slash}$rcfile' or '".$homedir."${slash}.$rcfile' or /etc/$rcfile'!\n" unless -r $file;
  logprint "Using configurationfile '$file'\n";

  TesteDateiBesitzer($file);

  my $fh = FileHandle->new();
  open($fh, $file);
  while(<$fh>)
  {
    next if /^\#/; # Kommentare überspringen
    $port                 = $1 if /^PORT\s*=\s*(.+)/i;
    $auth_hosts           = $1 if /^AUTHORIZED_HOSTS\s*=\s*(.+)/i;
    $ssl_ca_file          = $1 if /^SSL_CA_FILE\s*=\s*(.+)/i;
    $ssl_cert_file        = $1 if /^SSL_CERT_FILE\s*=\s*(.+)/i;
    $ssl_key_file         = $1 if /^SSL_KEY_FILE\s*=\s*(.+)/i;
    $ssl_peer_common_name = $1 if /^SSL_PEER_COMMON_NAME\s*=\s*(.+)/i;
    next;
  }
  close $fh;
  logdie "No 'PORT' in '$file' defined!\n"             if $port          eq '';
  logdie "No 'AUTHORIZED_HOSTS' in '$file' defined!\n" if $auth_hosts    eq '';
  logdie "No 'SSL_CA_FILE' in '$file' defined!\n"      if $ssl_ca_file   eq '';
  logdie "No 'SSL_CERT_FILE' in '$file' defined!\n"    if $ssl_cert_file eq '';
  logdie "No 'SSL_KEY_FILE' in '$file' defined!\n"     if $ssl_key_file  eq '';
  logdie "No 'SSL_PEER_COMMON_NAME' in '$file' defined!\n" if $ssl_peer_common_name  eq '';
  @authorized_hosts = split(/\s/,$auth_hosts);
  logprint "AUTHORIZED_HOSTS: '",join("','",@authorized_hosts),"'\n";
}


sub ReadConfigFile_Logfile
{
  # Versucht einen eventuellen LOGFILE-Eintrag aus sysconfrc zu lesen.
  # Ohne Fehlerbehandlung, da diese Funktion sehr früh aufgerufen wird.
  #
  # Parameter: -
  # Return:    Filename des Logfiles
  #
  my $logfile = $TempDir.$slash."\L$appname\E.log"; # Standardeinstellung
  my $homedir = GetHomeDir();

  # Wenn nicht als root aufgerufen, dann Usernamen in den Logfilenamen
  if ($UID != 0)
  {
    my $login = (getpwuid($UID))[0] || $UID;
    $logfile = $TempDir.$slash."\L$appname\E.$login.log";
  }

  my $file = '.' .$slash. $rcfile;
  $file = $homedir . $slash . '.' . $rcfile unless -r $file;
  $file = "/etc/$rcfile" unless -r $file;
  if ($osname eq 'MSWin32')
  {
    $file = "c:/sysconf/client/$rcfile" unless -r $file;
  }

  if(-r $file)
  {
    my $fh = FileHandle->new();
    open($fh, $file);
    while(<$fh>)
    {
      next if /^\#/; # Kommentare überspringen
      $logfile = $1 if /^LOGFILE\s*=\s*(.+)/i;
    }
    close $fh;
  }

  return $logfile;
}

######################################################################
### Debug, Logging, Exit, ...
######################################################################


sub myexit
{
  # Diese Funktion macht einen normalen exit() mit dem übergebenen
  # Exitcode
  # und erledigt vorher noch Aufräum-Arbeiten, wie LOG-file schliessen, ...
  my $temp = '';
  my $error = defined $_[0] ? shift : $NormalExitCode;
  logprint("$appname PID $$ end at ",date,"\n");
  close(LOG);
  exit $error;
}


sub logprint
{
  # Schreibt einen Text ins LOG-file
  print LOG @_;
}


sub debug
{
  # Es werden Debug-Informationen erzeugt
  logprint("DEBUG: ",@_) if $debug;
}


sub warning
{
  # Parameter: Fehlermeldung
  # Return:    -
  #
  logprint("WARN: ",@_);
}


sub error
{
  # Parameter: Fehlermeldung
  # Return:    -
  #
  logprint("ERROR: ",@_);
}


sub logdie
{
  # Schreibt einen Text ins LOG-file und stirbt dann
  logprint("FATAL: ",@_);
  myexit($ErrorExitCode);
}


sub loginit
{
  # Schreibt einen kleinen Header ins LOG-file
  my $login = getlogin || (getpwuid($<))[0] || 'unknown';
  my $uid   = $<;
  my ($gid) = split(' ',$();
  logprint("\n---\n\n$appname $version PID $$ with Perl $]\nStart at ",date,
           " by ",$login,"(UID:$uid,GID:$gid)\n");
}


sub catch_signal
{
  my $signame = shift;
  logdie "End of $appname due to signal SIG$signame.\n";
}


sub catch_signal_usr1
{
  my $signame = shift;
  logprint "Caught SIG$signame. Restarting after QUIT.\n";
  RemoteServerSSL::SetRestartAfterQuit();
}


sub catch_warning
{
  # Abfangen von Laufzeit-Warnungen
  my $warnung = shift;
  error "INTERNAL: $warnung (Internal warnings are likely to be internal program-errors or wrong input, which were not handled correctly!)\n";
}


sub restart
{
  # Neustart des Prozesses
  logprint("Restart...\n");
  if ($osname ne 'MSWin32')
  {
    logprint("... using exec().\n");
    exec $0;
  }
  else
  {
    # Mittels SCM den Service durchstarten
    logprint("... using SCM.\n");
    exec('cmd.exe /C net stop sysconf-client & net start sysconf-client');
  }
}


######################################################################
### Kopf und Hilfe
######################################################################


sub Kopf
{
  my $head = "$appname $version   -   by Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Syntax: \L$appname\E optionen

Mit \L$appname\E wird ein Dämon-Prozess gestartet, der die Sysconf-Betankung
ohne rsh oder ssh zulässt. Die TCP-Verbindung ist SSL-gesichert.

Optionen:
-d: Debug-Informationen ins Logfile schreiben

Durch das Signal USR1 wird nach QUIT ein Neustart von \L$appname\E
ausgelöst.

Details zur Konfiguration von sysconf und $appname entnehmen Sie bitte
der Anleitung von sysconf.

Im Konfigurationsfile './$rcfile' oder '\$HOME/.$rcfile'
oder '/etc/$rcfile' müssen diese Werte definiert werden:

PORT:                 Auf diesem Port lauscht der $appname.
AUTHORIZED_HOSTS:     Von diesen Hosts ist eine Verbindung zulässig.
SSL_CA_FILE:          SSL CA Datei
SSL_CERT_FILE:        SSL Zertifikat
SSL_KEY_FILE:         SSL Schlüssel
SSL_PEER_COMMON_NAME: commonName des Sysconf Master Zertifikats

Optionale Einträge:
LOGFILE:          Voller Pfad des Logfiles (Standard: $TempDir${slash}\L$appname\E.log)

Beispiel:
PORT=3334
AUTHORIZED_HOSTS     =sysconf-master.test.de 10.33.22.11 loopback
SSL_CA_FILE          =/etc/sc-ssl-certs/sysconf-ca.crt
SSL_CERT_FILE        =/etc/sc-ssl-certs/client.pem
SSL_KEY_FILE         =/etc/sc-ssl-certs/client-key.pem
SSL_PEER_COMMON_NAME =Sysconf Master
LOGFILE              =/var/log/\L$appname\E.log

For Windows it is recommended, to start $appname via SCM, e.g.:
  ".'sc create sysconf-client binpath= "\"c:\strawberry\perl\bin\perl.exe\" \"c:\sysconf\sysconf-client-ssl\"" DisplayName= "sysconf-client" start= auto
'."  sc start sysconf-client
  sc query sysconf-client
  sc stop sysconf-client

";
  logprint "Es wird nur Hilfe ausgegeben.\n";
  myexit;
}
