package Guardian::Events;
use strict;
use warnings;

use Exporter qw(import);

our @EXPORT_OK = qw(Init CheckAction GenerateIgnoreList Update);

# Hash which stores all supported commands from the queue.
my %commands = (
	'count' => \&Counter,
	'block' => \&CallBlock,
	'unblock' => \&CallUnblock,
	'flush' => \&CallFlush,
	'reload' => \&main::Reload,
	'reload-ignore-list' => \&main::ReloadIgnoreList,
	'logrotate' => \&main::Logrotate,
);

# Hash to store addresses and their current count.
my %counthash = ();

# Hash to store all currentyl blocked addresses and a timestamp
# when the block for this address can be released.
my %blockhash = ();

# Hash to store user-defined IP addresses and/or subnets which should be
# ignored in case any events should be repored for them. 
my %ignorehash = ();

# Array to store localhost related IP addresses.
# They are always white-listed to prevent guardian from blocking
# any local traffic.
my @localhost_addresses = ("127.0.0.1", "::1");

# This object will contain the reference to the logger object after calling Init.
my $logger;

#
## The "Init" (Block) function.
#
## This function is responsible to initialize Block as a class based object.
## It has to be called once before any blocking event can be processed.
#
## The following arguments must be passed, during initialization:
## "BlockCount" and "BlockTime" which both have to be natural numbers.
#
sub Init (%) {
	my ( $class, %args ) = @_;
	my $self = \%args;

	# Fail, if some critical arguments are missing.
	unless ((exists($self->{BlockCount})) && (exists($self->{BlockTime}))) {
		die "Could not initialize Block: Too less arguments are given.\n";
	}

	# Use bless to make "$self" to an object of class "$class".
	bless($self, $class);

	# Assign logger object.
	$logger = $self->{Logger};

	# Log used firewall engine.
	$logger->Log("debug", "Using firewall engine: $self->{FirewallEngine}");

	# Try to load the specified firewall engine or die.
	my $module_name = "Guardian::" . $self->{FirewallEngine};
	eval "use $module_name; 1" or die "Could not load a module for firewall engine: $self->{FirewallEngine}!";

	# Check if an IgnoreFile has been configured.
	if (exists($self->{IgnoreFile})) {
		# Call function to handle the ignore mechanism.
	 	&GenerateIgnoreList($self->{IgnoreFile});
	} else {
		# Whitelist local addresses.
		%ignorehash = &_whitelist_localhost();
	}

	# Return the class object.
	return $self;
}

#
## The "Update" Block settings function.
#
## This object based function is called to update various class settings.
#
sub Update (\%) {
        my $self = shift;

	# Dereference the given hash-ref and store
	# the values into a new temporary hash.
	my %settings = %{ $_[0] };

	# Skip settings update if some essential settings are missing.
	unless ((exists($settings{BlockCount})) && (exists($settings{BlockTime}))) {
		$logger->Log("err", "Values for BlockCount or BlockTime are missing, keeping previously configured settings.");

		# Return unmodified class object.
		return $self;
	}

	# Change settings.
        $self->{BlockCount} = $settings{BlockCount};
	$self->{BlockTime} = $settings{BlockTime};

	# Return modified class object.
        return $self;
}

#
## The main "CheckAction" function.
#
## This function is used to handle each recived event from the main event queue of guardian.
#
## It will check if the given command is valid and will pass it to the responsible function.
#
sub CheckAction ($$) {
	my $self = shift;
	my @event = split(/ /, $_[0], 4);
	my ($command, $address, $module, $message) = @event;

	# Check if we got an invalid command.
	unless(exists($commands{$command})) {
                $logger->Log("err", "The CheckAction function has been called with an unsupported command ($command)!");
                return;
        }

	# Check if the given event contains an address.
	if ($address) {
		# Convert and validate the given address.
		my $bin_address = &Guardian::Base::IPOrNet2Int($address);

		# Abort if the given address could not be converted because it is not valid.
		unless ($bin_address) {
			$logger->Log("err", "Invalid IP address: $address");
			return;
		}

		# Check if the given command is not "unblock" and the address should be ignored.
		if(($command ne "unblock") && (&_IsOnIgnoreList($bin_address))) {
			# Log message.
			$logger->Log("info", "Ignoring event for $address, because it is part of the ignore list.");
			return;
		}
	}

	# Call required handler.
	my $error = $commands{$command}->($self, $address, $module, $message);

	# If we got any return code, something went wrong and should be logged.
	if ($error) {
		$logger->Log("err", "Got error: $error");
		return;
	}
}

#
## The main "Counter" function.
#
## This function is used to handle each count message + address, which has been sent by the main event
## loop of guardian.
#
## It stores the address and the current count into the counthash and increase the count each time when
## the same address should be counted again. When the current count reaches the configured BlockCount,
## the responsible function will be called to block the address.
#
sub Counter ($@) {
	my $self = shift;
	my ($address, $module, $message) = @_;

	# Log event.
	$logger->Log("debug", "$module reported $message for address: $address");

	# Increase existing count or start counting for new source addresses.
	if (exists($counthash{$address})) {
		# Skip already blocked addresses.
		if (exists($blockhash{$address})) {
			return undef;
		}

		# Increase count of the existing entry.
		$counthash{$address} = $counthash{$address} + 1;

		# Log altered count of the address.
		$logger->Log("debug", "Source $address now has count $counthash{$address}/$self->{BlockCount}...");
	} else {
		# Log that counting for the address has been started.
		$logger->Log("debug", "Start counting for $address...");

		# Set count to "1".
		$counthash{$address} = 1;
	}

	# Check if the address has reached the configured count limit.
	if ($counthash{$address} >= $self->{BlockCount}) {
		# Write out log message.
		$logger->Log("info", "Blocking $address for $self->{BlockTime} seconds...");

		# Call block subroutine to block the address.
		my $error = &CallBlock($self, $address, $module, $message);

		# Return the message if an error occurs.
		return $error;
	}

	# Everything worked well, return nothing.
	return undef;
}

#
## The RemoveBlocks function.
#
## This function periodly will be called and is responsible for releasing the block if the Blocktime
## on an address has expired.
#
## To do this, the code will loop through all entries of the blockhash and check
## if the estimiated BlockTime of each address has reached and so the block can be released.
#
sub RemoveBlocks () {
	my $self = shift;

	# Get the current time.
	my $time = time();

	# Loop through the blockhash.
	foreach my $address (keys %blockhash) {
		# Check if the blocktime for the address has expired.
		if ($blockhash{$address} <= $time) {
			# Call unblock subroutine.
			my $error = &CallUnblock($self, $address, "BlockTime", "has expired for $address");

			# Log error messages if returned.
			if ($error) {
				$logger->Log("err", "$error");
			}
		}
	}

	# Everything okay, return nothing.
	return undef;
}

#
## The CallBlock function.
#
## This function is called, if the BlockCount for an address is reached or a direct
## request for blocking an address has been recieved.
#
sub CallBlock ($@) {
	my $self = shift;
	my ($address, $module, $message) = @_;

	# Log the call for blocking an address.
	$logger->Log("info", "$module - $message");

	# Check if an entry for this address exists
	# in the blockhash. If not, the address has
	# not been blocked yet, call the responisible
	# function to do this now.
	unless (exists($blockhash{$address})) {
		# Obtain the configured FirewallAction.
		my $action = $self->{FirewallAction};

		# Block the given address.
		my $error = &DoBlock($address, $action);

		# If we got back an error message something went wrong.
		if ($error) {
			# Exit function and return the used FirewallEngine and the error message.
			return "$self->{FirewallEngine} - $error";
		} else {
			# Address has been successfully blocked, print a log message.
			$logger->Log("debug", "Address $address successfully has been blocked...");
		}
	}

	# Generate time when the block will expire.
	my $expire = time() + $self->{BlockTime};

	# Store the blocked address and the expire time
	# in the blockhash.
	$blockhash{$address} = $expire;

	# Return nothing "undef" if everything is okay.
	return undef;
}

#
## CallUnblock function.
#
## This function is responsible for unblocking and deleting a given
## address from the blockhash.
#
sub CallUnblock ($) {
	my $self = shift;
	my ($address, $module, $message) = @_;

	# Log the call for unblocking an address.
	$logger->Log("info", "$module - $message");

	# Return an error if no entry for the given address
	# is present in the blockhash.
	unless (exists($blockhash{$address})) {
		return "Address $address was not blocked!";
	}

	# Unblock the address.
	my $error = &DoUnblock($address);

	# If an error message is returned, something went wrong.
	if ($error) {
		# Exit function and return the error message.
		return $error;
	} else {
		# Address successfully has been unblocked.
		$logger->Log("debug", "Address $address successfully has been unblocked...");
	}

	# Drop address from blockhash.
	delete ($blockhash{$address});

	# Drop address from counthash if the address has been unblocked
	# by the user. This happens when the called module is "Socket".
	if ($module eq "Socket") {
		delete ($counthash{$address});
	}

	# Everything worked well, return nothing.
	return undef;
}

#
## CallFlush function.
#
## This function is responsible for calling the used firewall
## engine to do a flush of the used firewall chain. This will
## clean the entire firewall chain.
#
sub CallFlush ($) {
	my $self = shift;

	# Log the call for flushing.
	$logger->Log("debug", "Flush has been called...");

	# Call flush.
	my $error = &DoFlush();

	# If an error message is returned, something went wrong.
	if ($error) {
		# Exit function and return the error message.
		return $error;
	} else {
		# Flush successfully has been performed.
		$logger->Log("debug", "Flush successfully has been performed...");
	}

	# Flush blockhash.
	%blockhash = ();

	# Everything worked well, return nothing.
	return undef;
}

#
## GenerateIgnoreList function.
#
## This function is responsible for generating/updating the
## IgnoreHash which contains all ignored IP addresses and
## networks.
#
sub GenerateIgnoreList($) {
	my $file = shift;
	my @include_files;

	# Reset current ignore hash and add
	# localhost related IP addresses.
	%ignorehash = &_whitelist_localhost();

	# Check if the given IgnoreFile could be opened.
	unless(-e $file) {
		$logger->Log("err", "The configured IgnoreFile \($file\) could not be opened. Skipped!");
		return;
	}

	# Open the given IgnoreFile.
	open (IGNORE, $file);

	# Read-in the file line by line.
	while (<IGNORE>) {
		# Skip comments.
		next if (/\#/);

		# Skip blank lines.
		next if (/^\s*$/);

		# Remove any newlines.
		chomp;

		# Check for an include instruction.
		if ($_ =~ /^Include_File = (.*)/) {
			my $include_file = $1;

			# Check if the parsed include file exists and is read-able.
			if (-e $include_file) {
				# Add file to the array of files wich will be included.
				push(@include_files, $include_file);

				# Write out log message.
				$logger->Log("debug", "Addresses from $include_file will be included...");
			} else {
				# Log missing file.
				$logger->Log("err", "$include_file will not be included. File does not exist!");
			}
		} else {
			# Check if the line contains a valid single address or network and
			# convert it into binary format. Store the result/start and
			# end values in a temporary array.
			my @values = &Guardian::Base::IPOrNet2Int($_);

			# If the function returned any values, the line contained a valid
			# single address or network which successfully has been converted into
			# binary format.
			if (@values) {
				# Assign the array as value to the ignorehash.
				$ignorehash{$_} = [@values];
			} else {
				# Log invalid entry.
				$logger->Log("err", "IgnoreFile contains an invalid address/network: $_");

				# Skip line.
				next;
			}
		}
	}

	# Close filehandle for the IgnoreFile.
	close (IGNORE);

	# Check if any files should be included.
	if (@include_files) {
		# Loop through the array of files which should be included.
		foreach my $file (@include_files) {
			# Open the file.
			open(INCLUDE, $file);

			# Read-in file line by line.
			while(<INCLUDE>) {
				# Skip any comments.
				next if (/\#/);

				# Skip any blank lines.
				next if (/^\s*$/);

				# Chomp any newlines.
				chomp;

				# Check if the line contains a valid single address or network and
				# convert it into binary format. Store the result/start and
				# end values in a temporary array.
				my @values = &Guardian::Base::IPOrNet2Int($_);

				# If the function returned any values, the line contained a valid
				# single address or network which successfully has been converted into
				# binary format.
				if (@values) {
					# Assign the array as value to the ignorehash.
					$ignorehash{$_} = [@values];
				} else {
					# Log invalid entry.
					$logger->Log("err", "$file contains an invalid address/network: $_");

					# Skip line.
					next;
				}
			}

			# Close filehandle.
			close(INCLUDE);
		}
	}

	# Get amount of current elements in hash.
	my $amount = scalar(keys(%ignorehash));

	# Write out log message.
	$logger->Log("debug", "Ignore list currently contains $amount entries:");

	# Sort the ignore hash.
	my @sorted_addresses = &Guardian::Base::SortAddressHash(\%ignorehash);

	# Loop through the entire array.
	foreach my $address (@sorted_addresses) {
		# Log the ignored address.
		$logger->Log("debug", "\- $address");
	}

	# Finished return nothing.
	return;
}

#
## Private function to check if an address is part of the Ignore Hash.
#
## This function checks if a passed IP address in binary format (!),
## directly or as part of an ignored network is stored in the ignore hash.
#
sub _IsOnIgnoreList ($) {
	my $bin_address = shift;

	# Loop through the ignore hash and grab the stored values.
	foreach my $key ( keys %ignorehash ) {
		# Dereference values array.
		my @values = @{$ignorehash{$key}};

		# Obtain amount of items for the current value array.
		my $items = scalar @values;

		# Whether the amount equals one, the given binary address just
		# needs to be compared against a single address.
		if ($items eq "1") {
			my ($ignored_address) = @values;

			# Simple check if the stored and the given binary address
			# are the same.
			if ($bin_address eq $ignored_address) {
				# The given address is part of the ignore list.
				$logger->Log("debug", "Address $key found on the ignore list.");

				# Return "1" (True).
				return 1;
			}
		}

		# If the amount equals two, for passed binary address needs to
		# be checked if it is part of the ignored network range.
		elsif ($items eq "2") {
			my ($first_address, $last_address) = @values;

			# Check if the passed binary address is bigger than
			# the first address and smaler than the last address
			# (between) the stored network range.
			if (($bin_address >= $first_address) && ($bin_address <= $last_address)) {
				# The address is part of an ignored network.
				$logger->Log("debug", "Address is found inside the ignored network $key.");

				# Return "1" (True).
				return 1;
			}

		# If the amount is not eighter one or two, the current entry of the ignorehash seems
		# to be corrupted. Skip and log it.
		} else {
			# Write log message about this corruped item in the ignore hash.
			$logger->Log("err", "Invalid item in the Ignore Hash: $key - @values");

			# Skip this element of the ignore hash.
			next;
		}
	}

	# If we got here, the given address is not part of the ignore hash.
	# Return nothing (False).
	return;
}

#
## The _whitelist_localhost function.
#
## This tiny private function simple generates and returns a hash which contains
## the clear and binary converted addresses for all array-stored
## (@localhost_addresses) in an ignorelist compatible format.
#
sub _whitelist_localhost () {
	my %temphash;

	# Loop through the array of localhost related addresses.
	foreach my $address (@localhost_addresses) {
		# Validate and convert the addresss into binary format.
		my @values = &Guardian::Base::IPOrNet2Int($address);

		# Check if any values are returned.
		if (@values) {
			# Store the converted binary values in the temporary hash.
			$temphash{$address} = [@values];
		}
	}

	# Return the temporary hash.
	return %temphash;
}

1;
