| #!/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 |