# ho_hammer.pl
#
# $Id: ho_hammer.pl,v 1.5 2004/09/11 12:21:49 jvunder REL_0_3 $
#
# Part of the Hybrid Oper Script Collection.
#
# Looks for hammering clients and acts upon them.
#
# TODO: code HOSC::Kliner and use it.

use strict;
use vars qw($VERSION %IRSSI $SCRIPT_NAME);

use Irssi;
use Irssi::Irc;
use HOSC::again;
use HOSC::again 'HOSC::Base';
use HOSC::again 'HOSC::Tools';
use HOSC::again 'HOSC::Kliner';
import HOSC::Tools qw{is_server_notice};

# ---------------------------------------------------------------------

($VERSION) = '$Revision: 1.5 $' =~ / (\d+\.\d+) /;
%IRSSI = (
    authors	=> 'Garion',
    contact	=> 'garion@efnet.nl',
    name	=> 'ho_hammer',
    description	=> 'Looks for hammering clients and acts upon them.',
    license	=> 'Public Domain',
    url		=> 'http://www.garion.org/irssi/hosc.php',
    changed	=> '19 January 2003 13:07:07',
);
$SCRIPT_NAME = 'Hammer';

# Hashtable with connection times per host
# Key is the host
# Value is an array of connection times (unix timestamp)
my %conntimes;

# The last time the connection hash has been cleaned (unix timestamp)
my $conntimes_last_cleaned = 0;

my $kliner = HOSC::Kliner->new();

# ---------------------------------------------------------------------
# A Server Event has occurred. Check if it is a server NOTICE; 
# if so, process it.

sub event_serverevent {
    my ($server, $msg, $nick, $hostmask) = @_;

    return unless is_server_notice(@_);
  
    process_event($server, $msg);
}


# ---------------------------------------------------------------------
# This function takes a server notice and matches it with a few regular
# expressions to see if any special action needs to be taken.

sub process_event {
    my ($server, $msg) = @_;

    # HYBRID 7 - need a setting to determine which server type we have!
    # Client connect: nick, user, host, ip, class, realname
    if ($msg =~ /Client connecting: (.*) \((.*)@(.*)\) \[(.*)\] {(.*)} \[(.*)\]/) {
        process_connect($server, $1, $2, $3, $4, $5, $6);
        return;
    }

    # HYBRID 6
    # Client connect: nick, user, host, ip, class, realname
    if ($msg =~ /Client connecting: (.*) \((.*)@(.*)\) \[(.*)\] {(.*)}/) {
        process_connect($server, $1, $2, $3, $4, $5, undef);
        return;
    }
}

# ---------------------------------------------------------------------
# This function processes a client connect.
# It shows warning in case of:
# - many connects in a short timespan from a single host

sub process_connect {
    my ($server, $nick, $user, $host, $ip, $class, $realname) = @_;

    return unless Irssi::settings_get_bool('ho_hammer_enable');

    return if $ip eq '255.255.255.255' &&
        Irssi::settings_get_bool('ho_hammer_ignore_spoofs');

    my $tag = $server->{tag};

	# Check whether the server notice is on one of the networks we are
	# monitoring.
	my $watch_this_network = 0;
	foreach my $network (split /\s+/, 
		lc(Irssi::settings_get_str('ho_hammer_network_tags'))
	) {
		if ($network eq lc($server->{tag})) {
			$watch_this_network = 1;
			last;
		}
	}
	return unless $watch_this_network;

    my $now = time();
    push @{ $conntimes{$tag}->{$host} }, $now;

    # Check whether this host has connected more than
    # ho_hammer_warning_count times in the past
    # ho_hammer_warning_time seconds.
    if (@{ $conntimes{$tag}->{$host} } == 
        Irssi::settings_get_int('ho_hammer_warning_count')
    ) {
        # Get the time of the first connect
        my $firsttime = ${ $conntimes{$tag}->{$host} }[0];

        # Get the time of the last connect
        my $lasttime = ${ $conntimes{$tag}->{$host} }[@{ $conntimes{$tag}->{$host} } - 1];

        my $timediff = $lasttime - $firsttime;

        if ($timediff < Irssi::settings_get_int('ho_hammer_warning_time')) {
            ho_print_warning("Hammer: " . @{ $conntimes{$tag}->{$host} } . "/".
                "$timediff: $nick ($user\@$host).");
        }
    }

    # Check whether this host has connected more than
    # ho_hammer_violation_count times in the past
    # ho_hammer_violation_time seconds.
    if (@{ $conntimes{$tag}->{$host} } >= 
        Irssi::settings_get_int('ho_hammer_violation_count')) {
        # Get the time of the first connect
        my $firsttime = ${ $conntimes{$tag}->{$host} }[0];

        # Get the time of the last connect
        my $lasttime = ${ $conntimes{$tag}->{$host} }[@{ $conntimes{$tag}->{$host} } - 1];

        my $timediff = $lasttime - $firsttime;

        if ($timediff < Irssi::settings_get_int('ho_hammer_violation_time')) {
            my $time   = Irssi::settings_get_int('ho_hammer_kline_time');
            my $reason = Irssi::settings_get_str('ho_hammer_kline_reason');

            # If number of connections is equal to max number of connections
            # allowed, kline user@host. If it is higher, that means the user
            # has been k-lined once and has changed ident; therefore, kline
            # *@host.
            if (@{ $conntimes{$tag}->{$host} } ==
                Irssi::settings_get_int('ho_hammer_violation_count')
            ) {
				$kliner->kline(
					server => $server, 
					user   => $user,
					host   => $host,
					reason => $reason,
				);
                ho_print("K-lined $user\@$host for hammering.");
            } else {
				$kliner->kline(
					server => $server, 
					user   => '*',
					host   => $host,
					reason => $reason,
				);
                ho_print("K-lined *\@$host for hammering.");
            }
        }
    }

    # Clean up the connection times hash to make sure it doesn't grow
    # to infinity :)
    # Do this every 60 seconds.
    if ($now > $conntimes_last_cleaned + 60) {
        $conntimes_last_cleaned = $now;
        cleanup_conntimes_hash(300);
    }
}



# ---------------------------------------------------------------------
# Cleans up the connection times hash.
# The only argument is the number of seconds to keep the hostnames for.
# This means that if the last connection from a hostname was longer ago
# than that number of seconds, the hostname is dropped from the hash.

sub cleanup_conntimes_hash {
    my ($keeptime) = @_;
    my $now = time();
  
    # If the last time this host has connected is over $keeptime secs ago,
    # delete it.
    for my $tag (keys %conntimes) {
        for my $host (keys %{ $conntimes{$tag} }) {
            my $lasttime = ${ $conntimes{$tag}->{$host} }[@{ $conntimes{$tag}->{$host} } - 1];

            # Discard this host if no connections have been made from it during
            # the last $keeptime seconds.
            if ($now > $lasttime + $keeptime) {
                delete $conntimes{$tag}->{$host};
            }
        }
    }
}

# ---------------------------------------------------------------------
# The /hammer command.

sub cmd_hammer {
    my ($data, $server, $item) = @_;
    if ($data =~ m/^[(help)]/i ) {
        Irssi::command_runsub ('hammer', $data, $server, $item);
    } else {
        ho_print("Use /HAMMER HELP for help.")
    }
}

# ---------------------------------------------------------------------
# The /hammer help command.

sub cmd_hammer_help {
    print_help();
}

# ---------------------------------------------------------------------

ho_print_init_begin();

Irssi::signal_add_first('server event', 'event_serverevent');

Irssi::command_bind('hammer',      'cmd_hammer');
Irssi::command_bind('hammer help', 'cmd_hammer_help');

Irssi::settings_add_bool('ho', 'ho_hammer_enable',            0);
Irssi::settings_add_bool('ho', 'ho_hammer_ignore_spoofs',     1);
Irssi::settings_add_int('ho', 'ho_hammer_warning_count',      8);
Irssi::settings_add_int('ho', 'ho_hammer_warning_time',     100);
Irssi::settings_add_int('ho', 'ho_hammer_violation_count',   10);
Irssi::settings_add_int('ho', 'ho_hammer_violation_time',   120);
Irssi::settings_add_int('ho', 'ho_hammer_kline_time',      1440);
Irssi::settings_add_str('ho', 'ho_hammer_network_tags',      '');
Irssi::settings_add_str('ho', 'ho_hammer_kline_reason', 
    '[Automated K-line] Reconnecting too fast. Please try again later.');

if (length Irssi::settings_get_str('ho_hammer_network_tags') > 0) {
	if (Irssi::settings_get_bool('ho_hammer_enable')) {
		ho_print("Script enabled for the following tags: " .
			Irssi::settings_get_str('ho_hammer_network_tags'));
	} else {
		ho_print("Script disabled. The following tags have been set: " .
			Irssi::settings_get_str('ho_hammer_network_tags') . 
			". Use /SET ho_hammer_enable ON to enable the script.");
	}
} else {
	ho_print_warning("No network tags set. Please use ".
		"/SET ho_hammer_network_tags tag1 tag2 tag3 .. ".
		"to choose the tags the script will work on.");
}

ho_print_init_end();
ho_print("Use /HAMMER HELP for help.");

# ---------------------------------------------------------------------

sub print_help {
    ho_print_help('head', $SCRIPT_NAME);

    ho_print_help('section', 'Description');
    ho_print_help("This script tracks reconnecting clients and can take action on ".
        "them, being either printing a warning or banning them from the server ".
        "automatically. Clients that reconnect rapidly are called 'hammering ".
        "clients', which explains the name of this script.\n");

    ho_print_help('section', 'Settings');
	ho_print_help('setting', 'ho_hammer_enable', 
		'Master setting to enable/disable this script.');
	ho_print_help('setting', 'ho_hammer_network_tags', 
		'Tags of the networks hammering clients must be tracked.');
	ho_print_help('setting', 'ho_hammer_ignore_spoofs', 
		'Whether spoofs should be ignored.');
	ho_print_help('setting', 'ho_hammer_kline_reason', 
		'The reason of the ban placed on the hammering client.');

    ho_print_help('setting', 'ho_hammer_warning_count', 'and');
    ho_print_help('setting', 'ho_hammer_warning_time', 
        'If clients from a host connect more than ho_hammer_warning_count '.
        'times in ho_hammer_warning_time seconds, a warning is printed.');

    ho_print_help('setting', 'ho_hammer_violation_count', 'and');
    ho_print_help('setting', 'ho_hammer_violation_time', 
        'If clients from a host connect more than ho_hammer_violation_count '.
        'times in ho_violation_warning_time seconds, the host is banned.');
}
