package Mail::SpamAssassin::Spamd::Apache2::Config; use strict; use Apache2::ServerUtil (); my $server = Apache2::ServerUtil->server() or die 'serverutil->server'; $server->push_handlers( PerlPostConfigHandler => [\&post_config, \&add_version_string,],); =head1 NAME Mail::SpamAssassin::Spamd::Apache2::Config -- configure Apache with SpamAssassin =head1 SYNOPSIS LoadModule perl_module modules/mod_perl.so PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config SAEnabled On # default off SAAllow from 127.0.0.1 192.168.0.0/24 ::1 SAIdent Off SATell Off SATimeout 300 # reasonable: around 30s SADebug info SAMsgSizeLimit 512000 =head1 DESCRIPTION Provides Apache config directives for configuring spamd. Initializes the C object. Note, that the defaults here apply to *this* code; C sets different ones to be compatible with C. =head1 DIRECTIVES =over =cut use Apache2::Module (); use Apache2::CmdParms (); use Apache2::Const -compile => qw(OK RSRC_CONF ITERATE ITERATE2 FLAG TAKE1 TAKE2 :log SERVER_ERROR); { my @directives; =item C Enables / disables SA for given vhost. Adds two handlers: SetHandler modperl PerlProcessConnectionHandler Mail::SpamAssassin::Spamd::Apache2 PerlPreConnectionHandler Mail::SpamAssassin::Spamd::Apache2::AclIP Defaults to Off. =cut push @directives, { # not inherited name => 'SAEnabled', args_how => Apache2::Const::FLAG, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAEnable { On | Off }', }; =item C Similar to C directive from C. Spamd's C<--allowed-ips> arguments should go here. Default is empty, meaning access is denied. =cut push @directives, { # inherited name => 'SAAllow', args_how => Apache2::Const::ITERATE2, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAAllow from 127.0.0.1 192.168/16 ::1 ...', }; =item C Enables RFC 1413 (ident) checks incoming connections. Note, that checking if a *remote* login matches a *local* one is usually pointless. See C for more details. Adds a handler: PerlPreConnectionHandler Mail::SpamAssassin::Spamd::Apache2::AclRFC1413 Requires C in current configuration scope. This directive is provided by the C module, separated from core in Apache 2.1. Default off. =cut push @directives, { # inherited name => 'SAIdent', args_how => Apache2::Const::FLAG, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAIdent { On | Off }', }; =item C Allow clients to issue the C command. Default off. =cut push @directives, { # inherited name => 'SATell', args_how => Apache2::Const::FLAG, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SATell { On | Off }', }; =item C Timeout for SpamAssassin checks. 25 seconds is a reasonable value. Default C<0> (unlimited). =cut push @directives, { # inherited name => 'SATimeout', args_how => Apache2::Const::TAKE1, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SATimeout 300 # unit: seconds', }; =item C Debug level for SpamAssassin. =cut push @directives, { # inherited name => 'SADebug', args_how => Apache2::Const::TAKE1, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SADebug { debug_level | 0 }', }; =item C Maximum message size which will be processed. You're strongly encouraged to set this value. Unit: bytes. =cut push @directives, { # inherited name => 'SAMsgSizeLimit', args_how => Apache2::Const::TAKE1, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAMsgSizeLimit limit_in_bytes', }; =item C Additional arguments to Cnew()>. Refer to C. =cut push @directives, { name => 'SANew', args_how => Apache2::Const::TAKE2, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SANew rules_filename "/some/path"', }; =item C Databases which should be checked for user information. Will be checked in the order specified. Default C. =cut push @directives, { # inherited name => 'SAUsers', args_how => Apache2::Const::ITERATE, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAUsers { none | local | sql | ldap }', }; =item C Value of the LANG environment variable SpamAssassin should run with. Default C, unless you set Apache otherwise somehow. =cut push @directives, { # inherited name => 'SALocale', args_how => Apache2::Const::TAKE1, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SALocale xx_XX', }; =item C Equivalent of the C<--cf> option for spamassassin / spamd / sa-learn. =cut push @directives, { # inherited name => 'SAConfigLine', args_how => Apache2::Const::TAKE1, req_override => Apache2::Const::RSRC_CONF, errmsg => 'SAConfigLine "body NEWRULE /text/"', }; Apache2::Module::add(__PACKAGE__, \@directives); } =back =cut # executed whenever directive is seen sub SAEnabled { my ($self, $parms, $arg) = @_; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{saenabled} = $arg; } sub SAAllow { # can't use mod_authz_host; it is HTTP-centric my ($self, $parms, $key, $val) = @_; die 'usage: SAAllow from ... ... ...' unless $key eq 'from'; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); push @{ $srv_cfg->{allowed_ips} }, $val; } sub SAIdent { my ($self, $parms, $arg) = @_; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{auth_ident} = $arg; } sub SATell { my ($self, $parms, $arg) = @_; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{allow_tell} = $arg; } sub SATimeout { my ($self, $parms, $arg) = @_; die "SATimeout accepts *seconds*\n" if $arg !~ /^\d+$/; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{satimeout} = $arg; } sub SADebug { my ($self, $parms, $arg) = @_; die "SADebug can't be used in vhost, see bug #4963\n" if $parms->server->is_virtual; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{sa_debug} = $arg; } sub SAMsgSizeLimit { my ($self, $parms, $arg) = @_; die "MsgSizeLimit accepts *number*\n" if $arg !~ /^\d+$/; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{msg_size_limit} = $arg; } sub SANew { my ($self, $parms, $key, $val) = @_; die "SANew can't be used in vhost, see bug #4963\n" if $parms->server->is_virtual; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{sa_args_to_new}->{$key} = $val; } sub SAUsers { my ($self, $parms, $arg) = @_; $arg = lc $arg; die "SAUsers: bad value\n" unless $arg =~ /^(?:none|local|sql|ldap)$/; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); push @{ $srv_cfg->{sa_users} }, $arg; } sub SALocale { my ($self, $parms, $arg) = @_; die "SALocale can't be used in vhost, see bug #4963\n" if $parms->server->is_virtual; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{sa_locale} = $arg; } sub SAConfigLine { my ($self, $parms, $arg) = @_; my $srv_cfg = Apache2::Module::get_config($self, $parms->server); $srv_cfg->{post_config_text} .= ( $srv_cfg->{post_config_text} ? "\n" : '' ) . $arg; } # executed after (XXX: not before?) SA* for every server (vhost or main) sub SERVER_CREATE { my ($class, $parms) = @_; bless { saenabled => 0, satimeout => 300, }, #msg_size_limit => 500*1024, }, $class; } # executed for every vhost, after processing SAOptions and SERVER_CREATE sub SERVER_MERGE { my ($base, $add) = @_; my $new = { saenabled => $add->{saenabled}, }; # SAallow in vhost completely overrides SAAllow in base, otherwise # inherit; maybe not very intuitive, but will do until better idea $new->{allowed_ips} = exists $add->{allowed_ips} ? [@{ $add->{allowed_ips} }] : exists $base->{allowed_ips} ? [@{ $base->{allowed_ips} }] : [warn('warning: access denied for everyone in vhost') && ()]; for my $opt ( qw(auth_ident ident_timeout allow_tell sa_debug sa_args_to_new sa_users sa_locale) ) { $new->{$opt} = exists $add->{$opt} ? $add->{$opt} : exists $base->{$opt} ? $base->{$opt} : 0; } $new->{satimeout} = exists $add->{satimeout} ? $add->{satimeout} : exists $base->{satimeout} ? $base->{satimeout} : die 'should not happen'; bless $new, ref $base; } use APR::Const -compile => qw(:error SUCCESS); use Apache2::ServerRec (); # $s->is_virtual use Apache2::Log (); use File::Temp (); # tempdir use File::Path (); # rmpath # PerlPostConfigHandler sub post_config { my ($conf_pool, $log_pool, $temp_pool, $serv) = @_; my ($num_vhosts, $num_configured); my $hackish_tmp_ref; for (my $s = $serv; $s; $s = $s->next) { die "\$num_vhosts>5000; loop?" if ++$num_vhosts > 1000; my $srv_cfg = Apache2::Module::get_config(__PACKAGE__, $s) || ''; # hack: if default server is configured with SAEnabled On, and a vhost # is not, the vhost inherits On value. I don't know how to prevent it # other way... check $hackish_tmp_ref use later. --radek $hackish_tmp_ref = $srv_cfg unless $s->is_virtual; # is SA enabled for this vhost? if (!$srv_cfg->{saenabled} or $s->is_virtual && $srv_cfg eq $hackish_tmp_ref) { my $msg = 'SAEnabled off for ' . _vhost_id($s); # it inherits handler too $msg .= ' and on in default server; it probably won\'t work as' . ' you intend it to -- either Apache or this code is broken' if $hackish_tmp_ref->{saenabled} && $srv_cfg && !$srv_cfg->{saenabled}; $s->log_serror(Apache2::Log::LOG_MARK(), Apache2::Const::LOG_DEBUG | Apache2::Const::LOG_STARTUP, APR::Const::SUCCESS, $msg); next; } # check options if (ref $srv_cfg->{sa_users} && grep { $_ eq 'none' } @{ $srv_cfg->{sa_users} }) { if (@{ $srv_cfg->{sa_users} } > 1) { die "if you add 'none' to SAUsers, it's pointless to add anything else\n"; } else { delete $srv_cfg->{sa_users}; } } # create list of allowed networks use APR::IpSubnet (); for my $net (@{ $srv_cfg->{allowed_ips} }) { my $ais = APR::IpSubnet->new($conf_pool, split m#/#, $net, 2) or die "APR::IpSubnet->new($net) failed"; push @{ $srv_cfg->{allowed_networks} }, $ais; } my @cfg = ( 'SetHandler modperl', 'PerlProcessConnectionHandler Mail::SpamAssassin::Spamd::Apache2', 'PerlPreConnectionHandler Mail::SpamAssassin::Spamd::Apache2::AclIP', ); require Mail::SpamAssassin::Spamd::Apache2; require Mail::SpamAssassin::Spamd::Apache2::AclIP; if ($srv_cfg->{auth_ident}) { require Mail::SpamAssassin::Spamd::Apache2::AclRFC1413; push @cfg, 'PerlPreConnectionHandler ' . 'Mail::SpamAssassin::Spamd::Apache2::AclRFC1413'; } $s->add_config(\@cfg); if (!$Mail::SpamAssassin::Spamd::Apache2::spamtest) { require Mail::SpamAssassin; local $/ = $/; # Razor resets this # Is there a way to toggle these settings in handler? See bug #4963. # Problem: if other vhost defined eg. SALocal, it would be silently # ignored, as this block executes only for the first SAEnabled seen. # Workaround: forcing these settings to be unavailable in vhosts # until the bug is resolved. my $sa = Mail::SpamAssassin->new({ ## dont_copy_prefs => $dontcopy, ## rules_filename => ($srv_cfg->{configpath} || 0), ## site_rules_filename => ($srv_cfg->{siteconfigpath} || 0), debug => ($srv_cfg->{sa_debug} || 0), ## paranoid => ($srv_cfg->{paranoid} || 0), ## PREFIX => $PREFIX, ## DEF_RULES_DIR => $DEF_RULES_DIR, ## LOCAL_RULES_DIR => $LOCAL_RULES_DIR, ## LOCAL_STATE_DIR => $LOCAL_STATE_DIR, ($srv_cfg->{sa_args_to_new} ? %{ $srv_cfg->{sa_args_to_new} } : ()), }) or die 'Mail::SpamAssassin->new() failed'; # initialize SA configuration my $tmphome = File::Temp::tempdir() or die "creating temp directory failed: $!"; my $tmpsadir = File::Spec->catdir($tmphome, '.spamassassin'); mkdir $tmpsadir, 0700 or die "spamd: cannot create $tmpsadir: $!"; $ENV{HOME} = $tmphome; $sa->compile_now(0, 1); delete $ENV{HOME}; system("rm -rf '$tmphome'"); $Mail::SpamAssassin::Spamd::Apache2::spamtest = $sa; Mail::SpamAssassin::Spamd::backup_config($sa); } $num_configured++; $s->log_serror(Apache2::Log::LOG_MARK(), Apache2::Const::LOG_DEBUG | Apache2::Const::LOG_STARTUP, APR::Const::SUCCESS, 'spamd handler configured for ', _vhost_id($s) ); } if (!$num_configured) { $serv->log_serror(Apache2::Log::LOG_MARK(), Apache2::Const::LOG_NOTICE | Apache2::Const::LOG_STARTUP, APR::Const::EGENERAL, 'no spamd handlers configured'); } return Apache2::Const::OK; } sub _vhost_id { my $s = shift; # ServerRec $s->is_virtual() ? 'vhost ' . $s->server_hostname() . ':' . $s->port() : 'default server'; } # PerlPostConfigHandler sub add_version_string { my ($conf_pool, $log_pool, $temp_pool, $serv) = @_; my $version = Mail::SpamAssassin->VERSION || '?'; $serv->add_version_component("SpamAssassin/$version"); return Apache2::Const::OK; } =head1 EXAMPLES You'll need some basic Apache directives in each configuration; that should be obvious. PidFile "/var/run/apache-spamd.pid" ServerName localhost TimeOut 30 StartServers 1 MinSpareServers 1 MaxSpareServers 2 MaxClients 5 MaxRequestsPerChild 200 If the Mail::SpamAssassin::* perl modules are installed somewhere outside of C<@INC>, you can use something like: PerlSwitches -I/home/users/someuser/lib =head2 simple Listen 127.0.0.1:30783 LoadModule perl_module /usr/lib/apache/mod_perl.so PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config SAenabled on SAAllow from 127.0.0.1 SAtimeout 25 SAdebug info SANew DEF_RULES_DIR "/usr/share/spamassassin" SANew LOCAL_RULES_DIR "/etc/mail/spamassassin" SANew LOCAL_STATE_DIR "/var/lib" SAUsers local sql =head2 vhosts with different config Listen 127.0.0.1:30783 Listen 30784 LoadModule perl_module /usr/lib/apache/mod_perl.so PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config SAenabled off SAtimeout 25 SAdebug info SANew DEF_RULES_DIR "/usr/share/spamassassin" SANew LOCAL_RULES_DIR "/etc/mail/spamassassin" SANew LOCAL_STATE_DIR "/var/lib" LoadModule ident_module /usr/lib/apache/mod_ident.so # local, ident-authenticated users only; search in /etc/passwd, # if that fails, try SQL IdentityCheck on IdentityCheckTimeout 4 SAenabled on SAident on SAAllow from 127.0.0.1 SAUsers local sql # serve for whole LAN, but don't read user configuration SAenabled on SAtimeout 30 SAAllow from 127.0.0.1 192.168.0.0/24 SAUsers none =head1 BUGS See Ehttp://bugzilla.spamassassin.org/E. =head1 SEE ALSO C, C, C, C, C, C =cut 1; # vim: ts=2 sw=2 et