File: apache-spamd.pl

package info (click to toggle)
spamassassin 4.0.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 25,724 kB
  • sloc: perl: 89,143; ansic: 5,193; sh: 3,737; javascript: 339; sql: 295; makefile: 209; python: 49
file content (367 lines) | stat: -rwxr-xr-x 11,828 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/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