[svn:qpsmtpd] r700 - in branches/0.3x: . config.sample lib lib/Qpsmtpd plugins
[email protected] Sun, 31 Dec 2006 03:07:34 -0800 (PST)
| Newsgroups | perl.cvs.qpsmtpd |
|---|---|
| Message-ID | <[email protected]> |
Author: vetinari
Date: Sun Dec 31 03:07:32 2006
New Revision: 700
Added:
branches/0.3x/plugins/skip_plugins
Modified:
branches/0.3x/Changes
branches/0.3x/config.sample/plugins
branches/0.3x/lib/Qpsmtpd.pm
branches/0.3x/lib/Qpsmtpd/Plugin.pm
Log:
New "skip plugin API" + example plugin skip_plugins, see perldoc
Qpsmtpd::Plugins for more info. This can be used to disable (and re-
enable) loaded plugins for the current connection.
Modified: branches/0.3x/Changes
==============================================================================
--- branches/0.3x/Changes (original)
+++ branches/0.3x/Changes Sun Dec 31 03:07:32 2006
@@ -1,5 +1,9 @@
0.33 (to be)
+ New "skip plugin API" + example plugin skip_plugins, see perldoc
+ Qpsmtpd::Plugins for more info. This can be used to disable (and re-
+ enable) loaded plugins for the current connection (Hanno Hecker)
+
Support "module" plugins ("My::Plugin" in the config/plugins file)
Make the badmailfrom plugin support (optional) rejection messages after the
Modified: branches/0.3x/config.sample/plugins
==============================================================================
--- branches/0.3x/config.sample/plugins (original)
+++ branches/0.3x/config.sample/plugins Sun Dec 31 03:07:32 2006
@@ -12,6 +12,9 @@
# from one IP!
hosts_allow
+# skip selected plugins for some hosts:
+skip_plugins
+
# enable to accept MAIL FROM:/RCPT TO: addresses without surrounding <>
dont_require_anglebrackets
Modified: branches/0.3x/lib/Qpsmtpd.pm
==============================================================================
--- branches/0.3x/lib/Qpsmtpd.pm (original)
+++ branches/0.3x/lib/Qpsmtpd.pm Sun Dec 31 03:07:32 2006
@@ -367,6 +367,11 @@
$@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;
}
else {
+ my $skip = $self->connection->notes('_skip_plugins');
+ if (exists $skip->{$code->{name}} and $skip->{$code->{name}}) {
+ $self->log(LOGDEBUG, "skipping plugin ".$code->{name});
+ next;
+ }
$self->varlog(LOGDEBUG, $hook, $code->{name});
eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };
$@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;
Modified: branches/0.3x/lib/Qpsmtpd/Plugin.pm
==============================================================================
--- branches/0.3x/lib/Qpsmtpd/Plugin.pm (original)
+++ branches/0.3x/lib/Qpsmtpd/Plugin.pm Sun Dec 31 03:07:32 2006
@@ -1,6 +1,7 @@
package Qpsmtpd::Plugin;
use Qpsmtpd::Constants;
use strict;
+use vars qw(%symbols);
# more or less in the order they will fire
our @hooks = qw(
@@ -116,6 +117,8 @@
# don't reload plugins if they are already loaded
return if defined &{"${newPackage}::plugin_name"};
+ ### someone test this please:
+ # return if $self->plugin_is_loaded($newPackage);
$self->compile($self->plugin_name . "_isa_$cleanParent",
$newPackage,
@@ -183,5 +186,211 @@
}
}
+=head1 SKIP PLUGINS API
+
+These functions allow to disable and re-enable loaded plugins. Loading
+plugins after the initial loading phase is not possible. The earliest
+place to disable a plugin is in C<hook_connect()>.
+
+If you want to run a plugin just for some clients, load it like a usual
+plugin and either hook it to the C<hook_connect()> (or any later hook)
+and disable it there, use the C<skip_plugins> plugin or write your own
+disabling plugin.
+
+These modifications of disabling/re-enabling a plugin are valid for the
+full connection, not transaction! For transaction based disabling of plugins,
+use the C<reset_transaction> hook to reset the list of disabled plugins.
+
+A small warning: the C<reset_transaction> hook is called at least three
+times: after the client sent the C<(HE|EH)LO>, every time the client
+issues a C<MAIL FROM:> and after the mail was queued (or rejected by a
+C<data_post> hook). Don't forget it is also called after C<RSET> and
+connection closing (e.g. after C<QUIT>).
+
+=over 7
+
+=item plugin_is_loaded( $plugin )
+
+Returns true, if the given (escaped) plugin name is a loaded plugin
+
+=cut
+
+sub plugin_is_loaded {
+ my ($self, $plugin) = @_;
+ $plugin =~ s/^Qpsmtpd::Plugin:://; # for _loaded();
+ # each plugin has a sub called "plugin_name()", see compile() above...
+ # ... this restricts qpsmtpd a bit: No module named
+ # Qpsmtpd::Plugin(|::Something) must have a sub "plugin_name()", or
+ # it will be returned as a loaded plugin...
+ return defined &{"Qpsmtpd::Plugin::${plugin}::plugin_name"};
+}
+
+=item plugin_status( $plugin )
+
+Shows the status of the given plugin. It returns undef if no plugin name
+given or the plugin is not loaded, "0" if plugin is loaded, but disabled
+and "1" if the plugin is loaded and active. The plugin name must be escaped
+by B<escape_plugin()>.
+
+=cut
+
+sub plugin_status {
+ my ($self, $plugin) = @_;
+ return undef unless $plugin;
+ return undef unless $self->plugin_is_loaded($plugin);
+ my $skip = $self->qp->connection->notes('_skip_plugins') || {};
+ return 0 if (exists $skip->{$plugin} and $skip->{$plugin});
+ return 1;
+}
+
+=item loaded_plugins( )
+
+This returns a hash. Keys are (escaped, see below) plugin names of loaded
+plugins. The value tells you if the plugin is currently active (1) or
+disabled (0).
+
+=cut
+
+sub loaded_plugins {
+ my $self = shift;
+ # all plugins are in their own class "below" Qpsmtpd::Plugin,
+ # so we start searching the symbol table at this point
+ my %plugins = map {
+ s/^Qpsmtpd::Plugin:://;
+ ($_, 1)
+ } $self->_loaded("Qpsmtpd::Plugin");
+ foreach ($self->disabled_plugins) {
+ $plugins{$_} = 0;
+ }
+ return %plugins;
+}
+
+sub _loaded {
+ my $self = shift;
+ my $base = shift;
+ my @loaded = ();
+ my (@sub, $symbol);
+ # let's see what's in this name space
+ no strict 'refs';
+ local (*symbols) = *{"${base}::"};
+ use strict 'refs';
+ foreach my $name (values %symbols) {
+ # $name is read only while walking the stash
+
+ # not a class name? ok, next
+ ($symbol = $name) =~ s/^\*(.*)::$/$1/ || next;
+ next if $symbol eq "Qpsmtpd::Plugin";
+
+ # in qpsmtpd we have no way of loading a plugin with the same
+ # name as a sub directory inside the ./plugins dir, so we can safely
+ # use either the list of sub classes or the class itself we're
+ # looking at (unlike perl, e.g. Qpsmtpd.pm <-> Qpsmtpd/Plugin.pm).
+ @sub = $self->_loaded($symbol);
+
+ if (@sub) {
+ push @loaded, @sub;
+ }
+ else {
+ # is this really a plugin?
+ next unless $self->plugin_is_loaded($symbol);
+ push @loaded, $symbol;
+ }
+ }
+ return @loaded;
+}
+
+=item escape_plugin( $plugin_name )
+
+Turns a plugin filename into the way it is used inside qpsmtpd. This needs to
+be done before you B<plugin_disable()> or B<plugin_enable()> a plugin. To
+see if a plugin is loaded, use something like
+
+ my %loaded = $self->loaded_plugins;
+ my $wanted = $self->escape_plugin("virus/clamav");
+ if (exists $loaded{$wanted}) {
+ ...
+ }
+... or shorter:
+
+ if ($self->plugin_is_loaded($self->escape_plugin("virus/clamav"))) {
+ ...
+ }
+
+=cut
+
+sub escape_plugin {
+ my $self = shift;
+ my $plugin_name = shift;
+ # "stolen" from Qpsmtpd.pm
+ # Escape everything into valid perl identifiers
+ $plugin_name =~ s/([^A-Za-z0-9_\/])/sprintf("_%2x",unpack("C",$1))/eg;
+
+ # second pass cares for slashes and words starting with a digit
+ $plugin_name =~ s{
+ (/+) # directory
+ (\d?) # package's first character
+ }[
+ "::" . (length $2 ? sprintf("_%2x",unpack("C",$2)) : "")
+ ]egx;
+ return $plugin_name;
+}
+
+=item disabled_plugins( )
+
+This returns a list of all plugins which are disabled for the current
+connection.
+
+=cut
+
+sub disabled_plugins {
+ my $self = shift;
+ my @skipped = ();
+ my $skip = $self->qp->connection->notes('_skip_plugins') || {};
+ foreach my $s (keys %{$skip}) {
+ push @skipped, $s if $skip->{$s};
+ }
+ return @skipped;
+}
+
+=item plugin_disable( $plugin )
+
+B<plugin_disable()> disables a (loaded) plugin, it requires the plugin name
+to be escaped by B<escape_plugin()>. It returns true, if the given plugin
+name is a loaded plugin (and disables it of course).
+
+=cut
+
+sub plugin_disable {
+ my ($self, $plugin) = @_;
+ # do a basic check if the supplied plugin name is really a plugin
+ return 0 unless $self->plugin_is_loaded($plugin);
+
+ my $skip = $self->qp->connection->notes('_skip_plugins') || {};
+ $skip->{$plugin} = 1;
+ $self->qp->connection->notes('_skip_plugins', $skip);
+ return 1;
+}
+
+=item plugin_enable( $plugin )
+
+B<plugin_enable()> re-enables a (loaded) plugin, it requires the plugin name
+to be escaped by B<escape_plugin()>. It returns "0", if the given plugin
+name is not a loaded plugin. Else it returns "1" after enabling.
+
+=cut
+
+sub plugin_enable {
+ my ($self, $plugin) = @_;
+ return 0 unless $self->plugin_is_loaded($plugin);
+
+ my $skip = $self->qp->connection->notes('_skip_plugins') || {};
+ $skip->{$plugin} = 0;
+ $self->qp->connection->notes('_skip_plugins', $skip);
+ return 1;
+}
+
+=back
+
+=cut
1;
Added: branches/0.3x/plugins/skip_plugins
==============================================================================
--- (empty file)
+++ branches/0.3x/plugins/skip_plugins Sun Dec 31 03:07:32 2006
@@ -0,0 +1,97 @@
+
+=head1 NAME
+
+skip_plugins - don't run selected plugins for some hosts
+
+=head1 DESCRIPTION
+
+The B<skip_plugins> plugin allows you to skip selected plugins for some
+clients. This is similar to some whitelist plugins, without the need to
+modify any plugin.
+
+This plugin should be run before any other plugins hooking to the
+I<hook_connect>. The config allows to run all plugins for one host in a
+subnet and skip some for all other hosts in this network.
+
+=head1 CONFIG
+
+The config file I<skip_plugins> contains lines with two or three items per
+line. The first field is a network/mask pair (or just a single IP address).
+An action is set in the second field: currently B<continue> or B<skip> are
+valid actions.
+
+If a host matches a B<continue> line, the parsing is stopped and all
+plugins are run for this host. A B<skip> action tells qpsmtpd to skip
+the plugins listed in the third field for this connection.
+
+The plugin list in the third field must be separated by "," without any spaces.
+
+=head1 EXAMPLE
+
+ 10.7.7.2 continue
+ 10.7.7.0/24 skip spamassassin,check_earlytalker
+
+To disable a plugin for all clients except for one subnet:
+
+ 10.1.0.0/16 continue
+ 0.0.0.0/0 skip virus/clamdscan
+
+=head1 NOTES
+
+See perldoc Qpsmtpd::Plugin for more about disabling / re-enabling plugins
+for the current connection.
+
+=head1 BUGS
+
+This plugin does not have IPv6 support.
+
+=cut
+
+use Socket;
+
+sub hook_connect {
+ my ($self,$transaction) = @_;
+
+ my %skip = ();
+ #my %l = $self->loaded_plugins;
+ #foreach my $p (keys %l) {
+ # $self->log(LOGDEBUG, "LOADED: $p");
+ #}
+ my $remote = $self->qp->connection->remote_ip;
+ foreach ($self->qp->config("skip_plugins")) {
+ chomp;
+ s/^\s*//;
+ s/\s*$//;
+ my ($ipmask, $action, $plugins) = split /\s+/, $_, 3;
+ next unless defined $action;
+ $action = lc $action;
+ $plugins = "" unless defined $plugins;
+
+ my ($net,$mask) = split '/', $ipmask, 2;
+ if (!defined $mask) {
+ $mask = 32;
+ }
+ $mask = pack "B32", "1"x($mask)."0"x(32-$mask);
+ if (join(".", unpack("C4", inet_aton($remote) & $mask)) eq $net) {
+ if ($action eq 'skip') {
+ foreach my $plugin (split /,/, $plugins) {
+ $self->plugin_disable($self->escape_plugin($plugin))
+ or $self->log(LOGWARN, "tried to disable a plugin "
+ ."which was not loaded: $plugin");
+ }
+ $self->log(LOGDEBUG, "skipping plugins "
+ .join(",", $self->disabled_plugins));
+ }
+ elsif ($action eq 'continue') {
+ $self->log(LOGDEBUG, "ok, doing nothing with the plugins");
+ }
+ else {
+ $self->log(LOGWARN, "unknown action '$action' for $ipmask");
+ }
+ last;
+ }
+ }
+ return (DECLINED);
+}
+
+# vim: sw=4 ts=4 expandtab syn=perl