added p0f config option
added POD docs to explain usage
modified $dbdir selection logic. The previous logic failed when QPHOME was
  not selected (as is the case when tests are being run).
Added '.' as the dir of last resort for $dbdir selection (others $EMPTY/dir
  dumped greylisting database in / )
Added t/plugin_tests/greylisting, with greylist logic testing (tests are
  disabled by default, as greylisting is disabled in config.sample/plugins)
---
 plugins/greylisting        |   57 +++++++++++++++++++++--
 t/plugin_tests/greylisting |  109 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 161 insertions(+), 5 deletions(-)
 create mode 100644 t/plugin_tests/greylisting

diff --git a/plugins/greylisting b/plugins/greylisting
index 2bdfbc1..9d28ce5 100644
--- a/plugins/greylisting
+++ b/plugins/greylisting
@@ -105,6 +105,23 @@ Flag to indicate whether to use per-recipient greylisting
 databases (default is to use a shared database).  Per-recipient configuration
 directories, if determined, supercede I<db_dir>.
 
+=item p0f
+
+Enable greylisting only when certain p0f criteria is met. The single
+required argument is a comma delimited list of key/value pairs. The keys
+are the following p0f TCP fingerprint elements: genre, detail, uptime,
+link, and distance. 
+
+To greylist emails from computers whose remote OS is windows, you'd use
+this syntax:
+
+  p0f genre,windows
+
+To greylist only windows computers on DSL links more than 3 network hops
+away:
+
+  p0f genre,windows,link,dsl,distance,3
+ 
 =back
 
 =head1 BUGS
@@ -117,6 +134,8 @@ use something like File::NFSLock instead.
 
 Written by Gavin Carr <ga...@openfusion.com.au>.
 
+Added p0f section <mattsimer...@cpan.org> (2010-05-03)
+
 =cut
 
 BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) }
@@ -124,13 +143,13 @@ use AnyDBM_File;
 use Fcntl qw(:DEFAULT :flock);
 use strict;
 
-my $VERSION = '0.07';
+my $VERSION = '0.08';
 
 my $DENYMSG = "This mail is temporarily denied";
 my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!);
 my $DB = "denysoft_greylist.dbm";
 my %PERMITTED_ARGS = map { $_ => 1 } qw(per_recipient remote_ip sender 
recipient
-  black_timeout grey_timeout white_timeout deny_late mode db_dir);
+  black_timeout grey_timeout white_timeout deny_late mode db_dir p0f );
 
 my %DEFAULTS = (
                 remote_ip     => 1,
@@ -140,6 +159,7 @@ my %DEFAULTS = (
                 grey_timeout  => 3 * 3600 + 20 * 60,
                 white_timeout => 36 * 24 * 3600,
                 mode          => 'denysoft',
+                p0f           => undef,
                );
 
 sub register {
@@ -224,17 +244,23 @@ sub denysoft_greylist {
     return DECLINED if $self->qp->connection->notes('whitelisthost');
     return DECLINED if $transaction->notes('whitelistsender');
 
+    # do not greylist if p0f matching is selected and message does not match
+    return DECLINED if $config->{'p0f'} && !$self->p0f_match( $config );
+
     if ($config->{db_dir} && $config->{db_dir} =~ m{^([-a-zA-Z0-9./_]+)$}) {
         $config->{db_dir} = $1;
     }
 
     # Setup database location
-    my $dbdir = $transaction->notes('per_rcpt_configdir')
+    my $dbdir;
+    $dbdir = $transaction->notes('per_rcpt_configdir')
       if $config->{per_recipient_db};
     for my $d ($dbdir, $config->{db_dir}, "/var/lib/qpsmtpd/greylisting",
-               "$QPHOME/var/db", "$QPHOME/config")
+               "$QPHOME/var/db", "$QPHOME/config", '.' )
     {
-        last if $dbdir ||= $d && -d $d && $d;
+        last if $dbdir && -d $dbdir;
+        next if ( ! $d || ! -d $d );
+        $dbdir = $d;
     }
     my $db = "$dbdir/$DB";
     $self->log(LOGINFO, "using $db as greylisting database");
@@ -316,5 +342,26 @@ sub denysoft_greylist {
     return $config->{mode} eq 'testonly' ? DECLINED : DENYSOFT, $DENYMSG;
 }
 
+sub p0f_match {
+    my $self = shift;
+    my $config = shift;
+
+    my $p0f = $self->connection->notes('p0f');
+    return if !$p0f || !ref $p0f;     # p0f fingerprint info not found
+
+    my %valid_matches = map { $_ => 1 } qw( genre detail uptime link distance 
);
+    my %requested_matches = split(/\,/, $config->{'p0f'} );
+
+    foreach my $key (keys %requested_matches) {
+        next if !defined $valid_matches{$key}; # discard invalid match keys
+        my $value = $requested_matches{$key};
+        return 1 if $key eq 'distance' && $p0f->{$key} > $value;
+        return 1 if $key eq 'genre'    && $p0f->{$key} =~ /$value/i;
+        return 1 if $key eq 'uptime'   && $p0f->{$key} < $value;
+        return 1 if $key eq 'link'     && $p0f->{$key} =~ /$value/i;
+    }
+    return;
+}
+
 # arch-tag: 6ef5919e-404b-4c87-bcfe-7e9f383f3901
 
diff --git a/t/plugin_tests/greylisting b/t/plugin_tests/greylisting
new file mode 100644
index 0000000..5c8f068
--- /dev/null
+++ b/t/plugin_tests/greylisting
@@ -0,0 +1,109 @@
+use Qpsmtpd::Address;
+
+my $test_email = 'u...@example.com';
+my $address = Qpsmtpd::Address->new( "<$test_email>" );
+
+my @greydbs = qw( denysoft_greylist.dbm denysoft_greylist.dbm.lock );
+foreach ( @greydbs ) {
+    unlink $_ if -f $_;
+};
+
+sub register_tests {
+    my $self = shift;
+    $self->register_test("test_greylist_p0f_genre_miss", 1);
+    $self->register_test("test_greylist_p0f_genre_hit", 1);
+    $self->register_test("test_greylist_p0f_distance_hit", 1);
+    $self->register_test("test_greylist_p0f_distance_miss", 1);
+    $self->register_test("test_greylist_p0f_link_hit", 1);
+    $self->register_test("test_greylist_p0f_link_miss", 1);
+    $self->register_test("test_greylist_p0f_uptime_hit", 1);
+    $self->register_test("test_greylist_p0f_uptime_miss", 1);
+}
+
+sub test_greylist_p0f_genre_miss {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'genre,Linux';
+    $self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
+    my $r = $self->rcpt_handler( $self->qp->transaction );
+
+    ok( $r == 909, 'p0f genre miss');
+}
+
+sub test_greylist_p0f_genre_hit {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'genre,Windows';
+    $self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r eq 'This mail is temporarily denied', 'p0f genre hit');
+}
+
+sub test_greylist_p0f_distance_hit {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'distance,8';
+    $self->connection->notes('p0f'=> { distance=>9 } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r eq 'This mail is temporarily denied', 'p0f distance hit');
+}
+
+sub test_greylist_p0f_distance_miss {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'distance,8';
+    $self->connection->notes('p0f'=> { distance=>7 } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r == 909, 'p0f distance miss');
+}
+
+sub test_greylist_p0f_link_hit {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'link,dsl';
+    $self->connection->notes('p0f'=> { link=>'DSL' } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r eq 'This mail is temporarily denied', 'p0f link hit');
+}
+
+sub test_greylist_p0f_link_miss {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'link,dsl';
+    $self->connection->notes('p0f'=> { link=>'Ethernet' } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r == 909, 'p0f link miss');
+}
+
+sub test_greylist_p0f_uptime_hit {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'uptime,100';
+    $self->connection->notes('p0f'=> { uptime=> 99 } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r eq 'This mail is temporarily denied', 'p0f uptime hit');
+}
+
+sub test_greylist_p0f_uptime_miss {
+    my $self = shift;
+
+    $self->{_greylist_config}{'p0f'} = 'uptime,100';
+    $self->connection->notes('p0f'=> { uptime=>500 } );
+    $self->qp->transaction->sender( $address );
+    my $r = $self->rcpt_handler( $self->qp->transaction  );
+
+    ok( $r == 909, 'p0f uptime miss');
+}
+
-- 
1.7.0.6

Reply via email to