#!perl -w use strict; use warnings; use Qpsmtpd::Constants; sub register_tests { my $self = shift; $self->register_test('test_add_headers'); $self->register_test('test_get_v2_query'); $self->register_test('test_get_v3_query'); $self->register_test('test_store_v2_results'); $self->register_test('test_store_v3_results'); $self->register_test('test_register_headers'); $self->register_test('test_register_genre_blocking'); $self->register_test('test_rcpt_handler'); $self->register_test('test_check_genre'); $self->register_test('test_exclude_connection'); $self->register_test('test_exclude_recipient'); } sub register_hook { # Override normal register_hook() to record behavior my $self = shift; $self->{_lastreg} = join ',', @_; } sub test_register_headers { my ( $self ) = @_; # reset last_register_args $self->register_hook(); delete $self->{_args}{add_headers}; $self->register_headers(); is( $self->{_lastreg}, 'data_post,add_headers', 'register_headers() registers data_post hook by default' ); $self->register_hook(); $self->{_args}{add_headers} = 'asdf'; $self->register_headers(); is( $self->{_lastreg}, 'data_post,add_headers', 'register_headers() registers data_post hook on invalid input' ); $self->register_hook(); $self->{_args}{add_headers} = 'true'; $self->register_headers(); is( $self->{_lastreg}, 'data_post,add_headers', 'register_headers() registers data_post hook when explicitly enabled' ); $self->register_hook(); $self->{_args}{add_headers} = 'false'; $self->register_headers(); is( $self->{_lastreg}, '', 'register_headers() does not register data_post hook when disabled' ); } sub test_register_genre_blocking { my ( $self ) = @_; $self->register_hook(); $self->register_genre_blocking(); is( $self->{_lastreg}, '', 'rcpt_handler not registered when no blocked genres are configured' ); $self->register_hook(); $self->fake_config( p0f_blocked_operating_systems => 'Crapple Macintawsh' ); $self->register_genre_blocking(); is( $self->{_lastreg}, 'rcpt,rcpt_handler', 'rcpt_handler registered when blocked genre phrases are configured' ); is( join('|', @{ delete $self->{os_block} || [] }), 'Crapple Macintawsh', 'blocked OS phrases processed correctly' ); is( join('|', @{ delete $self->{os_block_re} || [] }), '', 'no blocked OS regexes' ); $self->register_hook(); $self->fake_config( p0f_blocked_operating_systems => '/windoze/' ); $self->register_genre_blocking(); is( $self->{_lastreg}, 'rcpt,rcpt_handler', 'rcpt_handler registered when blocked genre regexes are configured' ); is( join('|', @{ delete $self->{os_block} || [] }), '', 'no blocked OS phrases' ); is( join('|', @{ delete $self->{os_block_re} || [] }), qr/windoze/i, 'blocked OS regexes processed correctly' ); $self->unfake_config; } sub test_rcpt_handler { my ( $self ) = @_; $self->{os_block} = ['Windows 7 or 8']; my ( $r, $msg ) = $self->rcpt_handler(); is( return_code($r) . "/$msg", "DENY/OS Blocked", 'rcpt_handler rejects on p0f genre match' ); $self->{os_block} = ['Commodor 16']; ( $r, $msg ) = $self->rcpt_handler(); $msg = 'undef' if ! defined $msg; is( return_code($r) . "/$msg", "DECLINED/undef", 'rcpt_handler returns DECLINED on no p0f genre match' ); delete $self->{os_block}; } sub test_check_genre { my ( $self ) = @_; $self->{os_block_re} = [qr/windows/i]; ok( $self->check_genre, 'check_genre() returns true on OS match' ); $self->{os_block_re} = [qr/windoze/i]; ok( ! $self->check_genre, 'check_genre() returns false for no OS match' ); delete $self->{os_block_re}; } sub test_exclude_connection { my ( $self ) = @_; $self->connection->relay_client(0); ok( ! $self->exclude_connection, 'default: exclude no connections from genre check' ); $self->connection->notes( p0f_exclude => undef ); $self->connection->relay_client(1); ok( $self->exclude_connection, 'relay clients excluded from genre check' ); } sub test_exclude_recipient { my ( $self ) = @_; ok( ! $self->exclude_recipient({}), 'default: exclude no recipients from genre check' ); } sub test_add_headers { my ( $self ) = @_; $self->connection->notes( 'p0f', { genre => 'test genre', link_type => 'test link_type', } ); my $header = $self->transaction->header( Mail::Header->new ); my @tags = (qw( X-P0F-Genre X-P0F-Link-Type )); $header->add( $_ => 'DELETETHIS' ) for @tags; $self->add_headers($self->transaction); is( $self->all_headers('X-P0F-Genre'), 'test genre', 'X-P0F-Genre header added' ); is( $self->all_headers('X-P0F-Link-Type'), 'test link_type', 'X-P0F-Link-Type header added' ); } sub all_headers { # Return all instances of a given message header my ( $self, $tag ) = @_; return join " | ", map { chomp $_; $_ } $self->transaction->header->get($tag); } sub test_query_p0f_v2 { #TODO # get path to p0f socket # see if it exists # try to connect to it # if connection succeeds, send it a query # do we a) pick an IP that recently connected? # or b) create a connection to localhost... # or c) is there a p0f test value? # parse and validate the response # using $self->test_v2_response() } sub test_query_p0f_v3 { #TODO: similar to v2 .... } sub test_get_v2_query { my $self = shift; my $local_ip = '208.75.177.101'; my $remote = '108.60.149.81'; $self->{_args}{local_ip} = $local_ip; $self->qp->connection->local_ip($local_ip); $self->qp->connection->remote_ip($remote); $self->qp->connection->local_port(25); $self->qp->connection->remote_port(2500); my $r = $self->get_v2_query(); ok( $r, 'r +' ); } sub test_get_v3_query { my $self = shift; my $remote = '108.60.149.81'; $self->qp->connection->remote_ip($remote); my $r = $self->get_v3_query(); ok( $r, 'any +' ); } sub test_store_v2_results { my $self = shift; my $response = pack("L L C Z20 Z40 c Z30 Z30 C C C s S N", '233811181', '1336687857', '0', 'Windows', 'XP/2000 (RFC1323+, w+, tstamp-)', '11', 'ethernet/modem', '', '0', '0', '1', '-25600', '255', '255' ); my $r = $self->store_v2_results( $response ); ok( $r, "r: +") or return; ok( $r->{genre} =~ /windows/i, "genre +" ); } sub test_store_v3_results { my $self = shift; my $response = pack("L L L L L L L L L s C C A32 A32 A32 A32 A32 A32 A32", 1345340930, 16, 1336676595, 1336680290, 3, 0, 0, 0, 0, 13, 0, 0, 'Windows', '7 or 8', '', '', 'Ethernet or modem', '', ''); my $r = $self->store_v3_results( $response ); ok( $r, "result"); ok( $r->{genre} =~ /windows/i, "genre" ); }