blob: 3f32ccf1005e432c27d5668d339ea20291daa01e [file] [log] [blame]
#!/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