#! /usr/bin/env perl # v2.2 20180421 RWR # Modified to be compatible with BSD Unix # # v2.3 20180423 RWR # allow underscores in module names $VERSION = '2.3'; $BUILDDATE = '20180423'; $LOGLEVEL = 0; # determines amount of stuff going into log use warnings; use Socket; # to resolve host names use Data::Dumper; # where to find the configuration file my @CONF_DIR = ( '/etc/rsbackup', '/usr/local/etc/rsbackup' ); my $CONF_NAME = 'rsbackup_server.conf'; my $CONF_DIR; my $PREPARE_COMMAND = 'prepare'; my $CLEANUP_COMMAND = 'cleanup'; # Following are global variables actually defined in the configuration file above # e-mail information my $ERROR_TO; my $DETAILS_TO; my $SENDMAIL; my $REPLY_TO; my $SUBJECT; my $FROM; # path and file name definitions my $LOCAL_BACKUP_PATH; my $MY_DIR; my $LOG_DIR = '/var/log'; my $LOG_NAME = 'backupservers.log'; my $MODULE_DIR = '/etc/rsbackup/modules'; my $CLOCK_SLOP; # Regular Expressions used my $REGEX_VALIDCOMMAND; my $REGEX_INVALIDCHARS; my $REGEX_PARSE_CLIENT_MACHINE; # Globally Used Variables my %user; # this is a hash of user/machine properties that is loaded by the following require my @status; # errors messages, if any are placed here, and cause the program to exit my $command; # this is the command that was passed in to us. We will verify it, then run it. my $client; # store the client name. This is used to determine the path on the server my $machine; # store the machine name. This is also used to determine the path on the server my $conf; my $temp; foreach $CONF_DIR ( @CONF_DIR ) { next unless -e "$CONF_DIR/$CONF_NAME"; open CONF, "<$CONF_DIR/$CONF_NAME" or die "Could not read configuration file $CONF_DIR/$CONF_NAME: $!"; $conf = join( '', ); $temp = $CONF_DIR; close CONF; } $CONF_DIR = $temp; unless ( eval ( $conf ) ) { die "Error in Configuration File:\n$@\n"; } ###################################################################################3 # Functions ###################################################################################3 sub dateTimeStamp { # my $format = shift; my $format = "%04d/%02d/%02d %02d:%02d:%02d"; # unless $format; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time); $year += 1900; $mon++; return sprintf( $format, $year, $mon, $mday, $hour, $min, $sec ); } sub sendError { my ($to, $content ) = @_; my $TO = "To: $to"; chomp $SUBJECT; # if hostname is read from cli, puts crlf at end open(SENDMAIL, "|$SENDMAIL") or die "Cannot open $SENDMAIL: $!"; # open(SENDMAIL, ">test.mail") or die "Cannot open $SENDMAIL: $!"; print SENDMAIL "$REPLY_TO\n"; print SENDMAIL "$SUBJECT\n"; print SENDMAIL "$TO\n"; print SENDMAIL "$FROM\n\n"; print SENDMAIL "Content-type: text/plain\n\n"; print SENDMAIL $content; close(SENDMAIL); } sub updateLog { open LOG, ">>$LOG_DIR/$LOG_NAME" or die "could not log to $LOG_NAME"; my $now = &dateTimeStamp(); foreach my $line (@_) { print LOG $now . " " . $line . "\n"; } close LOG; } # Validate the command coming in. This will check for a non-interactive command, and looks for invalid characters in it also # uses global variable @status sub validateSession { unless ( $ENV{'SSH_ORIGINAL_COMMAND'} ) { push @status, 'Interactive Session attempt'; return; } # check to make sure there is no funny stuff in command (ie, ampersands, semicolons, etc...) push @status, 'Invalid Characters in command' if $ENV{'SSH_ORIGINAL_COMMAND'} =~ $REGEX_INVALIDCHARS; # Version 0.5.0 of the client would pass getStats as a cleanup command. This simply translates that command # to the format we are expecting. It can be deleted once rsbackup v0.5.0 is replaced. if ( $ENV{'SSH_ORIGINAL_COMMAND'} =~ m!getStats ([^/]+)/([^/]+)$!i ) { $ENV{'SSH_ORIGINAL_COMMAND'} = "cleanup $1 $2"; } # check that command in proper format push @status, 'Command not in proper form' unless $ENV{'SSH_ORIGINAL_COMMAND'} =~ $REGEX_VALIDCOMMAND; } # get the client and machine names. Sets global variables # $client, $machine and $command sub parseClientMachine { $command = $ENV{'SSH_ORIGINAL_COMMAND'}; if ($command =~ $REGEX_PARSE_CLIENT_MACHINE ) { $client = $+{'client'}; $machine = $+{'machine'}; } } sub compareIP { my ( $desired, $actual ) = @_; if ($desired !~ m/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ ) { # non-numeric IP $desired = inet_ntoa(inet_aton($desired)); } return ($actual =~ m/$desired/); # see if the actual contains the desired IP } sub getMachineInformation { if ( $user{$client}{$machine} ) { return $user{$client}{$machine}; } push @status, "Could not find $client/$machine in configuration"; return ''; } sub doSecurityChecks { my $thisMachine = shift; my $realIP = $ENV{'SSH_CLIENT'}; $realIP =~ m/^([\d.]+)/; $realIP = $1; if ( $$thisMachine{'ip'} ) { push @status, "Client from invalid IP, wanting " . $$thisMachine{'ip'} . ', received ' . $realIP unless &compareIP( $$thisMachine{'ip'},$realIP ); } # if ( $$thisMachine{'start time'} ) { # $now = &dateTimeStamp('%04d/%02d/%02d %02d%02d'); # note, we are not getting the seconds, so the time in the second part of the space separated field # my ( $trash, $now ) = split( ' ', $now ); # push @status, "Backup allowed only at $$thisMachine{'start time'}, not allowed" unless $now - $CLOCK_SLOP < $$thisMachine{'start time'} and $now + $CLOCK_SLOP > $$thisMachine{'start time'}; # } } sub executeCommand { my $thisCommand = shift; &updateLog( "Running $thisCommand for $client/$machine" ) if $LOGLEVEL; $thisCommand =~ m/^([a-zA-Z0-9_]+)/; unless ( -x "$MODULE_DIR/$1" ) { # oops, not a good command # note, this will abort all subsquent commands push @status, "Module $1 does not exist"; return "Module $1 does not exist"; } # in this case, the three parameters below are the LAST ones passed to the script return qx!$MODULE_DIR/$thisCommand $LOCAL_BACKUP_PATH $client $machine $CONF_DIR/$CONF_NAME!; } &updateLog( "Going into validate" ) if $LOGLEVEL>1; &validateSession(); &updateLog( "parsing machine name" ) if $LOGLEVEL>1; &parseClientMachine() unless @status; my $thisMachine = &getMachineInformation() unless @status; &updateLog( "doing security checks " ) if $LOGLEVEL>1; &doSecurityChecks( $thisMachine ) unless @status; &updateLog( Dumper( $thisMachine ) ) if $LOGLEVEL>2; &updateLog( "---------------------------", "Connection from $client $machine" ) if $client && $machine; if ( @status ) { # we had some sort of failure foreach my $key ( keys %ENV ) { # dump the environment for debugging push @status, $key . '=' . $ENV{$key}; } &updateLog( @status ); # enter it into the log push @status, "backupServer $VERSION $BUILDDATE\n"; &sendError( $DETAILS_TO, join( "\n", @status) ); print "Rejected"; # and send the rejected error message back to client } else { # everything looks good if ( $command =~ m/^(prepare)|(cleanup)/ip ) { # the p means save the match in ${^MATCH} &updateLog( "Running $command for $client/$machine" ); &updateLog( Dumper( $thisMachine ) ) if $LOGLEVEL>2; my $commands = $$thisMachine{ ${^MATCH} }; &updateLog( Dumper( $commands ) ) if $LOGLEVEL>2; my $output = ''; if ( $commands ) { foreach my $command ( @$commands ) { $output .= &executeCommand( $command ); } print $output; } } elsif ( $command =~ m/^rsync/i ) { &updateLog( "Running rsync command" ); &updateLog( " $command" ); exec( $command ); } else { &updateLog( "Unknown Command [$command]" ); } } 1;