1#!/usr/bin/perl -w 2use strict; 3 4use Mail::SpamAssassin::Spamd::Config (); 5use Mail::SpamAssassin::Util (); # heavy, loads M::SA 6use Sys::Hostname qw(hostname); 7use File::Spec (); 8use Cwd (); 9 10=head1 NAME 11 12apache-spamd -- start spamd with Apache as backend 13 14=head1 SYNOPSIS 15 16 apache-spamd --pidfile ... [ OPTIONS ] 17 18OPTIONS: 19 --httpd_path=path path to httpd, eg. /usr/sbin/httpd.prefork 20 --httpd_opt=opt option for httpd (can occur multiple times) 21 --httpd_directive=line directive for httpd (can occur multiple times) 22 -k CMD passed to httpd (see L<httpd(1)> for values) 23 --apxs=path path to apxs, eg /usr/sbin/apxs 24 --httpd_conf=path just write a config file for Apache and exit 25 26See L<spamd(1)> for other options. 27 28If some modules are not in @INC, invoke this way: 29 perl -I/path/to/modules apache-spamd.pl \ 30 --httpd_directive "PerlSwitches -I/path/to/modules" 31 32Note: pass the -H / --helper-home-dir option; there is no reasonable default. 33 34=head1 DESCRIPTION 35 36Starts spamd with Apache as a backend. Apache is configured according to 37command line options, compatible to spamd where possible and makes sense. 38 39If this script doesn't work for you, complain. 40 41=head1 TODO 42 43 * misc MPMs 44 * testing on different platforms and configurations 45 * fix FIXME's 46 * review XXX's 47 * --create-prefs (?), --help, --virtual-config-dir 48 * current directory (home_dir_for_helpers?) 49 50=cut 51 52# NOTE: the amount of code here and list of loaded modules doesn't matter; 53# we exec() anyway. 54 55# NOTE: no point in using -T, it'd only mess up code with workarounds; 56# we don't process any user input but command line options. 57 58my $opt = Mail::SpamAssassin::Spamd::Config->new( 59 { 60 defaults => { daemonize => 1, port => 783, }, 61 moreopts => [ 62 qw(httpd_path|httpd-path=s httpd_opt|httpd-opt=s@ 63 httpd_directive|httpd-directive=s@ k:s apxs=s 64 httpd_conf|httpd-conf=s) 65 ], 66 } 67); 68 69# only standalone spamd implements these options. 70# you miss vpopmail? get a real MTA. 71for my $option ( 72 qw(round-robin setuid-with-sql setuid-with-ldap socketpath 73 socketowner socketgroup socketmode paranoid vpopmail) 74 ) 75{ 76 die "ERROR: --$option can't be used with apache-spamd\n" 77 if defined $opt->{$option}; 78} 79 80# 81# XXX: move these options (and sanity checks for them) to M::SA::S::Config? 82# 83 84die "ERROR: '$opt->{httpd_path}' does not exist or not executable\n" 85 if exists $opt->{httpd_path} 86 and !-f $opt->{httpd_path} || !-x _; 87$opt->{httpd_path} ||= 'httpd'; # FIXME: find full path 88 89$opt->{pidfile} ||= '/var/run/apache-spamd.pid' # reasonable default 90 if -w '/var/run/' && -x _ && !-e '/var/run/apache-spamd.pid'; 91die "ERROR: --pidfile is mandatory\n" # this seems ugly, but has advantages 92 unless $opt->{pidfile}; # we won't be able to stop otherwise 93$opt->{pidfile} = File::Spec->rel2abs($opt->{pidfile}); 94if (-d $opt->{pidfile}) { 95 die "ERROR: can't write pid, '$opt->{pidfile}' directory not writable\n" 96 unless -x _ && -w _; 97 $opt->{pidfile} = File::Spec->catfile($opt->{pidfile}, 'apache-spamd.pid'); 98} 99 100if (exists $opt->{k}) { # XXX: other option name? or not? 101 die "ERROR: can't use -k with --httpd_conf\n" if exists $opt->{httpd_conf}; 102 ## I'm not sure if this toggle idea is a good one... 103 ## useful for development. 104 $opt->{k} ||= -e $opt->{pidfile} ? 'stop' : 'start'; 105 die "ERROR: -k start|stop|restart|reload|graceful|graceful-stop" 106 . " or empty for toggle\n" 107 unless $opt->{k} =~ /^(?:start|stop|restart|reload|graceful(?:-stop)?)$/; 108} 109$opt->{k} ||= 'start'; 110 111if (exists $opt->{httpd_conf}) { 112 die "ERROR: --httpd_conf must be a regular file\n" 113 if -e $opt->{httpd_conf} && !-f _; 114 $opt->{httpd_conf} = File::Spec->rel2abs($opt->{httpd_conf}) 115 unless $opt->{httpd_conf} eq '-'; 116} 117 118unless ($opt->{username}) { 119 warn "$0: Running as root, huh? Asking for trouble, aren't we?\n" if $< == 0; 120 $opt->{username} = getpwuid($>); # weird apache behaviour on 64bit machines if it's missing 121 warn "$0: setting User to '$opt->{username}', pass --username to override\n" 122 if $opt->{debug} =~ /\b(?:all|info|spamd|prefork|config)\b/; 123} 124 125# 126# start processing command line and preparing config / cmd line for Apache 127# 128 129my @directives; # -C ... (or write these to a temporary config file) 130my @run = ( # arguments to exec() 131 $opt->{httpd_path}, 132 '-k', $opt->{k}, 133 '-d', Cwd::cwd(), # XXX: smarter... home_dir_for_helpers? 134); 135 136if ($opt->{debug} =~ /\ball\b/) { 137 push @run, qw(-e debug); 138 push @directives, 'LogLevel debug'; 139} 140 141push @run, '-X' if !$opt->{daemonize}; 142push @run, @{ $opt->{httpd_opts} } if exists $opt->{httpd_opts}; 143 144push @directives, 'ServerName ' . hostname(), 145 qq(PidFile "$opt->{pidfile}"), 146 qq(ErrorLog "$opt->{'log-file'}"); 147 148# 149# only bother with these when we're not stopping 150# 151if ($opt->{k} !~ /stop|graceful/) { 152 my $modlist = join ' ', static_apache_modules($opt->{httpd_path}); 153 154 push @directives, 155 'LoadModule perl_module ' . apache_module_path('mod_perl.so') 156 if $modlist !~ /\bmod.perl\.c\b/i; 157 158 # StartServers, MaxClients, etc 159 my $mpm = lc( 160 ( 161 $modlist =~ /\b(prefork|worker|mpm_winnt|mpmt_os2 162 |mpm_netware|beos|event|metuxmpm|peruser)\.c\b/ix 163 )[0] 164 ); 165 die "ERROR: unable to figure out which MPM is in use\n" unless $mpm; 166 push @directives, mpm_specific_config($mpm); 167 168 # directives from command line; might require mod_perl.so, so let's 169 # ignore these unless we're starting -- shouldn't be critical anyway 170 push @directives, @{ $opt->{httpd_directive} } 171 if exists $opt->{httpd_directive}; 172 173 push @directives, "TimeOut $opt->{'timeout-tcp'}" if $opt->{'timeout-tcp'}; 174 175 # Listen 176 push @directives, defined $opt->{'listen-ip'} 177 && @{ $opt->{'listen-ip'} } 178 ? map({ 'Listen ' . ($_ =~ /:/ ? "[$_]" : $_) . ":$opt->{port}" } 179 @{ $opt->{'listen-ip'} }) 180 : "Listen $opt->{port}"; 181 182 if ($opt->{ssl}) { 183 push @directives, 184 'LoadModule ssl_module ' . apache_module_path('mod_ssl.so') 185 if $modlist !~ /\bmod.ssl\.c\b/i; # XXX: are there other variants? 186 push @directives, qq(SSLCertificateFile "$opt->{'server-cert'}") 187 if exists $opt->{'server-cert'}; 188 push @directives, qq(SSLCertificateKeyFile "$opt->{'server-key'}") 189 if exists $opt->{'server-key'}; 190 push @directives, 'SSLEngine on'; 191 my $random = -r '/dev/urandom' ? 'file:/dev/urandom 256' : 'builtin'; 192 push @directives, "SSLRandomSeed startup $random", 193 "SSLRandomSeed connect $random"; 194 ##push @directives, 'SSLProtocol all -SSLv2'; # or v3 only? 195 } 196 197 # XXX: available in Apache 2.1+; previously in core (AFAIK); 198 # should we parse httpd -v? 199 push @directives, 200 'LoadModule ident_module ' . apache_module_path('mod_ident.so'), 201 'IdentityCheck on' 202 if $opt->{'auth-ident'}; 203 push @directives, "IdentityCheckTimeout $opt->{'ident-timeout'}" 204 if $opt->{'auth-ident'} && defined $opt->{'ident-timeout'}; 205 206 # SA stuff 207 push @directives, 208 'PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config', 209 'SAenabled on'; 210 push @directives, "SAAllow from @{$opt->{'allowed-ips'}}" 211 if exists $opt->{'allowed-ips'}; 212 push @directives, 'SAtell on' if $opt->{'allow-tell'}; 213 push @directives, "SAtimeout $opt->{'timeout-child'}" 214 if exists $opt->{'timeout-child'}; 215 push @directives, "SAdebug $opt->{debug}" if $opt->{debug}; 216 push @directives, 'SAident on' 217 if $opt->{'auth-ident'}; 218 219 push @directives, qq(SANew rules_filename "$opt->{configpath}") 220 if defined $opt->{configpath}; 221 push @directives, qq(SANew site_rules_filename "$opt->{siteconfigpath}") 222 if defined $opt->{siteconfigpath}; 223 push @directives, 224 qq(SANew home_dir_for_helpers "$opt->{home_dir_for_helpers}") 225 if defined $opt->{home_dir_for_helpers}; 226 push @directives, qq(SANew local_tests_only $opt->{local}) 227 if defined $opt->{local}; 228 push @directives, map qq(SANew $_ "$opt->{$_}"), grep defined $opt->{$_}, 229 qw(PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR); 230 push @directives, 'SANew paranoid 1' if $opt->{paranoid}; 231 push @directives, qq(SAConfigLine "$_") for @{ $opt->{cf} }; 232 233 my @users; 234 push @users, 'local' if $opt->{'user-config'}; 235 push @users, 'sql' if $opt->{'sql-config'}; 236 push @users, 'ldap' if $opt->{'ldap-config'}; 237 push @directives, join ' ', 'SAUsers', @users if @users; 238} 239 240# write directives to conf file (or STDOUT) and exit 241if ($opt->{httpd_conf}) { 242 my $fh; 243 if ($opt->{httpd_conf} eq '-') { 244 open $fh, '>&STDOUT' or die "open >&STDOUT: $!"; 245 } 246 else { 247 open $fh, '>', $opt->{httpd_conf} 248 or die "open >'$opt->{httpd_conf}': $!"; 249 } 250 print $fh join "\n", 251 "# generated by $0 on " . localtime(time), 252 @directives, 253 "# vim: filetype=apache\n"; 254 close $fh or warn "close: $!"; 255 exit 0; # user is supposed to run Apache himself 256} 257 258# 259# add directives to command line and run Apache 260# 261 262push @run, '-f', 263 File::Spec->devnull(), # XXX: will work on a non-POSIX platform? 264 map { ; '-C' => $_ } @directives; 265 266warn map({ /^-/ ? "\n $_" : " $_" } @run), "\n" 267 if $opt->{debug} =~ /\ball|spamd|config|info\b/; 268 269undef $opt; # there is no DESTROY... but could be one ;-) 270exec @run; # we are done 271 272# 273# helper functions 274# 275 276sub get_libexecdir { 277 get_libexecdir_A2BC() || get_libexecdir_apxs(); 278} 279 280# read it from Apache2::BuildConfig 281sub get_libexecdir_A2BC { 282 $INC{'Apache2/Build.pm'}++; # hack... needlessly required by BuildConfig 283 require Apache2::BuildConfig; 284 my $cfg = Apache2::BuildConfig->new; 285 $cfg->{APXS_LIBEXECDIR} || $cfg->{MODPERL_APXS_LIBEXECDIR}; 286} 287 288# `apxs -q LIBEXECDIR` 289sub get_libexecdir_apxs { 290 my @cmd = (($opt->{apxs} || 'apxs'), '-q', 'LIBEXECDIR'); 291 chomp(my $modpath = get_cmd_output(@cmd)); 292 die "ERROR: failed to obtain module path from '@cmd'\n" 293 unless length $modpath; 294 die "ERROR: '$modpath' returned by '@cmd' is not an existing directory\n" 295 unless -d $modpath; 296 $modpath; 297} 298 299# as above, cached version 300our $apache_module_path; 301sub apache_module_path { 302 my $modname = shift; 303 $apache_module_path ||= get_libexecdir(); # path is cached 304 my $module = File::Spec->catfile($apache_module_path, $modname); 305 die "ERROR: '$module' does not exist\n" if !-e $module; 306 $module; 307} 308 309# httpd -l 310# XXX: can MPM be a DSO? 311sub static_apache_modules { 312 my $httpd = shift; 313 my @cmd = ($httpd, '-l'); 314 my $out = get_cmd_output(@cmd); 315 my @modlist = $out =~ /\b(\S+\.c)\b/gi; 316 die "ERROR: failed to get list of static modules from '@cmd'\n" 317 unless @modlist; 318 @modlist; 319} 320 321sub get_cmd_output { 322 my @cmd = @_; 323 my $output = `@cmd` or die "ERROR: failed to run '@cmd': $!\n"; 324 $output; 325} 326 327sub mpm_specific_config { 328 my $mpm = shift; 329 my @ret; 330 331 if ($mpm =~ /^prefork|worker|beos|mpmt_os2$/) { 332 push @ret, "User $opt->{username}" if $opt->{username}; 333 push @ret, "Group $opt->{groupname}" if $opt->{groupname}; 334 } 335 elsif ($opt->{username} || $opt->{groupname}) { 336 die "ERROR: username / groupname not supported with MPM $mpm\n"; 337 } 338 339 if ($mpm eq 'prefork') { 340 push @ret, "StartServers $opt->{'min-spare'}"; 341 push @ret, "MinSpareServers $opt->{'min-spare'}"; 342 push @ret, "MaxSpareServers $opt->{'max-spare'}"; 343 push @ret, "MaxClients $opt->{'max-children'}"; 344 } 345 elsif ($mpm eq 'worker') { # XXX: we could be smarter here 346 push @ret, grep length, map { s/^\s+//; s/\s*\b#.*$//; $_ } split /\n/, 347 <<" EOF"; 348 StartServers 1 349 ServerLimit 1 350 MinSpareThreads $opt->{'min-spare'} 351 MaxSpareThreads $opt->{'max-spare'} 352 ThreadLimit $opt->{'max-children'} 353 ThreadsPerChild $opt->{'max-children'} 354 EOF 355 } 356 else { 357 warn "WARNING: MPM $mpm not supported, using defaults for performance settings\n"; 358 warn "WARNING: prepare for huge memory usage and maybe an emergency reboot\n"; 359 } 360 361 push @ret, "MaxRequestsPerChild $opt->{'max-conn-per-child'}" 362 if defined $opt->{'max-conn-per-child'}; 363 364 @ret; 365} 366 367# vim: ts=4 sw=4 noet 368