On Tue, 31 Oct 2006 19:56:11 -0800
Ask Bjørn Hansen <[EMAIL PROTECTED]> wrote:
> On Oct 31, 2006, at 6:38 PM, John Peacock wrote:
>
> > The issue is that this plugin requires a patch to the core
> > to actually implement the skip part, which might be fine, but there
> > might also
> > be a more elegant way to handle this that I can't see right now.
>
> I'm not sure what it would look like; but it'd be nice to have an API
> for querying and changing the list of configured plugins. The API
> should let you change them for the connection (changing them globally
> would be hard or impossible, so that's less important).
This diff now does that, see perldoc lib/Qpsmtpd/PlugOut.pm for what you
can do :)
The skip_plugins plugin now uses this new API.
Oh, even if it was done for r669, it can be applied to r672 without
problems
Hanno
diff -Nurx .svn -x config ../0.3x/config.sample/plugins ./config.sample/plugins
--- ../0.3x/config.sample/plugins 2006-04-07 22:21:23.000000000 +0200
+++ ./config.sample/plugins 2006-11-04 17:36:09.000000000 +0100
@@ -12,6 +12,9 @@
# from one IP!
hosts_allow
+# skip selected plugins for some clients:
+skip_plugins
+
# enable to accept MAIL FROM:/RCPT TO: addresses without surrounding <>
dont_require_anglebrackets
diff -Nurx .svn -x config ../0.3x/lib/Qpsmtpd/PlugOut.pm
./lib/Qpsmtpd/PlugOut.pm
--- ../0.3x/lib/Qpsmtpd/PlugOut.pm 1970-01-01 01:00:00.000000000 +0100
+++ ./lib/Qpsmtpd/PlugOut.pm 2006-11-05 12:11:41.000000000 +0100
@@ -0,0 +1,177 @@
+#
+# PlugOut - disable or re-enable loaded plugins
+#
+package Qpsmtpd::PlugOut;
+
+=head1 NAME
+
+Qpsmtpd::PlugOut - disable or re-enable loaded plugins for a connection
+
+=head1 DESCRIPTION
+
+The B<Qpsmtpd::PlugOut> module lets you disable (or reenable) plugins
+for the current connection. The functions are inside the plugins name space
+and have to be called like
+ $self->loaded_plugins;
+
+=head1 API
+
+=over 6
+
+=item loaded_plugins( )
+
+This returns a hash. Keys are (escaped, see below) plugin names. The value
+tells you if the plugin is 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 @loaded = $self->_loaded("Qpsmtpd::Plugin");
+ foreach (@loaded) {
+ s/^Qpsmtpd::Plugin:://;
+ }
+ my %plugins = map { ($_, 1) } @loaded;
+ 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
+ local (*symbols) = *{"${base}::"};
+ foreach my $name (values %symbols) {
+ # $name is read only while walking the stash
+
+ # not a class name? ok, next
+ ($symbol = $name) =~ s/^\*(.*)::$/$1/ || next;
+
+ # 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).
+ # ... this restricts qpsmtpd: No module named
+ # Qpsmtpd::Plugin::Something must exist, or it will be returned
+ # as a loaded plugin...
+ @sub = $self->_loaded($symbol);
+ push @loaded, @sub ? @sub : $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;
+}
+
+=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) = @_;
+ # each plugin has a sub called "plugin_name()";
+ return defined &{"Qpsmtpd::Plugin::${plugin}::plugin_name"};
+}
+
+=back
+
+=cut
+
+1;
+# the end :)
diff -Nurx .svn -x config ../0.3x/lib/Qpsmtpd/Plugin.pm ./lib/Qpsmtpd/Plugin.pm
--- ../0.3x/lib/Qpsmtpd/Plugin.pm 2006-07-22 19:45:37.000000000 +0200
+++ ./lib/Qpsmtpd/Plugin.pm 2006-11-05 09:52:47.000000000 +0100
@@ -153,9 +153,10 @@
"package $package;",
'use Qpsmtpd::Constants;',
"require Qpsmtpd::Plugin;",
+ "require Qpsmtpd::PlugOut;",
'use vars qw(@ISA);',
'use strict;',
- '@ISA = qw(Qpsmtpd::Plugin);',
+ '@ISA = qw(Qpsmtpd::Plugin Qpsmtpd::PlugOut);',
($test_mode ? 'use Test::More;' : ''),
"sub plugin_name { qq[$plugin] }",
$line,
diff -Nurx .svn -x config ../0.3x/lib/Qpsmtpd.pm ./lib/Qpsmtpd.pm
--- ../0.3x/lib/Qpsmtpd.pm 2006-07-22 19:45:37.000000000 +0200
+++ ./lib/Qpsmtpd.pm 2006-11-02 22:16:20.000000000 +0100
@@ -327,6 +327,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(LOGINFO, $hook, $code->{name});
eval { (@r) = $code->{code}->($self, $self->transaction, @_); };
$@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;
diff -Nurx .svn -x config ../0.3x/plugins/skip_plugins ./plugins/skip_plugins
--- ../0.3x/plugins/skip_plugins 1970-01-01 01:00:00.000000000 +0100
+++ ./plugins/skip_plugins 2006-11-05 11:49:50.000000000 +0100
@@ -0,0 +1,90 @@
+
+=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::PlugOut for more about disabling / reenabling plugins
+
+=cut
+
+use Socket;
+
+sub hook_connect {
+ my ($self,$transaction) = @_;
+
+ my %skip = ();
+ my $remote = $self->qp->connection->remote_ip;
+ foreach ($self->qp->config("skip_plugins")) {
+ chomp;
+ s/^\s*//;
+ my ($ipmask, $action, $plugins) = split /\s+/, $_, 3;
+ next unless defined $action;
+ $action = lc $action;
+ unless (defined $plugins) {
+ $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(LOGDEBUG, "unknown action '$action' for $ipmask");
+ }
+ last;
+ }
+ }
+ return (DECLINED);
+}
+
+# vim: sw=4 ts=4 expandtab syn=perl