1#!/usr/bin/perl -w
2
3########################################################################
4#
5# <@LICENSE>
6# Licensed to the Apache Software Foundation (ASF) under one or more
7# contributor license agreements.  See the NOTICE file distributed with
8# this work for additional information regarding copyright ownership.
9# The ASF licenses this file to you under the Apache License, Version 2.0
10# (the "License"); you may not use this file except in compliance with
11# the License.  You may obtain a copy of the License at:
12#
13#     http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20# </@LICENSE>
21#
22########################################################################
23
24# Written by Daryl C. W. O'Shea, DOS Technologies <spamassassin@dostech.ca>
25# See  perldoc sa-check_spamd  for program info.
26
27use strict;
28use warnings;
29use re 'taint';
30
31my $PREFIX          = '@@PREFIX@@';             # substituted at 'make' time
32my $DEF_RULES_DIR   = '@@DEF_RULES_DIR@@';      # substituted at 'make' time
33my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@';    # substituted at 'make' time
34my $LOCAL_STATE_DIR = '@@LOCAL_STATE_DIR@@';    # substituted at 'make' time
35use lib '@@INSTALLSITELIB@@';                   # substituted at 'make' time
36
37use Errno qw(EBADF);
38use File::Spec;
39use Config;
40use POSIX qw(locale_h setsid sigprocmask _exit);
41
42POSIX::setlocale(LC_TIME,'C');
43
44BEGIN {                          # see comments in "spamassassin.raw" for doco
45  my @bin = File::Spec->splitpath($0);
46  my $bin = ($bin[0] ? File::Spec->catpath(@bin[0..1], '') : $bin[1])
47            || File::Spec->curdir;
48
49  if (-e $bin.'/lib/Mail/SpamAssassin.pm'
50        || !-e '@@INSTALLSITELIB@@/Mail/SpamAssassin.pm' )
51  {
52    my $searchrelative;
53    $searchrelative = 1;    # disabled during "make install": REMOVEFORINST
54    if ($searchrelative && $bin eq '../' && -e '../blib/lib/Mail/SpamAssassin.pm')
55    {
56      unshift ( @INC, '../blib/lib' );
57    } else {
58      foreach ( qw(lib ../lib/site_perl
59                ../lib/spamassassin ../share/spamassassin/lib))
60      {
61        my $dir = File::Spec->catdir( $bin, split ( '/', $_ ) );
62        if ( -f File::Spec->catfile( $dir, "Mail", "SpamAssassin.pm" ) )
63        { unshift ( @INC, $dir ); last; }
64      }
65    }
66  }
67}
68
69use Getopt::Long;
70
71use constant HAS_TIME_HIRES => eval { require Time::HiRes; };
72use constant HAS_MSA_CLIENT => eval { require Mail::SpamAssassin::Client; };
73use constant HAS_MSA_TIMEOUT => eval { require Mail::SpamAssassin::Timeout; };
74use Mail::SpamAssassin::Util;
75
76### nagios plugin return codes: 0 OK / 1 Warning / 2 Critical / 3 Unknown ###
77use constant EX_OK	 => 0;
78use constant EX_WARNING	 => 1;
79use constant EX_CRITICAL => 2;
80use constant EX_UNKNOWN	 => 3;
81
82my $VERSION = $Mail::SpamAssassin::VERSION;
83
84my %opt = (
85  'critical'	=> undef,
86  'hostname'	=> undef,
87  'port'	=> undef,
88  'socketpath'	=> undef,
89  'timeout'	=> 45,
90  'verbose'	=> undef,
91  'warning'	=> undef,
92);
93
94# Parse the command line
95Getopt::Long::Configure("bundling");
96GetOptions(
97  'critical|c=s'	=> \$opt{'critical'},
98  'help|h|?'		=> sub { print_usage_and_exit(); },
99  'hostname|H=s'	=> \$opt{'hostname'},
100  'port|p=s'		=> \$opt{'port'},
101  'socketpath=s'	=> \$opt{'socketpath'},
102  'timeout|t=s'		=> \$opt{'timeout'},
103  'verbose|v'		=> \$opt{'verbose'},
104  'version|V'		=> sub { print "sa-check_spamd version $VERSION\n"; exit EX_UNKNOWN; },
105  'warning|w=s'		=> \$opt{'warning'},
106
107) or print_usage_and_exit();
108
109
110if (defined $opt{'critical'}) {
111  if ($opt{'critical'} =~ /^(\d+(?:\.\d*)?)$/) {
112    $opt{'critical'} = $1;
113  } else {
114    print "SPAMD UNKNOWN: invalid critical config value provided\n";
115    exit EX_UNKNOWN;
116  }
117}
118
119if (defined $opt{'hostname'}) {
120  if ($opt{'hostname'} =~ /^([A-Za-z0-9_.:-]+)$/) {
121    $opt{'hostname'} = $1;
122  } else {
123    print "SPAMD UNKNOWN: invalid hostname config value provided\n";
124    exit EX_UNKNOWN;
125  }
126}
127
128if (defined $opt{'port'}) {
129  if ($opt{'port'} =~ /^(\d+)$/) {
130    $opt{'port'} = $1;
131  } else {
132    print "SPAMD UNKNOWN: invalid port config value provided\n";
133    exit EX_UNKNOWN;
134  }
135}
136
137# TODO: --socketpath isn't checked, suboptimal
138
139if ($opt{'timeout'} =~ /^(\d+(?:\.\d*)?)$/ && $opt{'timeout'} >= 1) {
140  $opt{'timeout'} = $1;
141} else {
142  print "SPAMD UNKNOWN: invalid timeout config value provided\n";
143  exit EX_UNKNOWN;
144}
145
146if (defined $opt{'warning'}) {
147  if ($opt{'warning'} =~ /^(\d+(?:\.\d*)?)$/) {
148    $opt{'warning'} = $1;
149  } else {
150    print "SPAMD UNKNOWN: invalid warning config value provided\n";
151    exit EX_UNKNOWN;
152  }
153}
154
155# logic checking
156if (defined $opt{'critical'} && defined $opt{'warning'} &&
157	$opt{'critical'} < $opt{'warning'}) {
158  print "SPAMD UNKNOWN: critical value is less than warning value, config not valid\n";
159  exit EX_UNKNOWN;
160}
161
162if (defined $opt{'critical'} && defined $opt{'timeout'} &&
163	$opt{'critical'} > $opt{'timeout'}) {
164  print "SPAMD UNKNOWN: critical value is greater than timeout value, config not valid\n";
165  exit EX_UNKNOWN;
166}
167
168if (defined $opt{'warning'} && defined $opt{'timeout'} &&
169	$opt{'warning'} > $opt{'timeout'}) {
170  print "SPAMD UNKNOWN: warning value is greater than timeout value, config not valid\n";
171  exit EX_UNKNOWN;
172}
173
174# check to make sure that both TCP and UNIX domain socket info wasn't provided
175if ((defined $opt{'hostname'} || defined $opt{'port'}) && defined $opt{'socketpath'}) {
176  print "SPAMD UNKNOWN: both TCP and UNIX domain socket info provided, only one can be used\n";
177  exit EX_UNKNOWN;
178}
179
180# if not provided with a spamd service to connect to set some defaults
181unless (defined $opt{'socketpath'}) {
182  $opt{'hostname'} ||= 'localhost';
183  $opt{'port'} ||= 783;
184}
185
186
187if ($opt{'verbose'}) {
188  print ((HAS_MSA_CLIENT ? "loaded" : "failed to load") ." Mail::SpamAssassin::Client\n");
189  print ((HAS_MSA_TIMEOUT ? "loaded" : "failed to load") ." Mail::SpamAssassin::Timeout\n");
190}
191
192# If there's no client available, there's no way to check the service...
193unless (HAS_MSA_CLIENT && HAS_MSA_TIMEOUT) {
194  # Nagios will only display the first line printed.
195  print "SPAMD UNKNOWN: could not load M:SA::Client\n" unless HAS_MSA_CLIENT;
196  print "SPAMD UNKNOWN: could not load M:SA::Timeout\n" unless HAS_MSA_TIMEOUT;
197  print "cannot continue\n" if $opt{'verbose'};
198  exit EX_UNKNOWN;
199}
200
201
202# untaint the command-line args; since the root user supplied these, and
203# we're not a setuid script, we trust them.  This needs to be called explicitly
204foreach my $optkey (keys %opt) {
205  next if ref $opt{$optkey};
206  Mail::SpamAssassin::Util::untaint_var(\$opt{$optkey});
207}
208
209
210# If the client connection fails it'll spit out it's own error message which
211# is probably more appropriate than anything we can provide to Nagios ourself.
212# We'll still spit out something later, but Nagios will ignore it since it
213# only uses the first line of output.
214my $client;
215if (defined $opt{'port'}) {
216 $client = new Mail::SpamAssassin::Client({port => $opt{'port'},
217					   host => $opt{'hostname'}});
218} else {
219 $client = new Mail::SpamAssassin::Client({socketpath => $opt{'socketpath'}});
220}
221
222# this'd be weird, but totally dependent on the client
223unless (defined $client) {
224  print "SPAMD UNKNOWN: could not create M::SA::Client instance\n";
225  print "failed to create Mail::SpamAssassin::Client instance\n" if $opt{'verbose'};
226  exit EX_UNKNOWN;
227}
228
229# until we try a ping, the ping response status is unknown
230my $response = -1;
231print "connecting to spamd for ping\n" if $opt{'verbose'};
232
233my $timer = Mail::SpamAssassin::Timeout->new({ secs => $opt{'timeout'}});
234my $t0 = (HAS_TIME_HIRES ? Time::HiRes::time() : time());
235
236my $err = $timer->run(sub {
237  if ($client->ping()) {
238    $response = 1;
239  } else {
240    $response = 0;
241  }
242});
243
244my $elapsed = (HAS_TIME_HIRES ? Time::HiRes::time() : time()) - $t0;
245
246
247# a ping response should be most common, we'll handle it first
248if ($response == 1) {
249  # it's possible that we may timeout right after setting the response status to 1
250  # since the timeout value > the critical value, this is a critical state
251  if ((defined $opt{'critical'} && $elapsed > $opt{'critical'}) || $timer->timed_out()) {
252    printf("SPAMD CRITICAL: %.3f second ping response time\n", $elapsed);
253    exit EX_CRITICAL;
254  }
255
256  # warning state will never timeout since that'd be critical (above)
257  if (defined $opt{'warning'} && ($elapsed > $opt{'warning'})) {
258    printf("SPAMD WARNING: %.3f second ping response time\n", $elapsed);
259    exit EX_WARNING;
260  }
261
262  # otherwise we got a timely ping response
263  printf("SPAMD OK: %.3f second ping response time\n", $elapsed);
264  exit EX_OK;
265}
266
267# any way we get a failed ping response is a critical state
268if ($response == 0) {
269  printf("SPAMD CRITICAL: ping failed in %.3f seconds\n", $elapsed);
270  exit EX_CRITICAL;
271}
272
273if ($response == -1) {
274  # this is the common timeout scenario
275  if ($timer->timed_out()) {
276    printf("SPAMD CRITICAL: ping timed out in %.3f seconds\n", $elapsed);
277    exit EX_CRITICAL;
278  }
279
280  # dos: I'll buy lunch for the first person that gets a page about this while
281  # they're sleeping if they come to Midland, ON to get it
282  printf("SPAMD UNKNOWN: assertion! unknown ping response status without timeout after %.3f seconds\n", $elapsed);
283  exit EX_UNKNOWN;
284}
285
286# and some apple pie too
287exit EX_UNKNOWN;
288
289
290#############################################################################
291
292sub print_usage_and_exit {
293  print <<EOF;
294sa-check_spamd version $VERSION
295
296For more details, use "perldoc sa-check_spamd".
297
298Usage:
299   sa-check_spamd [options]
300
301    Options:
302
303     -c secs, --critical=secs          Critical ping response threshold
304     -h, -?, --help                    Print usage message
305     -H hostname, --hostname=hostname  Hostname of spamd service to ping
306     -p port, --port=port              Port of spamd service to ping
307     --socketpath=path                 Connect to given UNIX domain socket
308     -t secs, --timeout=secs           Max time to wait for a ping response
309     -v, --verbose                     Verbose debug output
310     -V, --version                     Output version info
311     -w secs, --warning=secs           Warning ping response threshold
312
313EOF
314
315  exit EX_UNKNOWN;
316}
317
318# Don't use a __DATA__ here, it screws up embedded Perl Nagios (ePN)
319
320=head1 NAME
321
322sa-check_spamd - spamd monitoring script for use with Nagios, etc.
323
324=head1 SYNOPSIS
325
326sa-check_spamd [options]
327
328Options:
329
330 -c secs, --critical=secs          Critical ping response threshold
331 -h, -?, --help                    Print usage message
332 -H hostname, --hostname=hostname  Hostname of spamd service to ping
333 -p port, --port=port              Port of spamd service to ping
334 --socketpath=path                 Connect to given UNIX domain socket
335 -t secs, --timeout=secs           Max time to wait for a ping response
336 -v, --verbose                     Verbose debug output
337 -V, --version                     Output version info
338 -w secs, --warning=secs           Warning ping response threshold
339
340
341=head1 DESCRIPTION
342
343The purpose of this program is to provide a tool to monitor the status of
344C<spamd> server processes.  spamd is the daemonized version of the
345spamassassin executable, both provided in the SpamAssassin distribution.
346
347This program is designed for use, as a plugin, with the Nagios service
348monitoring software available from http://nagios.org.  It might be compatible
349with other service monitoring packages.  It is also useful as a command line
350utility or as a component of a custom shell script.
351
352=head1 OPTIONS
353
354Options of the long form can be shortened as long as the remain
355unambiguous (i.e. B<--host> can be used instead of B<--hostname>).
356
357=over 4
358
359=item B<-c> I<secs>, B<--critical>=I<secs>
360
361Critical ping response threshold in seconds.  If a spamd ping response takes
362longer than the value specified (in seconds) the program will exit with a
363value of 2 to indicate the critical status.
364
365This value must be at least as long as the value specified for B<warning> and
366less than the value specified for B<timeout>.
367
368=item B<-h>, B<-?>, B<--help>
369
370Prints this usage message and exits.
371
372=item B<-H> I<hostname>, B<--hostname>=I<hostname>
373
374The hostname, or IP address, of the spamd service to ping.  By default the
375hostname B<localhost> is used.  If B<--socketpath> is set this value will be
376ignored.
377
378=item B<-p> I<port>, B<--port>=I<port>
379
380The port of the spamd service to ping.  By default port B<783> (the spamd
381default port number) is used.  If B<--socketpath> is set this value will be
382ignored.
383
384=item B<--socketpath>=I<path>
385
386Connect to given UNIX domain socket.  Use instead of a hostname and TCP port.
387When set, any hostname and TCP port specified will be ignored.
388
389=item B<-t> I<secs>, B<--timeout>=I<secs>
390
391The maximum time to wait for a ping response.  Once exceeded the program will
392exit with a value of 2 to indicate the critical status.  The default timeout
393value is 45 seconds.  The timeout must be no less than 1 second.
394
395This value must be greater than the values specified for both the B<critical>
396and B<warning> values.
397
398=item B<-v>, B<--verbose>
399
400Display verbose debug output on STDOUT.
401
402=item B<-V>, B<--version>
403
404Display version info on STDOUT.
405
406=item B<-w> I<secs>, B<--warning>=I<secs>
407
408Warning ping response threshold in seconds.  If a spamd ping response takes
409longer than the value specified (in seconds), and does not exceed the
410B<critical> threshold value, the program will exit with a value of 1 to
411indicate the warning status.
412
413This value must be no longer than the value specified for B<critical> and
414less than the value specified for B<timeout>.
415
416=back
417
418=head1 EXIT CODES
419
420The program will indicate the status of the spamd process being monitored by
421exiting with one of these values:
422
423=over 4
424
425=item C<0>
426
427OK: A spamd ping response was received within all threshold times.
428
429=item C<1>
430
431WARNING: A spamd ping response exceeded the warning threshold but not the
432critical threshold.
433
434=item C<2>
435
436CRITICAL: A spamd ping response exceeded either the critical threshold or the
437timeout value.
438
439=item C<3>
440
441UNKNOWN: An error, probably caused by a missing dependency or an invalid
442configuration parameter being supplied, occurred in the sa-check_spamd program.
443
444=back
445
446=head1 SEE ALSO
447
448spamc(1)
449spamd(1)
450spamassassin(1)
451
452=head1 PREREQUISITES
453
454C<Mail::SpamAssassin> version 3.1.1 or higher (3.1.6 or higher recommended)
455
456=head1 AUTHOR
457
458Daryl C. W. O'Shea, DOS Technologies <spamassassin@dostech.ca>
459
460=head1 COPYRIGHT AND LICENSE
461
462sa-check_spamd is distributed under the Apache License, Version 2.0, as
463described in the file C<LICENSE> included with the Apache SpamAssassin
464distribution and available at http://www.apache.org/licenses/LICENSE-2.0
465
466Copyright (C) 2015 The Apache Software Foundation
467
468=cut
469