mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-08-24 08:24:36 +00:00
368 lines
12 KiB
Prolog
Executable File
368 lines
12 KiB
Prolog
Executable File
#!/usr/bin/perl -w
|
|
use strict;
|
|
|
|
use Mail::SpamAssassin::Spamd::Config ();
|
|
use Mail::SpamAssassin::Util (); # heavy, loads M::SA
|
|
use Sys::Hostname qw(hostname);
|
|
use File::Spec ();
|
|
use Cwd ();
|
|
|
|
=head1 NAME
|
|
|
|
apache-spamd -- start spamd with Apache as backend
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
apache-spamd --pidfile ... [ OPTIONS ]
|
|
|
|
OPTIONS:
|
|
--httpd_path=path path to httpd, eg. /usr/sbin/httpd.prefork
|
|
--httpd_opt=opt option for httpd (can occur multiple times)
|
|
--httpd_directive=line directive for httpd (can occur multiple times)
|
|
-k CMD passed to httpd (see L<httpd(1)> for values)
|
|
--apxs=path path to apxs, eg /usr/sbin/apxs
|
|
--httpd_conf=path just write a config file for Apache and exit
|
|
|
|
See L<spamd(1)> for other options.
|
|
|
|
If some modules are not in @INC, invoke this way:
|
|
perl -I/path/to/modules apache-spamd.pl \
|
|
--httpd_directive "PerlSwitches -I/path/to/modules"
|
|
|
|
Note: pass the -H / --helper-home-dir option; there is no reasonable default.
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Starts spamd with Apache as a backend. Apache is configured according to
|
|
command line options, compatible to spamd where possible and makes sense.
|
|
|
|
If this script doesn't work for you, complain.
|
|
|
|
=head1 TODO
|
|
|
|
* misc MPMs
|
|
* testing on different platforms and configurations
|
|
* fix FIXME's
|
|
* review XXX's
|
|
* --create-prefs (?), --help, --virtual-config-dir
|
|
* current directory (home_dir_for_helpers?)
|
|
|
|
=cut
|
|
|
|
# NOTE: the amount of code here and list of loaded modules doesn't matter;
|
|
# we exec() anyway.
|
|
|
|
# NOTE: no point in using -T, it'd only mess up code with workarounds;
|
|
# we don't process any user input but command line options.
|
|
|
|
my $opt = Mail::SpamAssassin::Spamd::Config->new(
|
|
{
|
|
defaults => { daemonize => 1, port => 783, },
|
|
moreopts => [
|
|
qw(httpd_path|httpd-path=s httpd_opt|httpd-opt=s@
|
|
httpd_directive|httpd-directive=s@ k:s apxs=s
|
|
httpd_conf|httpd-conf=s)
|
|
],
|
|
}
|
|
);
|
|
|
|
# only standalone spamd implements these options.
|
|
# you miss vpopmail? get a real MTA.
|
|
for my $option (
|
|
qw(round-robin setuid-with-sql setuid-with-ldap socketpath
|
|
socketowner socketgroup socketmode paranoid vpopmail)
|
|
)
|
|
{
|
|
die "ERROR: --$option can't be used with apache-spamd\n"
|
|
if defined $opt->{$option};
|
|
}
|
|
|
|
#
|
|
# XXX: move these options (and sanity checks for them) to M::SA::S::Config?
|
|
#
|
|
|
|
die "ERROR: '$opt->{httpd_path}' does not exist or not executable\n"
|
|
if exists $opt->{httpd_path}
|
|
and !-f $opt->{httpd_path} || !-x _;
|
|
$opt->{httpd_path} ||= 'httpd'; # FIXME: find full path
|
|
|
|
$opt->{pidfile} ||= '/var/run/apache-spamd.pid' # reasonable default
|
|
if -w '/var/run/' && -x _ && !-e '/var/run/apache-spamd.pid';
|
|
die "ERROR: --pidfile is mandatory\n" # this seems ugly, but has advantages
|
|
unless $opt->{pidfile}; # we won't be able to stop otherwise
|
|
$opt->{pidfile} = File::Spec->rel2abs($opt->{pidfile});
|
|
if (-d $opt->{pidfile}) {
|
|
die "ERROR: can't write pid, '$opt->{pidfile}' directory not writable\n"
|
|
unless -x _ && -w _;
|
|
$opt->{pidfile} = File::Spec->catfile($opt->{pidfile}, 'apache-spamd.pid');
|
|
}
|
|
|
|
if (exists $opt->{k}) { # XXX: other option name? or not?
|
|
die "ERROR: can't use -k with --httpd_conf\n" if exists $opt->{httpd_conf};
|
|
## I'm not sure if this toggle idea is a good one...
|
|
## useful for development.
|
|
$opt->{k} ||= -e $opt->{pidfile} ? 'stop' : 'start';
|
|
die "ERROR: -k start|stop|restart|reload|graceful|graceful-stop"
|
|
. " or empty for toggle\n"
|
|
unless $opt->{k} =~ /^(?:start|stop|restart|reload|graceful(?:-stop)?)$/;
|
|
}
|
|
$opt->{k} ||= 'start';
|
|
|
|
if (exists $opt->{httpd_conf}) {
|
|
die "ERROR: --httpd_conf must be a regular file\n"
|
|
if -e $opt->{httpd_conf} && !-f _;
|
|
$opt->{httpd_conf} = File::Spec->rel2abs($opt->{httpd_conf})
|
|
unless $opt->{httpd_conf} eq '-';
|
|
}
|
|
|
|
unless ($opt->{username}) {
|
|
warn "$0: Running as root, huh? Asking for trouble, aren't we?\n" if $< == 0;
|
|
$opt->{username} = getpwuid($>); # weird apache behaviour on 64bit machines if it's missing
|
|
warn "$0: setting User to '$opt->{username}', pass --username to override\n"
|
|
if $opt->{debug} =~ /\b(?:all|info|spamd|prefork|config)\b/;
|
|
}
|
|
|
|
#
|
|
# start processing command line and preparing config / cmd line for Apache
|
|
#
|
|
|
|
my @directives; # -C ... (or write these to a temporary config file)
|
|
my @run = ( # arguments to exec()
|
|
$opt->{httpd_path},
|
|
'-k', $opt->{k},
|
|
'-d', Cwd::cwd(), # XXX: smarter... home_dir_for_helpers?
|
|
);
|
|
|
|
if ($opt->{debug} =~ /\ball\b/) {
|
|
push @run, qw(-e debug);
|
|
push @directives, 'LogLevel debug';
|
|
}
|
|
|
|
push @run, '-X' if !$opt->{daemonize};
|
|
push @run, @{ $opt->{httpd_opts} } if exists $opt->{httpd_opts};
|
|
|
|
push @directives, 'ServerName ' . hostname(),
|
|
qq(PidFile "$opt->{pidfile}"),
|
|
qq(ErrorLog "$opt->{'log-file'}");
|
|
|
|
#
|
|
# only bother with these when we're not stopping
|
|
#
|
|
if ($opt->{k} !~ /stop|graceful/) {
|
|
my $modlist = join ' ', static_apache_modules($opt->{httpd_path});
|
|
|
|
push @directives,
|
|
'LoadModule perl_module ' . apache_module_path('mod_perl.so')
|
|
if $modlist !~ /\bmod.perl\.c\b/i;
|
|
|
|
# StartServers, MaxClients, etc
|
|
my $mpm = lc(
|
|
(
|
|
$modlist =~ /\b(prefork|worker|mpm_winnt|mpmt_os2
|
|
|mpm_netware|beos|event|metuxmpm|peruser)\.c\b/ix
|
|
)[0]
|
|
);
|
|
die "ERROR: unable to figure out which MPM is in use\n" unless $mpm;
|
|
push @directives, mpm_specific_config($mpm);
|
|
|
|
# directives from command line; might require mod_perl.so, so let's
|
|
# ignore these unless we're starting -- shouldn't be critical anyway
|
|
push @directives, @{ $opt->{httpd_directive} }
|
|
if exists $opt->{httpd_directive};
|
|
|
|
push @directives, "TimeOut $opt->{'timeout-tcp'}" if $opt->{'timeout-tcp'};
|
|
|
|
# Listen
|
|
push @directives, defined $opt->{'listen-ip'}
|
|
&& @{ $opt->{'listen-ip'} }
|
|
? map({ 'Listen ' . ($_ =~ /:/ ? "[$_]" : $_) . ":$opt->{port}" }
|
|
@{ $opt->{'listen-ip'} })
|
|
: "Listen $opt->{port}";
|
|
|
|
if ($opt->{ssl}) {
|
|
push @directives,
|
|
'LoadModule ssl_module ' . apache_module_path('mod_ssl.so')
|
|
if $modlist !~ /\bmod.ssl\.c\b/i; # XXX: are there other variants?
|
|
push @directives, qq(SSLCertificateFile "$opt->{'server-cert'}")
|
|
if exists $opt->{'server-cert'};
|
|
push @directives, qq(SSLCertificateKeyFile "$opt->{'server-key'}")
|
|
if exists $opt->{'server-key'};
|
|
push @directives, 'SSLEngine on';
|
|
my $random = -r '/dev/urandom' ? 'file:/dev/urandom 256' : 'builtin';
|
|
push @directives, "SSLRandomSeed startup $random",
|
|
"SSLRandomSeed connect $random";
|
|
##push @directives, 'SSLProtocol all -SSLv2'; # or v3 only?
|
|
}
|
|
|
|
# XXX: available in Apache 2.1+; previously in core (AFAIK);
|
|
# should we parse httpd -v?
|
|
push @directives,
|
|
'LoadModule ident_module ' . apache_module_path('mod_ident.so'),
|
|
'IdentityCheck on'
|
|
if $opt->{'auth-ident'};
|
|
push @directives, "IdentityCheckTimeout $opt->{'ident-timeout'}"
|
|
if $opt->{'auth-ident'} && defined $opt->{'ident-timeout'};
|
|
|
|
# SA stuff
|
|
push @directives,
|
|
'PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config',
|
|
'SAenabled on';
|
|
push @directives, "SAAllow from @{$opt->{'allowed-ips'}}"
|
|
if exists $opt->{'allowed-ips'};
|
|
push @directives, 'SAtell on' if $opt->{'allow-tell'};
|
|
push @directives, "SAtimeout $opt->{'timeout-child'}"
|
|
if exists $opt->{'timeout-child'};
|
|
push @directives, "SAdebug $opt->{debug}" if $opt->{debug};
|
|
push @directives, 'SAident on'
|
|
if $opt->{'auth-ident'};
|
|
|
|
push @directives, qq(SANew rules_filename "$opt->{configpath}")
|
|
if defined $opt->{configpath};
|
|
push @directives, qq(SANew site_rules_filename "$opt->{siteconfigpath}")
|
|
if defined $opt->{siteconfigpath};
|
|
push @directives,
|
|
qq(SANew home_dir_for_helpers "$opt->{home_dir_for_helpers}")
|
|
if defined $opt->{home_dir_for_helpers};
|
|
push @directives, qq(SANew local_tests_only $opt->{local})
|
|
if defined $opt->{local};
|
|
push @directives, map qq(SANew $_ "$opt->{$_}"), grep defined $opt->{$_},
|
|
qw(PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR);
|
|
push @directives, 'SANew paranoid 1' if $opt->{paranoid};
|
|
push @directives, qq(SAConfigLine "$_") for @{ $opt->{cf} };
|
|
|
|
my @users;
|
|
push @users, 'local' if $opt->{'user-config'};
|
|
push @users, 'sql' if $opt->{'sql-config'};
|
|
push @users, 'ldap' if $opt->{'ldap-config'};
|
|
push @directives, join ' ', 'SAUsers', @users if @users;
|
|
}
|
|
|
|
# write directives to conf file (or STDOUT) and exit
|
|
if ($opt->{httpd_conf}) {
|
|
my $fh;
|
|
if ($opt->{httpd_conf} eq '-') {
|
|
open $fh, '>&STDOUT' or die "open >&STDOUT: $!";
|
|
}
|
|
else {
|
|
open $fh, '>', $opt->{httpd_conf}
|
|
or die "open >'$opt->{httpd_conf}': $!";
|
|
}
|
|
print $fh join "\n",
|
|
"# generated by $0 on " . localtime(time),
|
|
@directives,
|
|
"# vim: filetype=apache\n";
|
|
close $fh or warn "close: $!";
|
|
exit 0; # user is supposed to run Apache himself
|
|
}
|
|
|
|
#
|
|
# add directives to command line and run Apache
|
|
#
|
|
|
|
push @run, '-f',
|
|
File::Spec->devnull(), # XXX: will work on a non-POSIX platform?
|
|
map { ; '-C' => $_ } @directives;
|
|
|
|
warn map({ /^-/ ? "\n $_" : " $_" } @run), "\n"
|
|
if $opt->{debug} =~ /\ball|spamd|config|info\b/;
|
|
|
|
undef $opt; # there is no DESTROY... but could be one ;-)
|
|
exec @run; # we are done
|
|
|
|
#
|
|
# helper functions
|
|
#
|
|
|
|
sub get_libexecdir {
|
|
get_libexecdir_A2BC() || get_libexecdir_apxs();
|
|
}
|
|
|
|
# read it from Apache2::BuildConfig
|
|
sub get_libexecdir_A2BC {
|
|
$INC{'Apache2/Build.pm'}++; # hack... needlessly required by BuildConfig
|
|
require Apache2::BuildConfig;
|
|
my $cfg = Apache2::BuildConfig->new;
|
|
$cfg->{APXS_LIBEXECDIR} || $cfg->{MODPERL_APXS_LIBEXECDIR};
|
|
}
|
|
|
|
# `apxs -q LIBEXECDIR`
|
|
sub get_libexecdir_apxs {
|
|
my @cmd = (($opt->{apxs} || 'apxs'), '-q', 'LIBEXECDIR');
|
|
chomp(my $modpath = get_cmd_output(@cmd));
|
|
die "ERROR: failed to obtain module path from '@cmd'\n"
|
|
unless length $modpath;
|
|
die "ERROR: '$modpath' returned by '@cmd' is not an existing directory\n"
|
|
unless -d $modpath;
|
|
$modpath;
|
|
}
|
|
|
|
# as above, cached version
|
|
our $apache_module_path;
|
|
sub apache_module_path {
|
|
my $modname = shift;
|
|
$apache_module_path ||= get_libexecdir(); # path is cached
|
|
my $module = File::Spec->catfile($apache_module_path, $modname);
|
|
die "ERROR: '$module' does not exist\n" if !-e $module;
|
|
$module;
|
|
}
|
|
|
|
# httpd -l
|
|
# XXX: can MPM be a DSO?
|
|
sub static_apache_modules {
|
|
my $httpd = shift;
|
|
my @cmd = ($httpd, '-l');
|
|
my $out = get_cmd_output(@cmd);
|
|
my @modlist = $out =~ /\b(\S+\.c)\b/gi;
|
|
die "ERROR: failed to get list of static modules from '@cmd'\n"
|
|
unless @modlist;
|
|
@modlist;
|
|
}
|
|
|
|
sub get_cmd_output {
|
|
my @cmd = @_;
|
|
my $output = `@cmd` or die "ERROR: failed to run '@cmd': $!\n";
|
|
$output;
|
|
}
|
|
|
|
sub mpm_specific_config {
|
|
my $mpm = shift;
|
|
my @ret;
|
|
|
|
if ($mpm =~ /^prefork|worker|beos|mpmt_os2$/) {
|
|
push @ret, "User $opt->{username}" if $opt->{username};
|
|
push @ret, "Group $opt->{groupname}" if $opt->{groupname};
|
|
}
|
|
elsif ($opt->{username} || $opt->{groupname}) {
|
|
die "ERROR: username / groupname not supported with MPM $mpm\n";
|
|
}
|
|
|
|
if ($mpm eq 'prefork') {
|
|
push @ret, "StartServers $opt->{'min-spare'}";
|
|
push @ret, "MinSpareServers $opt->{'min-spare'}";
|
|
push @ret, "MaxSpareServers $opt->{'max-spare'}";
|
|
push @ret, "MaxClients $opt->{'max-children'}";
|
|
}
|
|
elsif ($mpm eq 'worker') { # XXX: we could be smarter here
|
|
push @ret, grep length, map { s/^\s+//; s/\s*\b#.*$//; $_ } split /\n/,
|
|
<<" EOF";
|
|
StartServers 1
|
|
ServerLimit 1
|
|
MinSpareThreads $opt->{'min-spare'}
|
|
MaxSpareThreads $opt->{'max-spare'}
|
|
ThreadLimit $opt->{'max-children'}
|
|
ThreadsPerChild $opt->{'max-children'}
|
|
EOF
|
|
}
|
|
else {
|
|
warn "WARNING: MPM $mpm not supported, using defaults for performance settings\n";
|
|
warn "WARNING: prepare for huge memory usage and maybe an emergency reboot\n";
|
|
}
|
|
|
|
push @ret, "MaxRequestsPerChild $opt->{'max-conn-per-child'}"
|
|
if defined $opt->{'max-conn-per-child'};
|
|
|
|
@ret;
|
|
}
|
|
|
|
# vim: ts=4 sw=4 noet
|