#!/usr/local/bin/perl

use DBI;
use strict;
use warnings;
use Glib;
use sigtrap qw/handler signal_handler normal-signals/;
use My::Savepid qw(savepid loadpid);
use Tie::Syslog; 
use LWP::UserAgent;

my $pid		= $$;
my $program	= $0;
my $daemonize	= 1;
my $verbose	= 1;
my $logfile	= '/var/log/flush_reject.log';
my $pidfile	= '/var/run/flush_reject.pid';
my $time_keep	= '10 MINUTE';				# for how long do we want to block blacklisted host, sql time (SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR)
my $default_ban	= '6 HOUR';				# how long do we ban offenders? sql time
my $max_block	= 5;					#max amount of blocks before we stop freeing them (aka permanent ban)
my $max_time	= '31 DAY';				#for how long do permanent bans last? sql time
my $sql_user	= '';
my $sql_pwd	= '';
my $sql_host	= '';
my $table	= '';
my $block_table	= '';
my $opnsense	= '';
my $api_key	= '';
my $api_secret	= '';
my $api_alias	= '';

my $logf;
my $dp;
my $dbh;
my $loop;
my $timer;
my $x;
if($daemonize) {
	if(my $tp = fork) {
		exit 0;
	} else {
		$pid = $$;
		&init;
	}
} else {
	&init;
}

sub init {
	&savepid($pid,$pidfile);
	open($logf, '>>', "$logfile") || die "Could not write to logfile";
	select $logf; $| = 1;
	if($daemonize) {
		$x = tie *STDERR, 'Tie::Syslog','local0.err',$program,'pid','unix';
		$x->ExtendedSTDERR();
		close STDIN;
	}
	&log(1,"$0($pid) started");
	$loop	= Glib::MainLoop->new;
	$timer	= Glib::Timeout -> add_seconds(300, \&update);
	$dbh	= &connect;
	&update;
	$loop -> run;
}

sub update {
	$dbh		= &connect unless($dbh->ping);
	&update_old;
	my @work	= &find_work;
	my $selrows	= scalar(@work);
	&log(2,"Found $selrows fresh entries to work on") if($selrows);
	if($selrows) {
		foreach my $host(@work) {
			my @tolog;
			my $rows	= &find_fresh($host);
			push(@tolog,"$rows nonsleeping < 6hours");
			if($rows > 1) {
				my $setsleeprows = &set_sleeping($host);
				push(@tolog,"$setsleeprows set as sleeping") if($setsleeprows);
			}
			my $trows = &find_sleeping($host);
			push(@tolog,"$trows sleeping entries") if($trows);
			my $age	= &find_newest_entry($host);
			unless($trows) {
				if($rows <= 1) {
					if(&find_total($host) < $max_block) {
						push(@tolog, "deleting pf rule.");
						&clean_host($host,$age);
					} else {
						push(@tolog, "doing nothing, $host has too many blocks");
					}
				}
			} else {
				push(@tolog, "$trows sleeping, noop");
			}
			if($age) {
				my $todel	= 21600 - $age;
				my $page	= 'last entry is '.&gettime($age).' old';
				my $ptodel	= '';
				$ptodel		= ', will be deleted in '.&gettime($todel) if($trows);	
				push(@tolog,"$page$ptodel") if($age);
			}
			my $logline = "$host: ";
			$logline .= join(',', @tolog);
			&log(2,"$logline");
		}
	}
	system("/sbin/pfctl -t $block_table -T show > /etc/pf.$block_table");
	$dbh->disconnect;
	return 1;
}

sub opnsense_api_delete {
	my $ip		= shift;
	my $ua		= LWP::UserAgent->new;
	$ua->agent('flush_reject');
	$ua->timeout(1);
	my $req		= HTTP::Request->new(POST => 'http://'.$opnsense.'/api/firewall/alias_util/delete/'.$api_alias);
	$req->authorization_basic($api_key, $api_secret);
	$req->content_type('application/json');
	$req->content('{"address":"'.$ip.'"}');
	my $res		= $ua->request($req);
	unless ($res->is_success) {
		&log(2, 'Failed to request to delete '.$ip.' from opnsense.');
	}
}

sub find_total {
	my $host	= shift;
	my $sth = $dbh->prepare("
		SELECT
			count(ip)
		FROM
			$table
		WHERE
			ip = ? AND
			time > DATE_SUB(NOW(), INTERVAL $max_time)
		");
	$sth->execute($host);
	my $rows	= $sth->fetchrow_arrayref->[0] || 0;
	return $rows;
}

sub update_old {
	## Set entries older than 6 hours as not sleeping if they are set as so, this means they will be deleted unless they're permanently banned
	my $sth	= $dbh->prepare("
		UPDATE	$table
		SET	sleep = 0
		WHERE	deleted = 0 AND
			SLEEP = 1 AND
			time < DATE_SUB(NOW(), INTERVAL $default_ban)")
		|| die "Failed to update old records".DBI::errstr;
	$sth->execute();
	my $rows	= $sth->rows || 0;
	return $rows;
}

sub find_work {
	my @work;
	my %ret;
	my $sth		= $dbh->prepare("
		SELECT ip
		FROM $table
		WHERE
			deleted = 0 AND
			time < DATE_SUB(NOW(), INTERVAL $time_keep)")
		|| die "Failed to find records to work on".DBI::errstr;
	$sth->execute;
	my $rows	= $sth->rows;
	while(my $ref = $sth->fetchrow_hashref) {
		my $workip	= $$ref{'ip'};
		$ret{$workip}	= 1;
	}
	foreach my $key(keys %ret) {
		push(@work,$key);
	}
	return @work;
}

sub set_sleeping {
	my $host	= shift;
	my $sth		= $dbh->prepare("
		UPDATE $table
		SET sleep = 1
		WHERE
			ip = ? AND
			deleted = 0
			AND sleep = 0")
		|| die "Failed to update record for $host with sleep=1 ".DBI::errstr;
	$sth->execute($host);
	my $rows	= $sth->rows || 0;
	return $rows;
}

sub find_fresh {
	my $host	= shift;
	my $sth		= $dbh->prepare("
		SELECT count(*)
		FROM $table
		WHERE
			ip = ? AND
			sleep = 0 AND
			time > DATE_SUB(NOW(), INTERVAL $default_ban)")
		|| die "Failed to find number of records for $host newer than 6 hours ".DBI::errstr;
	$sth->execute($host);
	my $rows	= $sth->fetchrow_arrayref->[0];
	return $rows;
}

sub find_sleeping {
	my $host	= shift;
	my $sth	= $dbh->prepare("
		SELECT count(*)
		FROM $table
		WHERE
			ip = ? AND
			sleep = 1")
		|| die "Failed to find number of records for sleeping host $host ".DBI::errstr;
	$sth->execute($host);
	my $trows	= $sth->fetchrow_arrayref->[0] || 0;
	return $trows;
}

sub find_newest_entry {
	my $host	= shift;
	my $sth		= $dbh->prepare("
		SELECT TIMESTAMPDIFF(SECOND, time, now()) AS age
		FROM $table
		WHERE ip = ?
		ORDER BY time DESC
		LIMIT 1")
		|| die "Failed to find age of last entry of $host ".DBI::errstr;
	$sth->execute($host);
	my $age		= $sth->fetchrow_arrayref->[0] || 0;
	return $age;
}

sub clean_host {
	my $ip	= shift;
	my $age	= shift || 0;
	my $sth = $dbh->prepare("
		UPDATE $table
		SET deleted = 1
		WHERE
			ip = ? AND
			deleted = 0")
		|| die "Failed to update record for $ip with deleted=1".DBI::errstr;
	$sth->execute($ip) || die "Failed to update record for $ip:".DBI::errstr;
	my $delrows = $sth->rows || 0;
	system "/sbin/pfctl -q -t $block_table -T delete $ip" if($delrows);
	opnsense_api_delete($ip);
	my $prettyage	= &gettime($age);
	&log(2,"Updated $delrows rows for host $ip to mark them as deleted");
	&log(1,"Cleared $ip after $prettyage.");
}

sub log {
	my $sev = shift;
	my $msg = shift;
	unless($msg) {
		$msg	= $sev;
		$sev	= 0;
	}
	print STDERR "$msg" unless($sev);
	if($verbose >= $sev) {
		my $now = localtime();
		print $logf "$now: $msg\n";
	}
	return 1;
}

sub reload_log {
	close($logf);
	open($logf, '>>', "$logfile") || die "Could not write to logfile";
	select $logf; $| = 1;
}

sub signal_handler {
	my $signal = shift;
	&log(1,"Recieved $signal: ");
	if($signal eq 'HUP') {
		&log(1,'Reloaded log');
		&reload_log;
	} else {
		system("/sbin/pfctl -t $block_table -T show > /etc/pf.$block_table");
		&log(1,"$program($pid) Shutting down");
		$dbh->disconnect;
		undef $x;
		untie *STDERR;
		close($logf);
		exit 1;
	}
}

sub connect {
	my $databasec;
	while(1) {
		last if($databasec = DBI->connect("DBI:mysql:database=syslog;host=$sql_host",$sql_user,$sql_pwd, { PrintError => 1, mysql_auto_reconnect=>1, AutoCommit => 1 }));
		&log(2,'Connection problems, reconnecting in 10 seconds');
		sleep(10);
	}
	return $databasec;
}

sub gettime # gettime(seconds)
{
	my $time = $_[0];
	if($time > 0) {
		my $days	= int($time/24/60/60);
		my $hours	= sprintf("%02d", (int($time/60/60)%24));
		my $minutes	= sprintf("%02d", (int($time/60)%60));
		my $seconds	= sprintf("%02d", (int($time)%60));
		$time		= "$minutes:$seconds"			if($minutes > 0);
		$time		= "$hours hours $minutes:$seconds"	if($hours > 0);
		$time		= "$days days $hours:$minutes:$seconds"	if($days > 0);
		return $time;
	}
}
