File: Config.pm

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 (381 lines) | stat: -rw-r--r-- 12,223 bytes parent folder | download | duplicates (3)
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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package Mail::SpamAssassin::Spamd::Config;
use strict;

use Mail::SpamAssassin::Util ();
use File::Spec ();
use Getopt::Long qw(GetOptions :config bundling);    # configuration is global

=head1 NAME

Mail::SpamAssassin::Spamd::Config -- parse spamd command line options

=head1 SYNOPSIS

  my $conf = Mail::SpamAssassin::Spamd::Config->new(
    {
      argv     => \@ARGV,
      defaults => { 'user-config' => 0, },
      moreopts => [ qw( foo-option|f=s@ bar-option|b=i ) ],
    }
  );

=head1 DESCRIPTION

This module uses Getopt::Long to parse and validate spamd command line options.

Returns blessed hash reference, containing coherent set of options.
Dies on error.

See source and C<spamd(1)> for list of valid options.

=head1 WARNING

This interface is considered experimental and likely to change.  Notify the dev
list if you're planning to rely on it.

Getopt::Long::Configure is used to modify global C<Getopt::Long(3)>
configuration.  If you don't want C<:bundling> and / or wish to enable /
disable something else for whatever reason... well, keep that in mind.

=head1 BUGS

Error messages are not unified.

See E<lt>http://bugzilla.spamassassin.org/E<gt>

=head1 SEE ALSO

C<spamd(1)>,
C<apache-spamd(1)>

=cut

my %defaults = (
  'user-config'   => 1,
  'ident-timeout' => 5.0,

  # scaling settings; some of these aren't actually settable via cmdline
  'server-scale-period' => 2,    # how often to scale the # of kids, secs
  'min-children'        => 1,    # min kids to have running
  'min-spare'           => 1,    # min kids that must be spare
  'max-spare'           => 2,    # max kids that should be spare
  'max-children'        => 5,
  'max-conn-per-child'  => 200,
  'timeout-child'       => 300,
  'timeout-tcp'         => 30,

  # substituted at 'make' time
  PREFIX          => '/usr',
  DEF_RULES_DIR   => '/usr/share/spamassassin',
  LOCAL_RULES_DIR => '/etc/mail/spamassassin',
  LOCAL_STATE_DIR => '/var/lib',
);

sub new {
  my ($class, $parms) = @_;
  $parms ||= {};
  die 'usage: ' . __PACKAGE__
    . '->new({ argv=>\@, defaults=>\%, moreopts=>\@ })'
    if ref $parms ne 'HASH'
    or exists $parms->{argv}     && ref $parms->{argv}     ne 'ARRAY'
    or exists $parms->{defaults} && ref $parms->{defaults} ne 'HASH'
    or exists $parms->{moreopts} && ref $parms->{moreopts} ne 'ARRAY';
  $parms->{argv} ||= \@ARGV;

  local *ARGV = [@{ $parms->{argv} }];

  my $self = { exists $parms->{defaults} ? %{ $parms->{defaults} } : () };

  Getopt::Long::Configure('bundling');
  GetOptions(
    $self,

    # !xargs -n1|sort|column|expand
    qw(
      allowed-ips|A=s@                round-robin!
      allow-tell|l                    server-cert=s
      auth-ident                      server-key=s
      cf=s@                           setuid-with-ldap
      configpath|C=s                  setuid-with-sql|Q
      create-prefs|c!                 siteconfigpath=s
      daemonize|d!                    socketgroup=s
      debug|D:s                       socketmode=s
      groupname|g=s                   socketowner=s
      help|h                          socketpath=s
      ident-timeout=f                 sql-config|q!
      ldap-config!                    ssl
      listen-ip|ip-address|i:s@       syslog-socket=s
      local|L!                        syslog|s=s
      max-children|m=i                timeout-child|t=i
      max-conn-per-child=i            timeout-tcp|T=i
      max-spare=i                     user-config!
      min-children=i                  username|u=s
      min-spare=i                     version|V
      paranoid|P!                     virtual-config-dir=s
      pidfile|r=s                     vpopmail|v!
      port|p=s

      home_dir_for_helpers|helper-home-dir|H:s
      PREFIX=s
      DEF_RULES_DIR=s
      LOCAL_RULES_DIR=s
      LOCAL_STATE_DIR=s
      ),

    x => sub { $self->{'user-config'} = 0 },

    # NOTE: These are old options.  We should ignore (but warn about)
    # the ones that are now defaults.  Everything else gets a die (see note2)
    # so the user doesn't get us doing something they didn't expect.
    #
    # NOTE2: 'die' doesn't actually stop the process, GetOptions() catches
    # it, then passes the error on, so we'll end up doing a Usage statement.
    # You can avoid that by doing an explicit exit in the sub.

    # last in 2.3
    'F:i' => sub {
      die "spamd: the -F option has been removed from spamd,",
        " please remove from your commandline and re-run\n";
    },
    'add-from!' => sub {
      die "spamd: the --add-from option has been removed from spamd,",
        " please remove from your commandline and re-run\n";
    },

    # last in 2.4
    'stop-at-threshold|S' => sub {
      warn "spamd: the --stop-at-threshold|-S option has been deprecated",
        " and is no longer supported, ignoring\n";
    },

    (exists $parms->{moreopts} ? @{$parms->{moreopts}} : ()),
  ) or die 'GetOptions() failed';

  # XXX: uncomment this?
  #$self = { map { y/-/_/; $_ => $self->{$_}; } keys %$self };
  bless $self, $class;


  $self->_validate_logging;
  $self->_validate;

  $self;
}


# check & set some factory settings
sub _validate {
  my ($self) = @_;

  # sanity checking on parameters: if --socketpath is used, it means that we're
  # using UNIX domain sockets, none of the IP params are allowed. The code would
  # probably work ok if we didn't check it, but it's better if we detect the
  # error and report it lest the admin find surprises.
  if (exists $self->{'socketpath'}) {
    die "ERROR: --socketpath mutually exclusive with"
      . " --allowed-ips/--ssl/--auth-ident/--port params"
      if exists $self->{'allowed-ips'} && @{ $self->{'allowed-ips'} } > 0
      || exists $self->{'ssl'}
      || exists $self->{'auth-ident'}
      || exists $self->{'port'};
  }
  else {
    die "ERROR: --socketowner/group/mode requires --socketpath param"
      if exists $self->{'socketowner'}
      || exists $self->{'socketgroup'}
      || exists $self->{'socketmode'};

    # These can be changed on command line with -A flag,
    # but only if we're not using UNIX domain sockets
    # warning: no validation here
    $self->{'allowed-ips'} =
      exists $self->{'allowed-ips'} && @{ $self->{'allowed-ips'} }
      ? [map { split /,/, $_; } @{ $self->{'allowed-ips'} }]
      : ['127.0.0.1'];

    $self->{'listen-ip'} =
      !exists $self->{'listen-ip'}
      ? ['127.0.0.1']
      : defined $self->{'listen-ip'} && grep(length, @{ $self->{'listen-ip'} })
        ? [grep length, map { split /,/, $_; } @{ $self->{'listen-ip'} }]
        : undef;          # handle !defined elsewhere
  }

  # bug 2228: make the values of (almost) all parameters which accept file paths
  # absolute, so they are still valid after daemonize()
  for my $opt (
    grep(exists $self->{$_},
    qw(configpath siteconfigpath socketpath pidfile server-cert server-key
       PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR)),
    grep { exists $self->{$_} && $self->{$_} }
    qw(home_dir_for_helpers)    # value is optional
    )
  {
    $self->{$opt} = Mail::SpamAssassin::Util::untaint_file_path(
      File::Spec->rel2abs($self->{$opt})    # rel2abs taints the new value!
    );
  }

  # -d
  for my $opt (
    grep(exists $self->{$_},
    qw(configpath siteconfigpath
       PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR)),
    grep { exists $self->{$_} && $self->{$_} }
    qw(home_dir_for_helpers)    # value is optional
    )
  {
    die "ERROR: --$opt='$self->{$opt}' does not exist or not a directory\n"
      unless -d $self->{$opt};
  }

  # >= 0
  for my $opt (grep exists $self->{$_}, qw(min-spare max-spare)) {
    die "ERROR: --$opt must be >= 0\n" if $self->{$opt} <= 0;
  }

  # >= 1
  for my $opt (
    grep exists $self->{$_},
    qw(timeout-tcp timeout-child min-children max-children max-conn-per-child)
    )
  {
    next if $self->{$opt} >= 1;
    warn "ERROR: --$opt must be >= 1, ignoring\n";    # die?
    delete $self->{$opt};
  }

  # ident-based spamc user authentication
  if ($self->{'auth-ident'}) {
    eval { sub Net::Ident::_export_hooks(); require Net::Ident };
    die "spamd: ident-based authentication requested,",
      " but Net::Ident is unavailable\n"
      if $@;

    if (exists $self->{'ident-timeout'} && $self->{'ident-timeout'} <= 0) {
      die "ERROR: --ident-timeout must be > 0\n";
    }
    ##Net::Ident->import(qw(ident_lookup));
  }

  # let's not modify %ENV here...
  my $home =
    (exists $ENV{HOME} && defined $ENV{HOME} && -d $ENV{HOME})
    ? $ENV{HOME}
    : undef;

  if (exists $self->{username})
  {    # spamd is going to run as another user, so reset $HOME
    if (my $nh = (getpwnam($self->{username}))[7]) {
      $home = $nh;
    }
    else {
      die "spamd: unable to determine home directory for user"
        . " '$self->{username}'\n";
    }
  }

  if (!exists $self->{home_dir_for_helpers}) {
    die "ERROR: \$HOME='$home' does not exist or not a directory\n"
      unless defined $home && -d $home;
    $self->{home_dir_for_helpers} = $home;
  }

  if (exists $self->{'max-spare'}) {
    if (exists $self->{'min-spare'}) {
      ## emulate Apache behaviour:
      ## http://httpd.apache.org/docs-2.0/mod/prefork.html#maxspareservers
      $self->{'max-spare'} = $self->{'min-spare'} + 1
        if $self->{'max-spare'} < $self->{'min-spare'};
    }
    else {
      $self->{'min-spare'} = $self->{'max-spare'};
    }
  }
  elsif (exists $self->{'min-spare'}) {
    $self->{'max-spare'} = $self->{'min-spare'};
  }

  # set other defaults
  for my $opt (keys %defaults) {
    $self->{$opt} = $defaults{$opt} if !exists $self->{$opt};
  }

  # check for server certs, must be done after setting other defaults
  if ($self->{'ssl'}) {
    $self->{'server-key'}  ||= "$self->{LOCAL_RULES_DIR}/certs/server-key.pem";
    $self->{'server-cert'} ||= "$self->{LOCAL_RULES_DIR}/certs/server-cert.pem";
    eval { require IO::Socket::SSL };
    die "spamd: SSL encryption requested, but IO::Socket::SSL is unavailable\n"
      if $@;
    die "spamd: server key file '$self->{'server-key'}' does not exist\n"
      unless -f $self->{'server-key'};
    die "spamd: server certificate file '$self->{'server-cert'}' does not exist\n"
      unless -f $self->{'server-cert'};
  }

  # XXX: delete LOCAL_{RULES,STATE}_DIR and PREFIX if eq $defaults{$_}?

  1;
}

sub _validate_logging {
  my $self = shift;

  # Enable debugging, if any areas were specified.  We do this already here,
  # accessing some non-public API so we can use the convenient dbg() routine.
  # Don't do this at home (aka any 3rd party tools), kids!
  $self->{debug} ||= 'all' if exists $self->{debug};

  # always turn on at least info-level debugging for spamd
  $self->{debug} ||= 'info';

  $self->{'syslog-socket'} = lc $self->{'syslog-socket'} || 'unix';
  $self->{'log-facility'} = $self->{syslog} || 'mail';

  # This is the default log file; it can be changed on the command line
  # via a --syslog flag containing non-word characters.
  $self->{'log-file'} = 'spamd.log';

  if ($self->{'log-facility'} =~ /[^a-z0-9]/) {
    # A specific log file was given (--syslog=/path/to/file).
    $self->{'log-file'}      = $self->{'log-facility'};
    $self->{'syslog-socket'} = 'file';
  }
  elsif ($self->{'log-facility'} eq 'file') {
    # The generic log file was requested (--syslog=file).
    $self->{'syslog-socket'} = 'file';
  }
  else {
    # The casing is kept only if the facility specified a file.
    $self->{'log-facility'} = lc $self->{'log-facility'};
  }

  if ($self->{'syslog-socket'} eq 'file') {
    # Either above or at the command line the socket was set
    # to 'file' (--syslog-socket=file).
    $self->{'log-facility'} = 'file';
  }
  elsif ($self->{'syslog-socket'} eq 'none') {
    # The socket 'none' (--syslog-socket=none) historically
    # represents logging to STDERR.
    $self->{'log-facility'} = 'stderr';
  }

  # Either above or at the command line the facility was set
  # to 'stderr' (--syslog=stderr).
  $self->{'syslog-socket'} = 'file' if $self->{'log-facility'} eq 'stderr';

  1;
}

sub option {
  my ($self, $opt) = @_;
  return exists $self->{$opt}
    ? $self->{$opt}
    : undef;
}

1;

# vim: ts=8 sw=2 et