The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package BGPmon::Fetch::Client;
our $VERSION = '1.09';

use 5.006;
use strict;
use warnings;
use IO::Socket;
use IO::Select;
use POSIX qw/strftime/;

require Exporter;
our $AUTOLOAD;
our @ISA = qw(Exporter);
#our %EXPORT_TAGS = ( 'all' => [ qw(connect_bgpmon read_xml_message close_connection is_connected messages_read uptime connection_endtime) ] );
#our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT_OK = qw(connect_bgpmon read_xml_message close_connection is_connected messages_read uptime connection_endtime init_bgpdata get_error_code get_error_message get_error_msg set_timeout);

# connection status
our $msgs_read = 0;
our $start_time = time;
our $end_time = time;
our $connected = 0;

# socket and persistent buffer for storing partial messages
our $sock;
our $sel;
our $read_buffer = "";
our $chunksize = 1024;
our $socket_timeout = 600; # Socket times out after 10 min.
our %socket_buffer = ();

# Variables to store error codes and messages.
my %error_code;
my %error_msg;

# Error messages
use constant NO_ERROR_CODE => 0;
use constant NO_ERROR_MSG => 'No error.';
use constant CONNECT_ERROR_CODE => 201;
use constant CONNECT_ERROR_MSG => 'Could not connect to BGPmon.';
use constant NOT_CONNECTED_CODE => 202;
use constant NOT_CONNECTED_MSG => 'Not connected to a BGPmon instance.';
use constant ALREADY_CONNECTED_CODE => 203;
use constant ALREADY_CONNECTED_MSG => 'Already connected to a BGPmon instance.';
use constant XML_MSG_READ_ERROR_CODE => 204;
use constant XML_MSG_READ_ERROR_MSG => 'Error reading XML message from stream.';
use constant CONNECTION_CLOSED_CODE => 205;
use constant CONNECTION_CLOSED_MSG => 'Connection to BGPmon closed.';
use constant INVALID_NUM_BYTES_ERROR_CODE => 206;
use constant INVALID_NUM_BYTES_ERROR_MSG => 'Attempting to read invalid number of bytes.';
use constant READ_ERROR_CODE => 207;
use constant READ_ERROR_MSG => 'Could not read from file handle. Error in sysread.';
use constant SOCKET_TIMEOUT_CODE => 208;
use constant SOCKET_TIMEOUT_MSG => 'Socket timed out.';
use constant ARGUMENT_ERROR_CODE => 297;
use constant ARGUMENT_ERROR_MSG => 'Invalid number of arguments.';
use constant INVALID_FUNCTION_SPECIFIED_CODE => 298;
use constant INVALID_FUNCTION_SPECIFIED_MSG => 'Invalid function name specified.';
use constant UNKNOWN_ERROR_CODE => 299;
use constant UNKNOWN_ERROR_MSG => 'Unknown error occurred.';

# Initially, all functions are error-free.
my @functions = qw(init_bgpdata connect_bgpmon read_xml_message read_n_bytes close_connection is_connected messages_read uptime connection_endtime);
for my $function (@functions) {
    $error_code{$function} = NO_ERROR_CODE;
    $error_msg{$function} = NO_ERROR_MSG;
}

=head1 NAME

BGPmon::Fetch::Client

The BGPmon Client module, to connect to BGPmon and receive XML messages one at a time.

=cut

=head1 SYNOPSIS

The BGPmon::Client module provides functionality to connect to a bgpmon instance and read one XML message at a time.

    use BGPmon::Fetch::Client;
    my $ret = connect_bgpmon();
    set_timeout($time_out_seconds);
    my $xml_msg = read_xml_message();
    my $ret = is_connected();
    my $num_read = messages_read();
    my $uptime = uptime();
    my $ret = close_connection();
    my $downtime = connection_endtime();
=cut

=head1 EXPORT

init_bgpdata
connect_bgpmon
set_timeout
read_xml_message
close_connection
is_connected
messages_read
uptime
connection_endtime

=cut

=head1 SUBROUTINES/METHODS

=head2 init_bgpdata

Initializes the client module. Takes no arguments.

=cut

sub init_bgpdata {
    return 1;
}

=head2 set_timeout

Sets the socket timeout value in seconds. Takes one argument - the timeout value in seconds.

=cut

sub set_timeout {
    $socket_timeout = shift;
}

=head2 connect_bgpmon

This function connects to the BGPmon server. If the connection succeeds, the function
attempts to read the starting <xml> tag from the BGPmon server. If that succeeds, the
created socket is returned. If not, the function returns undef.

=cut

sub connect_bgpmon {
    my ($server, $port) = @_;
    my $fname = 'connect_bgpmon';

    if ($connected == 1) {
        $error_code{$fname} = ALREADY_CONNECTED_CODE;
        $error_msg{$fname} = ALREADY_CONNECTED_MSG;
        return -1;
    }

    # Connect to the BGPmon instance to receive XML xml_stream
    $sock = new IO::Socket::INET(PeerAddr => $server, PeerPort => $port, Proto => 'tcp');
    if(!defined $sock) {
        $error_code{$fname} = CONNECT_ERROR_CODE;
        $error_msg{$fname} = CONNECT_ERROR_MSG;
        return -2;
    }

    # Create select object, to be used later.
    $sel = new IO::Select->new();
    $sel->add($sock);

    my $data;
    my @ready = $sel->can_read($socket_timeout);
    if (scalar(@ready) > 0) { #We have data on socket.
        $data = read_n_bytes(5);
        if (!defined $data) {
            $error_code{$fname} = XML_MSG_READ_ERROR_CODE;
            $error_msg{$fname} = XML_MSG_READ_ERROR_MSG;
            $sel->remove($sock);
            close($sock);
            return -3;
        }
        if ($data ne "<xml>") {
            $error_code{$fname} = XML_MSG_READ_ERROR_CODE;
            $error_msg{$fname} = XML_MSG_READ_ERROR_MSG;
            $sel->remove($sock);
            close($sock);
            return -4;
        }
        $connected = 1;
        $start_time = time;
        $msgs_read = 0;

        $error_code{$fname} = NO_ERROR_CODE;
        $error_msg{$fname} = NO_ERROR_MSG;
        return 0;
    }

    # Socket timed out.
    $error_code{$fname} = SOCKET_TIMEOUT_CODE;
    $error_msg{$fname} = SOCKET_TIMEOUT_MSG;
    $sel->remove($sock);
    close($sock);
    return -5;
}

=head2 read_xml_message

This function reads one xml message at a time from the BGPmon XML stream.

=cut

sub read_xml_message {
    my $complete_xml_msg = "";
    my $fname = 'read_xml_message';

    # Check if we are connected to a server
    unless ($connected == 1) {
        $error_code{$fname} = NOT_CONNECTED_CODE;
        $error_msg{$fname} = NOT_CONNECTED_MSG;
        return undef;
    }

    my $tempbuf;
    my $bytesread = 0;
    my @ready = $sel->can_read($socket_timeout);

    # If there is a socket with data.
    if (scalar(@ready) > 0) {
        while ($read_buffer !~ /<BGP_MESSAGE.*?BGP_MESSAGE>/) {
            $bytesread = sysread($sock, $tempbuf, $chunksize);
            if (!defined $bytesread) {
                $error_code{$fname} = XML_MSG_READ_ERROR_CODE;
                $error_msg{$fname} = XML_MSG_READ_ERROR_MSG;
                close_connection();
                return undef;
            }
            if ($bytesread == 0) {
                $error_code{$fname} = CONNECTION_CLOSED_CODE;
                $error_msg{$fname} = CONNECTION_CLOSED_MSG;
                close_connection();
                return undef;
            }
            $read_buffer .= $tempbuf;
        }

        # at this point I have a complete message OR my socket closed
        if ($read_buffer =~ /^(<BGP_MESSAGE.*?BGP_MESSAGE>)/) {
            $complete_xml_msg = $1;
            $msgs_read++;
            $read_buffer =~ s/^<BGP_MESSAGE.*?BGP_MESSAGE>//;
        } else {
            $error_code{$fname} = UNKNOWN_ERROR_CODE;
            $error_msg{$fname} = UNKNOWN_ERROR_MSG;
        }
        return $complete_xml_msg;
    }
    # Timeout.
    $error_code{$fname} = SOCKET_TIMEOUT_CODE;
    $error_msg{$fname} = SOCKET_TIMEOUT_MSG;
    return undef;
}

=head2 close_connection

Function to close open files and sockets.

=cut

sub close_connection {
    $connected = 0;
    $end_time = time;
    if ($sock) {
        $sel->remove($sock);
        $sock->close();
    }
}

=head2 is_connected

Function to report whether currently connected to BGPmon.

=cut

sub is_connected {
    return $connected;
}

=head2 messages_read

Get number of messages read.

=cut

sub messages_read {
    return $msgs_read;
}

=head2 uptime

Returns number of seconds the connection has been up.
If the connection is down, return 0.

=cut

sub uptime {
    if ($connected) {
        return time() - $start_time;
    }
    return 0;

}

=head2 connection_endtime

Returns the time the connection ended .
If the connection is up, return 0.

=cut

sub connection_endtime {
    if ($connected) {
        return 0;
    }
    return $end_time;

}

=head2 read_n_bytes

This function reads exactly n bytes from a connection.
returns undef on any error or connection close

=cut

sub read_n_bytes {
    my ($bytesneeded) = shift;
    my $fname = 'read_n_bytes';
    if ($bytesneeded < 0) {
        $error_code{$fname} = INVALID_NUM_BYTES_ERROR_CODE;
        $error_msg{$fname} = INVALID_NUM_BYTES_ERROR_MSG;
        return undef;
    }
    my $data = "";
    my $buf = "";
    my $bytesread = 0;
    while($bytesneeded > 0) {
        $bytesread = sysread($sock, $buf, $bytesneeded);
        if (!defined $bytesread) {
            $error_code{$fname} = READ_ERROR_CODE;
            $error_msg{$fname} = READ_ERROR_MSG . ": $!";
            return undef;
        }
        if ($bytesread == 0) {
            $error_code{$fname} = CONNECTION_CLOSED_CODE;
            $error_msg{$fname} = CONNECTION_CLOSED_MSG;
            return undef;
        }
        $data .= $buf;
        $bytesneeded = $bytesneeded - $bytesread;
    }
    return $data;
}

=head2 get_error_code

Get the error code for a given function
Input : the name of the function whose error code we should report
Output: the function's error code
        or ARGUMENT_ERROR if the user did not supply a function
        or INVALID_FUNCTION_SPECIFIED if the user provided an invalid function
Usage:  my $err_code = get_error_code("connect_archive");

=cut
sub get_error_code {
    my $function = shift;
    unless (defined $function) {
        return ARGUMENT_ERROR_CODE;
    }

    return $error_code{$function} if (defined $error_code{$function});
    return INVALID_FUNCTION_SPECIFIED_CODE;
}

=head2 get_error_message {

Get the error message of a given function
Input : the name of the function whose error message we should report
Output: the function's error message
        or ARGUMENT_ERROR if the user did not supply a function
        or INVALID_FUNCTION_SPECIFIED if the user provided an invalid function
Usage:  my $err_msg = get_error_message("read_xml_message");

=cut
sub get_error_message {
    my $function = shift;
    unless (defined $function) {
        return ARGUMENT_ERROR_MSG;
    }

    return $error_msg{$function} if (defined $error_msg{$function});
    return INVALID_FUNCTION_SPECIFIED_MSG;
}

=head2 get_error_msg

Shorthand call for get_error_message

=cut

sub get_error_msg{
    my $fname = shift;
    return get_error_message($fname);
}

=head1 AUTHOR

Kaustubh Gadkari, C<< <kaustubh at cs.colostate.edu> >>

=head1 BUGS

Please report any bugs or feature requests to C<bgpmon at netsec.colostate.edu>, or through
the web interface at L<http://bgpmon.netsec.colostate.edu>.


=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc BGPmon::Client

=cut

=head1 LICENSE AND COPYRIGHT
Copyright (c) 2012 Colorado State University

    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or
    sell copies of the Software, and to permit persons to whom
    the Software is furnished to do so, subject to the following
    conditions:

    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.\


  File: Client.pm
  Authors: Kaustubh Gadkari, Dan Massey, Cathie Olschanowsky
  Date: May 21, 2012

=cut
1; # End of BGPmon::Client