Author: vetinari
Date: Fri Jan 25 11:35:49 2008
New Revision: 838
Added:
contrib/vetinari/pipeline_sync
Log:
pipeline_sync - advanced check_earlytalker plugin
* checks at connect, data, vrfy and noop
* different wait times for each hook
Added: contrib/vetinari/pipeline_sync
==============================================================================
--- (empty file)
+++ contrib/vetinari/pipeline_sync Fri Jan 25 11:35:49 2008
@@ -0,0 +1,216 @@
+
+=head1 NAME
+
+pipeline_sync - advanced version of the check_earlytalker plugin
+
+=head1 DESCRIPTION
+
+This plugin checks if the remote host honours RFC 1854 (SMTP Service Extension
+for Command Pipelining), which we announce:
+
+ ``The EHLO, DATA, VRFY, EXPN, TURN, QUIT, and NOOP commands can
+ only appear as the last command in a group since their success
+ or failure produces a change of state which the client SMTP
+ must accommodate. (NOOP is included in this group so it can be
+ used as a synchronization point.)''
+
+The B<pipeline_sync> plugin may be configure to check at EHLO/HELO, DATA,
+VRFY and NOOP. QUIT would be to late anyway and the other commands are not
+supported by qpsmtpd. Any data sent by the client before the configured
+timeout is a no-go and the hook returns with the specified action.
+
+=head1 CONFIGURATION
+
+Arguments are key/value pairs. Setting the TIMEOUT values below to C<0>
+disables the checks for the given hook. Known arguments are:
+
+=over 4
+
+=item connect TIMEOUT
+
+Wait TIMEOUT seconds at connect before sending out the greeting banner, default
+timeout is C<1>.
+
+=item data TIMEOUT
+
+Wait TIMEOUT seconds before sending the C<354> message to the client, default
+timeout is C<1>.
+
+
+=item noop TIMEOUT
+
+Wait TIMEOUT seconds before returning C<250 OK> to the client, this check is
+disabled by default.
+
+=item vrfy TIMEOUT
+
+Wait TIMEOUT seconds before the other hook_vrfy() do something, this check is
+disabled by default.
+
+=item action DENY(SOFT)?(_DISCONNECT)?
+
+Sent (a temporary) error to the client and optionally disconnect, see
+F<docs/plugins.pod> and F<Qpsmtpd::Constants> for more info about the
+DENY, DENYSOFT, DENY_DISCONNECT and DENYSOFT_DISCONNECT constants.
+
+Default action is C<DENY>.
+
+=item defer BOOLEAN
+
+If set to a true value, clients talking before the greeting banner will be
+punished after sending C<MAIL FROM> instead of immediately. Default is NOT
+to defer pubishing.
+
+=back
+
+=head1 NOTES
+
+C<hook_vrfy()> does currently not support DENYSOFT* and DENY_DISCONNECT.
+
+C<hook_noop()> does currently not support DENYSOFT*.
+
+Drop me a note if you want to use one of the above.
+
+=head1 AUTHOR
+
+Based on the check_earlytalker plugin from the qpsmtpd distribution.
+
+(c) 2008, Hanno Hecker <[EMAIL PROTECTED]>
+
+This software is free software and may be distributed under the same
+terms as qpsmtpd itself.
+
+=cut
+
+use IO::Select;
+
+use warnings;
+use strict;
+
+sub register {
+ my ($self, $qp, %args) = @_;
+
+ $self->{_is_apache} = 0;
+ $self->{_action} = "DENY";
+ $self->{_defer} = 0;
+ my %checks = ( # default values... timeout in seconds
+ 'connect' => 1,
+ 'data' => 1,
+ 'vrfy' => 0,
+ 'noop' => 0,
+ );
+
+ foreach my $key (keys %args) {
+ if (exists $checks{$key}) {
+ next unless $args{$key} =~ /^\d+$/;
+ $checks{$key} = $args{$key};
+ }
+ elsif ($key eq "action") {
+ next unless $args{"action"} =~ /^DENY(SOFT)?(_DISCONNECT)?$/i;
+ $self->{_action} = Qpsmtpd::Constants::return_code(uc
$args{"action"});
+ }
+ elsif ($key eq "defer-reject") {
+ $self->{_defer} = $args{"defer-reject"} ? 1 : 0;
+ }
+ }
+ $self->{_checks} = \%checks;
+
+ if ($qp->{conn} && $qp->{conn}->isa('Apache2::Connection')) {
+ require APR::Const;
+ APR::Const->import(qw(POLLIN SUCCESS));
+ $self->{_is_apache} = 1;
+ }
+ 1;
+}
+
+sub sync_check {
+ my $self = shift;
+
+ my $hook = $self->hook_name();
+ my $timeout = $self->{_checks}->{$hook} || 0;
+ ## print STDERR "hook = $hook(), timeout = $timeout\n";
+ return (DECLINED) unless $timeout;
+
+ return DECLINED
+ if ($self->qp->connection->notes('whitelistclient'));
+
+ my $rc = 0;
+ if ($self->{_is_apache}) {
+ $rc = $self->wait_apache($timeout);
+ }
+ else {
+ $rc = $self->wait_qpsmtpd($timeout);
+ }
+ return (DECLINED, "remote host said nothing spontaneous at hook_$hook(),
proceeding")
+ unless ($rc);
+
+ my $ip = $self->qp->connection->remote_ip;
+
+ $_ = $hook;
+ /^connect$/
+ and do {
+ $self->log(LOGNOTICE, "remote host [$ip] started talking before we
said hello");
+ if ($self->{_defer}) {
+ $self->qp->connection->notes('earlytalker', 1);
+ return (DECLINED);
+ }
+ return($self->{_action}, "Connecting host started transmitting before
SMTP greeting");
+ };
+
+ /^data$/
+ and $self->log(LOGNOTICE, "remote host [$ip] started with message before
we allowed to"),
+ return($self->{_action}, "I wasn't ready to receive your message");
+
+ /^noop$/
+ and $self->log(LOGNOTICE, "remote host [$ip] didn't wait after NOOP"),
+ return($self->{_action}, "NOOP took longer than you expected?");
+
+ /^vrfy$/
+ and $self->log(LOGNOTICE, "remote host [$ip] didn't wait for address
verification"),
+ return($self->{_action}, "Not enough time to wait for verification of
recipient?");
+
+ return (DECLINED); # how did we get here?
+}
+
+*hook_connect =
+ *hook_data =
+ *hook_noop =
+ *hook_vrfy =
+ \&sync_check;
+
+sub hook_mail {
+ my $self = shift;
+ return (DECLINED)
+ unless ($self->{_defer} and $self->qp->connection->notes('earlytalker'));
+
+ return($self->{_action},
+ "Connecting host started transmitting before SMTP greeting");
+}
+
+sub wait_apache {
+ my ($self, $timeout) = @_;
+
+ $timeout *= 1_000_000;
+
+ my $c = $self->qp->{conn};
+ my $socket = $c->client_socket;
+
+ my $rc = $socket->poll($c->pool, $timeout, APR::Const::POLLIN());
+ return 1 if ($rc == APR::Const::SUCCESS());
+
+ return 0;
+}
+
+sub wait_qpsmtpd {
+ my ($self, $timeout) = @_;
+ my $in = new IO::Select;
+
+ $in->add(\*STDIN) || return 0;
+ return 1 if $in->can_read($timeout);
+
+ return 0;
+}
+
+1;
+
+# vim: ts=4 sw=4 expandtab syn=perl