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

=pod

=head1 NAME

Nagios::NRPE::Packet - Assembly and de-assembly of an NRPE packet

=head1 SYNOPSIS

 use IO::Socket;
 use IO::Socket::INET;
 # Import necessary constants into Namespace
 use Nagios::NRPE::Packet qw(NRPE_PACKET_VERSION_3
                             NRPE_PACKET_QUERY
                             MAX_PACKETBUFFER_LENGTH
                             STATE_UNKNOWN
                             STATE_CRITICAL
                             STATE_WARNING
                             STATE_OK);

 my $packet = Nagios::NRPE::Packet->new();

 my $socket = IO::Socket::INET->new(
                    PeerAddr => $host,
                    PeerPort => $port,
                    Proto    => 'tcp',
                    Type     => SOCK_STREAM) or die "ERROR: $@ \n";

 print $socket $packet->assemble(type => QUERY_PACKET,
                              buffer => "check_load 1 2 3",
                              version => NRPE_PACKET_VERSION_3 );

 my $data = <$socket>
 my $response = $packet->deassemble($data);

 print $response->{buffer};
 exit $response->{result_code};

=head1 DESCRIPTION

This class is meant to be used when an active connection exists and is ready to send the
packet.

=head1 CONSTRUCTION

=over

=item new

Takes the following options as a hashref

=back

=head1 SUBROUTINES

Following functions can be used after the creation of the packet

=over 2

=item assemble()

Takes a hash of options defining the packet to be sent and returns the assembled packet. You can print this
to an open socket and send it to either a server or the client depending on your situation.

 * check

A string defining the check to be run or the output of a check eg: "check_cpu"
NOTE: Nagios can accept arguments appended to the check in the form: "check_somecheck!ARG1!ARG2!ARG..."

 * version

The NRPE version you want to use (only V2 and V3 work V1 is not supported, deafult is V3).

See CONSTANTS for options here.

 * type

The TYPE of packet you wish to send, which is either QUERY or RESPONSE.

See CONSTANTS for options here.

 * result_code

This is the exit code of the check script that is run, and check_nrpe.pl will exit with this value from the 
RESPONSE packet.

A set value for the QUERY type packet is 2324.

=item assemble_v2()

A helper function to assemble a V2 packet.

=item assemble_v3()

A helper function to assemble a V3 packet.

=item deassemble()

Takes a packet recieved by either client or server and deassembles them. The returned hashref contains 
the following values for a V3 packet:

 packet_version 
 packet_type    
 crc32_value    
 result_code    
 alignment      
 buffer_length  
 buffer

and the following values for a V2 packet:

 packet_version 
 packet_type    
 crc32_value    
 result_code    
 buffer

=item deassemble_v2()

Helper function for deassembleing a V2 packet

=item deassemble_v3()

Helper function for deassembleing a V3 packet

=item validate($packet)

Validates the contents of a packet using CRC32 checksumming. Returns undef
if not succesful.


=item packet_dump

Debugging function for hexdumping a binary string.

=back

=head1 CONSTANTS

These constants can be exported upon request with the 'use' pragma like this:

 # Will only import the constant NRPE_PACKET_VERSION_3 into your namespace
 use Nagios::NRPE::Packet qw(NRPE_PACKET_VERSION_3);

=over 2

=item * NRPE_PACKET_VERSION_3
        NRPE_PACKET_VERSION_2
        NRPE_PACKET_VERSION_1

The value of the NRPE version you want/need to use.

=item * QUERY_PACKET
        RESPONSE_PACKET

The packet type you want to send or recieve

=item * MAX_PACKETBUFFER_LENGTH
        MAX_COMMAND_ARGUMENTS

A threshhold on the send data

=item * NRPE_HELLO_COMMAND

unknown

=item * DEFAULT_SOCKET_TIMEOUT
        DEFAULT_CONNECTION_TIMEOUT

The default timeout for a connection and its corresponding socket

=item * STATE_UNKNOWN
        STATE_CRITICAL
        STATE_WARNING
        STATE_OK

States returned by the check

=back

=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

package Nagios::NRPE::Packet;

our $VERSION = '1.0.2';

use 5.010_000;
require Exporter;
require overload;

BEGIN {
    @ISA       = qw(Exporter);
    @EXPORT_OK = qw(NRPE_PACKET_VERSION_3
      NRPE_PACKET_VERSION_2
      NRPE_PACKET_VERSION_1
      NRPE_PACKET_QUERY
      NRPE_PACKET_RESPONSE
      MAX_PACKETBUFFER_LENGTH
      MAX_COMMAND_ARGUMENTS
      NRPE_HELLO_COMMAND
      DEFAULT_SOCKET_TIMEOUT
      DEFAULT_CONNECTION_TIMEOUT
      STATE_UNKNOWN
      STATE_CRITICAL
      STATE_WARNING
      STATE_OK);
}

use warnings;
use strict;

use Carp;
use Convert::Binary::C;
use Digest::CRC 'crc32';

use constant {

    # packet version identifier
    NRPE_PACKET_VERSION_3 => 3,
    NRPE_PACKET_VERSION_2 => 2,
    NRPE_PACKET_VERSION_1 => 1,

    # id code for queries and responses to queries
    NRPE_PACKET_QUERY    => 1,
    NRPE_PACKET_RESPONSE => 2,

    # max amount of data we'll send in one query/response
    MAX_PACKETBUFFER_LENGTH    => 1024,
    MAX_COMMAND_ARGUMENTS      => 16,
    NRPE_HELLO_COMMAND         => "_NRPE_CHECK",
    DEFAULT_SOCKET_TIMEOUT     => 10,
    DEFAULT_CONNECTION_TIMEOUT => 300,

    # /* service state return codes */
    STATE_UNKNOWN  => 3,
    STATE_CRITICAL => 2,
    STATE_WARNING  => 1,
    STATE_OK       => 0,
};

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

    # taken with modifications from common.h in nagios-nrpe
    my $c = Convert::Binary::C->new( ByteOrder => 'BigEndian', Alignment => 0 );
    $self->{c} = $c;
    bless $self, $class;
}

sub assemble {
    my ( $self, %options ) = @_;
    croak "ERROR: Cannot send Packet with empty buffer!"
      if ( not defined $options{check} );
    my $packed;
    if ( $options{version} eq NRPE_PACKET_VERSION_2 ) {
        $packed = $self->assemble_v2(%options);
    }
    else {
        $packed = $self->assemble_v3(%options);
    }
    return $packed;

}

sub assemble_v3 {
    my ( $self, %options ) = @_;
    my $unpacked = {};
    my $len      = length( $options{check} ) + 1;

    $unpacked->{alignment}      = 0;
    $unpacked->{buffer_length}  = $len;
    $unpacked->{buffer}         = $options{check};
    $unpacked->{crc32_value}    = "\x00\x00\x00\x00";
    $unpacked->{packet_type}    = $options{type} || NRPE_PACKET_QUERY;
    $unpacked->{packet_version} = NRPE_PACKET_VERSION_3;
    $unpacked->{result_code}    = $options{result_code} || 2324;

    $self->{c}->parse(<<PACKET_STRUCT);
struct Packet{
  unsigned short   packet_version;
  unsigned short   packet_type;
  unsigned int     crc32_value;
  unsigned short   result_code;
  unsigned short   alignment;
  int              buffer_length;
  char             buffer[$len];
};
PACKET_STRUCT
    $self->{c}->tag( 'Packet.buffer', Format => 'String' );

    my $packed = $self->{c}->pack( 'Packet', $unpacked );

    $unpacked->{crc32_value} = crc32($packed);
    $packed = $self->{c}->pack( 'Packet', $unpacked );
    return $packed;

}

sub assemble_v2 {

    my ( $self, %options ) = @_;
    my $unpacked = {};

    $unpacked->{buffer}         = $options{check};
    $unpacked->{crc32_value}    = "\x00\x00\x00\x00";
    $unpacked->{packet_type}    = $options{type} || NRPE_PACKET_QUERY;
    $unpacked->{packet_version} = NRPE_PACKET_VERSION_2;
    $unpacked->{result_code}    = $options{result_code} || 2324;

    $self->{c}->parse(<<PACKET_STRUCT);
struct Packet{
  unsigned short   packet_version;
  unsigned short   packet_type;
  unsigned int     crc32_value;
  unsigned short   result_code;
  char             buffer[1024];
};
PACKET_STRUCT
    $self->{c}->tag( 'Packet.buffer', Format => 'String' );

    my $packed = $self->{c}->pack( 'Packet', $unpacked );

    $unpacked->{crc32_value} = crc32($packed);
    $packed = $self->{c}->pack( 'Packet', $unpacked );
    return $packed;

}

sub validate {
    my ( $self, $packet ) = @_;
    my $unpacked = $self->{c}->unpack( 'Packet', $packet );
    my $checksum = $unpacked->{crc32_value};
    $unpacked->{crc32_value} = "\x00\x00\x00\x00";
    my $packed = $self->{c}->pack( 'Packet', $unpacked );
    if ( crc32($packed) != $checksum ) {
        return undef;
    }
    else {
        return 1;
    }
}

sub deassemble {
    my ( $self, $packet ) = @_;
    my $ver = unpack( "n", $packet );
    my $unpacked = {};
    if ( $ver eq NRPE_PACKET_VERSION_2 ) {
        $unpacked = $self->deassemble_v2($packet);
    }
    else {
        $unpacked = $self->deassemble_v3($packet);
    }
    return $unpacked;
}

sub deassemble_v3 {
    my ( $self, $packet ) = @_;
    my @arr = unpack( "n2 N n2 N Z*", $packet );
    my $unpacked = {};
    $unpacked->{packet_version} = $arr[0];
    $unpacked->{packet_type}    = $arr[1];
    $unpacked->{crc32_value}    = $arr[2];
    $unpacked->{result_code}    = $arr[3];
    $unpacked->{alignment}      = $arr[4];
    $unpacked->{buffer_length}  = $arr[5];
    $unpacked->{buffer}         = $arr[6];
    return $unpacked;
}

sub deassemble_v2 {
    my ( $self, $packet ) = @_;
    my @arr = unpack( "n2 N n Z*", $packet );
    my $unpacked = {};
    $unpacked->{packet_version} = $arr[0];
    $unpacked->{packet_type}    = $arr[1];
    $unpacked->{crc32_value}    = $arr[2];
    $unpacked->{result_code}    = $arr[3];
    $unpacked->{buffer}         = $arr[4];
    return $unpacked;
}

sub packet_dump {
    my $packet = shift;
    my $i;
    my $k;
    my $dump;
    my $l;
    my $ascii;
    my $c;
    for ( $i = 0 ; $i < length($packet) ; $i += 16 ) {
        $l = $i + 16;
        if ( $l > length($packet) ) {
            $l = length($packet);
        }
        $dump = sprintf( "%04d - %04d: ", $i, $l );
        $ascii = "";
        for ( $k = $i ; $k < $l ; $k++ ) {
            $c = ( ord( substr( $packet, $k, 1 ) ) );
            $dump .= sprintf( "%02x ", $c );
            if ( ( $c >= 32 ) && ( $c <= 126 ) ) {
                $ascii .= chr($c);
            }
            else {
                $ascii .= ".";
            }
        }
        for ( $k = 0 ; $k < ( $i + 16 - $l ) ; $k++ ) {
            $dump .= "   ";
        }
        print( "packet_dump() " . $dump . " [" . $ascii . "]" . "\n" );
    }
}

1;