#!/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 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 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" =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 '-'; } # # 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} eq 'all') { 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} eq 'all'; warn "$0: Running as root, huh? Asking for trouble, aren't we?\n" if $< == 0 && !$opt->{username}; 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 use vars '$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