qpsmtpd/t/plugin_tests/greylisting
2015-03-09 17:17:26 -05:00

399 lines
13 KiB
Perl

#!perl -w
use strict;
use warnings;
use Qpsmtpd::Address;
use Qpsmtpd::Constants;
my $test_email = 'user@example.com';
sub register_tests {
my $self = shift;
$self->register_test("test_load_exclude_files");
$self->register_test('test_data_handler');
$self->register_test('test_get_greylist_key');
$self->register_test('test_exclude');
$self->register_test("test_greylist_geoip");
$self->register_test("test_greylist_p0f_genre");
$self->register_test("test_greylist_p0f_distance");
$self->register_test("test_greylist_p0f_link");
$self->register_test("test_greylist_p0f_uptime");
$self->register_test('test_exclude_file_match');
$self->register_test('test_prune_db');
$self->register_test('test_greylist');
$self->register_test('test_init_redis');
$self->register_test('test_init_dbm');
$self->register_test('test_parse_redis_server');
$self->register_test('test_failcode');
$self->register_test('test_errcode');
}
sub test_load_exclude_files {
my ( $self ) = @_;
delete $self->{$_} for qw( _exclude_ip _exclude_hostname exclude_re );
$self->load_exclude_files();
ok( $self->{_exclude_ip}{'194.7.234.142'},
'Excluded IPs populated by load_exclude_files()' );
ok( $self->{_exclude_hostname}{'yahoo.com'},
'Excluded hostnames populated by load_exclude_files()' );
ok( ( grep { $_ eq qr/^mta[12].siol.net$/ } @{ $self->{_exclude_re} || [] } ),
'Excluded REs populated by load_exlude_files()' );
}
sub test_exclude_file_match {
my ( $self ) = @_;
my @test_data = (
{
ip => 192.168.1.1,
hostname => 'mta1234.siol.net',
expected => 0,
descr => 'miss',
},
{
ip => '194.7.234.142',
hostname => 'mta1234.siol.net',
expected => 1,
descr => 'IP match',
},
{
ip => 192.168.1.1,
hostname => 'postini.com',
expected => 1,
descr => 'Hostname match',
},
{
ip => 192.168.1.1,
hostname => 'mta2.siol.net',
expected => 1,
descr => 'Regex match',
},
);
for my $t ( @test_data ) {
$self->connection->remote_ip( $t->{ip} );
$self->connection->remote_host( $t->{hostname} );
is( $self->exclude_file_match(), $t->{expected}, "exclude_file_match(): $t->{descr}" );
}
}
sub test_data_handler {
my $self = shift;
my $transaction = $self->qp->transaction;
my ($code, $mess) = $self->data_handler( $transaction );
cmp_ok( $code, '==', DECLINED, "no note" );
$transaction->notes('greylist', 1);
($code, $mess) = $self->data_handler( $transaction );
cmp_ok( $code, '==', DECLINED, "no recipients");
my $address = Qpsmtpd::Address->new( "<$test_email>" );
$transaction->recipients( $address );
$transaction->notes('whitelistrcpt', 2);
($code, $mess) = $self->data_handler( $transaction );
cmp_ok( $code, '==', DENYSOFT, "missing recipients");
$transaction->notes('whitelistrcpt', 1);
($code, $mess) = $self->data_handler( $transaction );
cmp_ok( $code, '==', DECLINED, "missing recipients");
}
sub test_get_greylist_key {
my $self = shift;
$self->{_args}{sender} = 0;
$self->{_args}{recipient} = 0;
$self->{_args}{remote_ip} = 0;
my $test_ip = '192.168.1.1';
my $address = Qpsmtpd::Address->new( "<$test_email>" );
$self->qp->transaction->sender( $address );
$self->qp->transaction->add_recipient( $address );
$self->qp->connection->remote_ip($test_ip);
my $key = $self->get_greylist_key();
ok( ! $key, "db key empty: -");
$self->{_args}{remote_ip} = 1;
$key = $self->get_greylist_key( $address, $address );
cmp_ok( $key, 'eq', '3232235777', "db key: $key");
$self->{_args}{sender} = 1;
$key = $self->get_greylist_key( $address, $address );
cmp_ok( $key, 'eq', "3232235777:$test_email", "db key: $key");
$self->{_args}{recipient} = 1;
$key = $self->get_greylist_key( $address, $address );
cmp_ok( $key, 'eq', "3232235777:$test_email:$test_email", "db key: $key");
}
sub test_exclude {
my ( $self ) = @_;
$self->connection->relay_client(1);
ok( $self->exclude(), "Relay client results in exclude() hit" );
$self->connection->relay_client(0);
ok( ! $self->exclude(), "Non-relay client results in exclude() miss" );
my $old_p0f = $self->connection->notes('p0f');
$self->connection->notes('p0f'=> { genre => 'windows' } );
delete $self->{_args}{p0f};
ok( ! $self->exclude(), 'no p0f args = no exclusion' );
$self->{_args}{'p0f'} = 'genre,Lindows';
ok( $self->exclude(), 'p0f miss = exclusion' );
$self->{_args}{'p0f'} = 'genre,Windows';
ok( ! $self->exclude(), 'p0f hit = no exclusion' );
$self->connection->notes( p0f => $old_p0f );
}
sub test_greylist_geoip {
my $self = shift;
$self->{_args}{'geoip'} = 'US,UK,HU';
my @valid = qw/ US us UK hu /;
my @invalid = qw/ PK RU ru /;
foreach my $cc ( @valid ) {
$self->connection->notes('geoip_country', $cc );
ok( $self->geoip_match(), "match + ($cc)");
ok( $self->exclude(), "match + ($cc) results in exclude() hit");
}
foreach my $cc ( @invalid ) {
$self->connection->notes('geoip_country', $cc );
ok( ! $self->geoip_match(), "bad - ($cc)");
ok( ! $self->exclude(), "miss - ($cc) results in exclude() miss");
}
}
sub test_greylist_p0f_genre {
my $self = shift;
$self->{_args}{'p0f'} = 'genre,Linux';
$self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
ok( ! $self->p0f_match(), 'p0f genre miss');
$self->{_args}{'p0f'} = 'genre,Windows';
$self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
ok( $self->p0f_match(), 'p0f genre hit');
}
sub test_greylist_p0f_distance {
my $self = shift;
$self->{_args}{'p0f'} = 'distance,8';
$self->connection->notes('p0f'=> { distance=>9 } );
ok( $self->p0f_match(), 'p0f distance hit');
$self->{_args}{'p0f'} = 'distance,8';
$self->connection->notes('p0f'=> { distance=>7 } );
ok( ! $self->p0f_match(), 'p0f distance miss');
}
sub test_greylist_p0f_link {
my $self = shift;
$self->{_args}{'p0f'} = 'link,dsl';
$self->connection->notes('p0f'=> { link=>'DSL' } );
ok( $self->p0f_match(), 'p0f link hit');
ok( ! $self->exclude(), 'p0f link hit results in exclude() miss' );
$self->{_args}{'p0f'} = 'link,dsl';
$self->connection->notes('p0f'=> { link=>'Ethernet' } );
ok( ! $self->p0f_match(), 'p0f link miss');
ok( $self->exclude(), 'p0f link miss results in exclude() hit' );
}
sub test_greylist_p0f_uptime {
my $self = shift;
$self->{_args}{'p0f'} = 'uptime,100';
$self->connection->notes('p0f'=> { uptime=> 99 } );
ok( $self->p0f_match(), 'p0f uptime hit');
$self->{_args}{'p0f'} = 'uptime,100';
$self->connection->notes('p0f'=> { uptime=>500 } );
ok( ! $self->p0f_match(), 'p0f uptime miss');
}
my $mocktime;
{
no warnings qw( redefine );
sub now { $mocktime || time() }
}
sub test_prune_db {
my ($self) = @_;
my $start = time() - 40 * 3600 * 24; # 40 days ago
my $oneday = $start - 60 * 60 * 24;
my $onemonth = $start - 60 * 60 * 24 * 30;
my $twomonths = $start - 60 * 60 * 24 * 60;
$self->{_args} = {
white_timeout => 36 * 3600 * 24, # 36 days
};
for my $test_class (@Qpsmtpd::DB::child_classes) {
delete $self->{db};
eval { $self->db( class => $test_class, dir => 't/tmp' ) };
next if $@;
$self->db->lock;
$self->db->flush;
$self->db->set( startkey => "$start:testdata" );
$self->db->set( onedaykey => "$oneday:testdata" );
$self->db->set( onemonthkey => "$onemonth:testdata" );
$self->db->set( twomonthkey => "$twomonths:testdata" );
$self->db->unlock;
is( $self->allkeys, 'onedaykey|onemonthkey|startkey|twomonthkey',
'initial prune_db() test data set correctly' );
$self->db->unlock;
$mocktime = $start;
$self->prune_db;
is( $self->allkeys, 'onedaykey|onemonthkey|startkey',
'prune_db() expires two-month-old data' );
$mocktime = $start + 60 * 60 * 24 * 7;
$self->prune_db;
is( $self->allkeys, 'onedaykey|startkey',
'prune_db() expires one-month-old data 7 days later' );
$mocktime = $start + 60 * 60 * 24 * 37;
$self->prune_db;
is( $self->allkeys, '',
'prune_db() expires all remaining keys 37 days later' );
}
# Ensure prune_db does not leave the database locked after doing nothing
delete $self->{db};
$self->db( class => 'Qpsmtpd::DB::File::DBM', dir => 't/tmp' );
$self->prune_db;
$self->prune_db;
ok(1, "We won't get this far if prune_db leaves the database locked");
}
sub allkeys {
my ($self) = @_;
$self->db->lock;
my $allkeys = join '|', sort $self->db->get_keys;
$self->db->unlock;
return $allkeys;
}
sub test_greylist {
my ( $self ) = @_;
$self->{_args} = {
remote_ip => 1,
sender => 0,
recipient => 0,
reject => 1,
black_timeout => 50 * 60, # 50m
grey_timeout => 3 * 3600 + 20 * 60, # 3h:20m
white_timeout => 36 * 3600 * 24, # 36 days
p0f => 0,
geoip => 0,
};
$self->connection->remote_host('example.com');
$self->connection->remote_ip('1.2.3.4');
my $sender = Qpsmtpd::Address->new( "<$test_email>" );
my $rcpt = Qpsmtpd::Address->new( "<$test_email>" );
for my $test_class (@Qpsmtpd::DB::child_classes) {
delete $self->{db};
eval { $self->db( class => $test_class, dir => 't/tmp' ) };
if ( $@ ) {
warn "Unable to test greylisting against $test_class: $@";
next;
}
$self->db->lock;
$self->db->flush;
$self->db->unlock;
my $start = time() - 40 * 3600 * 24; # 40 days ago
$mocktime = $start;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
'Initial connection attempt greylisted' );
$mocktime = $start + 60 * 49;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
'Greylisted 49 minutes later' );
$mocktime = $start + 60 * 51;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DECLINED',
'Allowed 51 minutes later' );
$mocktime = $start + 60 * 52 + 36 * 3600 * 24;
$self->prune_db;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
're-greylisted 36 days later' );
$self->db->lock;
$self->db->flush;
$self->db->unlock;
}
}
sub rc {
my ( $self, $r, $msg ) = @_;
return '' if ! defined $r;
return return_code($r) if ! defined $msg;
return return_code($r) . ": $msg";
}
sub test_init_redis {
my ($self) = @_;
delete $self->{db};
$self->{_args}{redis} = 'testredis';
$self->init_db;
is( keyvals($self->db_args),
'class=Qpsmtpd::DB::Redis;cnx_timeout=1;'
. 'name=greylist;server=testredis:6379',
'init_redis() sets redis args' );
}
sub keyvals {
my ( %h ) = @_;
return join ";", map { "$_=$h{$_}" } sort keys %h;
}
sub test_init_dbm {
my ($self) = @_;
delete $self->{db};
delete $self->{_args}{redis};
$self->{_args}{db_dir} = 't/tmp';
$self->init_db;
is( $self->db->name, 'greylist', 'init_dbm() sets correct db name' );
is( $self->db->path, 't/tmp/greylist.dbm', 'init_dbm() sets correct path' );
is( ref $self->db, 'Qpsmtpd::DB::File::DBM', 'init_dbm() gives DBM object' );
}
sub test_parse_redis_server {
my ($self) = @_;
$self->{_args}{redis} = 'asdf:1234';
is( $self->parse_redis_server, 'asdf:1234',
'parse_redis_server(): leave provided port alone' );
$self->{_args}{redis} = 'qwerty';
is( $self->parse_redis_server, 'qwerty:6379',
'parse_redis_server(): add default port' );
}
sub test_failcode {
my ($self) = @_;
$self->{_args}{reject} = 0;
is( $self->rc( $self->failcode() ), 'DECLINED',
'failcode(): reject disabled' );
delete $self->{_args}{reject};
is( $self->rc( $self->failcode() ), 'DENYSOFT: This mail is temporarily denied',
'failcode(): reject enabled' );
}
sub test_errcode {
my ($self) = @_;
delete $self->qp->{_logged};
is( $self->rc( $self->errcode('test error') ),
'DECLINED',
'errcode(): proper return code' );
is( join("\n", @{ $self->qp->{_logged} || [] }),
'LOGERROR:greylistingUNABLE TO OBTAIN GREYLIST RESULT:test error',
'errocode(): logged error' );
}