The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

=head1 NAME

Nagios::NRPE::Daemon - A Nagios NRPE Daemon

=head1 SYNOPSIS

 use Nagios::NRPE::Daemon;
 use IPC::Cmd qw(can_run run run_forked);

 # create the commandlist we accept
 my $commandlist = 
 };
 my $callback = sub {
   my ($self,$check,@options) = @_;
   my $commandlist = $self->commandlist();
   if ($commandlist->{$check}) {
     my $args = $commandlist->{$check}->{args};
     my $i = 0;
     foreach (@options) {
       $i++;
       $args =~ "s/\$ARG$i\$/$_/";
     }
     my $buffer;
     if (scalar run(command => $commandlist->{$check}->{bin} . " " . $args,
                    verbose => 0,
                    buffer => \$buffer,
                    timeout => 20)) {
       return $buffer;
     }
   }
 };

 my $daemon = Nagios::NRPE::Daemon->new(
   listen => "127.0.0.1",
   port => "5666",
   pid_dir => '/var/run',
   ssl => 0,
   commandlist => {
     "check_cpu" => { bin => "/usr/lib/nagios/plugin/check_cpu",
                      args => "-w 50 -c 80" }
   },
   callback => $callback
 );

=head1 DESCRIPTION

A simple daemon implementation with the capabillity to add your own callbacks 
and hooks in case you want to build your own NRPE Server.

=cut

package Nagios::NRPE::Daemon;

our $VERSION = '1.0.2';

use 5.010_000;

use strict;
use warnings;

use Data::Dumper;
use Carp;
use IO::Socket;
use IO::Socket::INET6;
use Nagios::NRPE::Packet qw(NRPE_PACKET_VERSION_3
  NRPE_PACKET_VERSION_2
  NRPE_PACKET_RESPONSE
  MAX_PACKETBUFFER_LENGTH
  STATE_UNKNOWN
  STATE_CRITICAL
  STATE_WARNING
  STATE_OK);

=pod

=head1 SUBROUTINES

=over

=item new()

Takes the following options as a hashref:

 * listen:

Listen on this IP Address

 * port:

Port to listen on

 * pid_dir

The pidfile for this daemon

 * ssl

Use ssl (1|0)

 * commandlist

A hashref of the allowed commands on the daemon

 * callback

A sub executed everytime a check should be run. Giving the daemon full control what should happen.

 my $callback = sub {
   my ($self,$check,@options) = @_;
   my $commandlist = $self->commandlist();
   if ($commandlist->{$check}) {
     my $args = $commandlist->{$check}->{args};
     my $i = 0;
     foreach (@options) {
       $i++;
       $args =~ "s/\$ARG$i\$/$_/";
     }
     my $buffer;
     if (scalar run(command => $commandlist->{$check}->{bin} . " " . $args,
                    verbose => 0,
                    buffer => \$buffer,
                    timeout => 20)) {
       return $buffer;
     }
   }
 };


=back

=cut

sub new {
    my ( $class, %hash ) = @_;
    my $self = {};

    $self->{listen}      = delete $hash{listen}      || "0.0.0.0";
    $self->{port}        = delete $hash{port}        || "5666";
    $self->{pid_dir}     = delete $hash{pid_dir}     || "/var/run";
    $self->{ssl}         = delete $hash{ssl}         || 0;
    $self->{commandlist} = delete $hash{commandlist} || {};
    $self->{callback}    = delete $hash{callback}    || sub { };

    bless $self, $class;
}

=pod

=over

=item start()

Starts the server and enters the Loop listening for packets

=back

=cut

sub start {
    my $self     = shift;
    my $packet   = Nagios::NRPE::Packet->new();
    my $callback = $self->{callback};
    my ( $socket, $s );

    $socket = $self->create_socket();

    while (1) {
        while ( ( $s = $socket->accept() ) ) {
            my $request;
            $s->recv( $request, 1036 );
            my $unpacked_request = $packet->deassemble($request);
            my $buffer           = $unpacked_request->{buffer};
            my $version          = $unpacked_request->{packet_version};
            my ( $command, @options ) = split /!/, $buffer;

            my $return = $self->{callback}( $self, $command, @options );
            eval {
                print $s $packet->assemble(
                    version => $version,
                    type    => NRPE_PACKET_RESPONSE,
                    check   => $return
                );
            };

            close($s);
        }
    }
}

=pod

=over

=item commandlist()

A hashref of elements that are valid commands.
An example for it is:

 "check_cpu" => { bin => "/usr/lib/nagios/plugin/check_cpu",
                  args => "-w 50 -c 80" }

C<args> can contain $ARG1$ elements like normal nrpe.cfg command elements.

=back

=cut

sub commandlist {
    my $self = shift;
    return $self->{commandlist};
}

=pod

=over

=item create_socket()

A shorthand function returning either an encrypted or unencrypted socket
depending on wether ssl is set to 1 or 0.

=back

=cut

sub create_socket {
    my $self = shift;
    my $socket;

    if ( $self->{ssl} ) {
        eval {
            # required for new IO::Socket::SSL versions
            require IO::Socket::SSL;
            IO::Socket::SSL->import();
            IO::Socket::SSL::set_ctx_defaults( SSL_verify_mode => 0 );
        };
        $socket = IO::Socket::SSL->new(
            Listen          => 5,
            LocalAddr       => $self->{host},
            LocalPort       => $self->{port},
            Proto           => 'tcp',
            Reuse           => 1,
            SSL_verify_mode => 0x01,
            Type            => SOCK_STREAM
        ) or die( IO::Socket::SSL::errstr() );
    }
    else {
        $socket = IO::Socket::INET6->new(
            Listen    => 5,
            LocalAddr => $self->{host},
            LocalPort => $self->{port},
            Reuse     => 1,
            Proto     => 'tcp',
            Type      => SOCK_STREAM
        ) or die "ERROR: $@ \n";
    }
    return $socket;
}

=pod

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by the authors (see AUTHORS file).

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut

1;