Asterisk-AMI

 view release on metacpan or  search on metacpan

lib/Asterisk/AMI.pm  view on Meta::CPAN

#!/usr/bin/perl

=head1 NAME

Asterisk::AMI - Perl module for interacting with the Asterisk Manager Interface

=head1 VERSION

0.2.8

=head1 SYNOPSIS

        use Asterisk::AMI;
        my $astman = Asterisk::AMI->new(PeerAddr => '127.0.0.1',
                                        PeerPort => '5038',
                                        Username => 'admin',
                                        Secret => 'supersecret'
                                );
        
        die "Unable to connect to asterisk" unless ($astman);

        my $action = $astman->({ Action => 'Command',
                                 Command => 'sip show peers'
                                });

=head1 DESCRIPTION

This module provides an interface to the Asterisk Manager Interface. It's goal is to provide a flexible, powerful, and 
reliable way to interact with Asterisk upon which other applications may be built. It utilizes AnyEvent and therefore 
can integrate very easily into event-based applications, but it still provides blocking functions for us with standard 
scripting.

=head2 SSL SUPPORT INFORMATION

For SSL support you will also need the module that AnyEvent::Handle uses for SSL support, which is not a required 
dependency. Currently that module is 'Net::SSLeay' (AnyEvent:Handle version 5.251) but it may change in the future.

=head3 CentOS/Redhat

If the version of Net:SSLeay included in CentOS/Redhat does not work try installing an updated version from CPAN.

=head2 Constructor

=head3 new([ARGS])

Creates a new AMI object which takes the arguments as key-value pairs.

        Key-Value Pairs accepted:
        PeerAddr        Remote host address        <hostname>
        PeerPort        Remote host port        <service>
        Events Enable/Disable Events 'on'|'off'
        Username        Username to access the AMI
        Secret Secret used to connect to AMI
        AuthType        Authentication type to use for login        'plaintext'|'MD5'
        UseSSL Enables/Disables SSL for the connection 0|1
        BufferSize        Maximum size of buffer, in number of actions
        Timeout Default timeout of all actions in seconds
        Handlers        Hash reference of Handlers for events        { 'EVENT' => \&somesub };
        Keepalive        Interval (in seconds) to periodically send 'Ping' actions to asterisk
        TCP_Keepalive        Enables/Disables SO_KEEPALIVE option on the socket        0|1
        Blocking        Enable/Disable blocking connects        0|1
        on_connect        A subroutine to run after we connect
        on_connect_err        A subroutine to call if we have an error while connecting
        on_error        A subroutine to call when an error occurs on the socket
        on_disconnect        A subroutine to call when the remote end disconnects
        on_timeout        A subroutine to call if our Keepalive times out
        OriginateHack        Changes settings to allow Async Originates to work 0|1

        'PeerAddr' defaults to 127.0.0.1.
        'PeerPort' defaults to 5038.
        'Events' default is 'off'. May be anything that the AMI will accept as a part of the 'Events' parameter for the
        login action.
        'Username' has no default and must be supplied.
        'Secret' has no default and must be supplied.
        'AuthType' sets the authentication type to use for login. Default is 'plaintext'.  Use 'MD5' for MD5 challenge
        authentication.
        'UseSSL' defaults to 0 (no ssl). When SSL is enabled the default PeerPort changes to 5039.
        'BufferSize' has a default of 30000. It also acts as our max actionid before we reset the counter.
        'Timeout' has a default of 0, which means no timeout on blocking.
        'Handlers' accepts a hash reference setting a callback handler for the specified event. EVENT should match
        the contents of the {'Event'} key of the event object will be. The handler should be a subroutine reference that
        will be passed the a copy of the AMI object and the event object. The 'default' keyword can be used to set
        a default event handler. If handlers are installed we do not buffer events and instead immediately dispatch them.
        If no handler is specified for an event type and a 'default' was not set the event is discarded.
        'Keepalive' only works when running with an event loop. Used with on_timeout, this can be used to detect if
        asterisk has become un-responsive.
        'TCP_Keepalive' default is disabled. Activates the tcp keep-alive at the socket layer. This does not require
        an event-loop and is lightweight. Useful for applications that use long-lived connections to Asterisk but
        do not run an event loop.
        'Blocking' has a default of 1 (block on connecting). A value of 0 will cause us to queue our connection
        and login for when an event loop is started. If set to non blocking we will always return a valid object.

        'on_connect' is a subroutine to call when we have successfully connected and logged into the asterisk manager.
        it will be passed our AMI object.

        'on_connect_err', 'on_error', 'on_disconnect'
        These three specify subroutines to call when errors occur. 'on_connect_err' is specifically for errors that
        occur while connecting, as well as failed logins. If 'on_connect_err' or 'on_disconnect' it is not set,
        but 'on_error' is, 'on_error' will be called. 'on_disconnect' is not reliable, as disconnects seem to get lumped
        under 'on_error' instead. When the subroutine specified for any of theses is called the first argument is a copy
        of our AMI object, and the second is a string containing a message/reason. All three of these are 'fatal', when
        they occur we destroy our buffers and our socket connections.

        'on_timeout' is called when a keep-alive has timed out, not when a normal action has. It is non-'fatal'.
        The subroutine will be called with a copy of our AMI object and a message.

        'OriginateHack' defaults to 0 (off). This essentially enables 'call' events and says 'discard all events
        unless the user has explicitly enabled events' (prevents a memory leak). It does its best not to mess up
        anything you have already set. Without this, if you use 'Async' with an 'Originate' the action will timeout
        or never callback. You don't need this if you are already doing work with events, simply add 'call' events
        to your eventmask.

=head2 Disabling Warnings

        If you have warnings enabled this module will emit a number of them on connection errors, deprecated features, etc.
        To disable this but still have all other warnings in perl enabled you can do the following:

                use Asterisk::AMI;
                use warnings;
                no warnings qw(Asterisk::AMI);

        That will enable warnings but disable any warnings from this module.

=head2 Warning - Mixing Event-loops and blocking actions

        For an intro to Event-Based programming please check out the documentation in AnyEvent::Intro.

        If you are running an event loop and use blocking methods (e.g. get_response, check_response, action,
        simple_action, connected, or a blocking connect) the outcome is unspecified. It may work, it may lock everything up, the action may
        work but break something else. I have tested it and behavior seems unpredictable at best and is very
        circumstantial.

        If you are running an event-loop use non-blocking callbacks! It is why they are there!

        However if you do play with blocking methods inside of your loops let me know how it goes.

=head2 Actions

=head3 ActionIDs

This module handles ActionIDs internally and if you supply one in an action it will simply be ignored and overwritten.

=head3 Construction

No matter which method you use to send an action (send_action(), simple_action(), or action()), they all accept 
actions in the same format, which is a hash reference. The only exceptions to this rules are when specifying a 
callback and a callback timeout, which only work with send_action.

To build and send an action you can do the following:

        %action = ( Action => 'Command',
                    Command => 'sip show peers'
                );

        $astman->send_action(\%action);

Alternatively you can also do the following to the same effect:

        $astman->send_action({  Action => 'Command',
                                Command => 'sip show peers'
                                });

Additionally the value of the hash may be an array reference. When an array reference is used, every value in the 
array is append as a different line to the action. For example:

lib/Asterisk/AMI.pm  view on Meta::CPAN

                                        #lowercase it for consistency
                                        $val = $lval;
                                }
                        }

                #Ensure all handlers are sub refs
                } elsif ($opt eq 'HANDLERS') {
                        while (my ($event, $handler) = each %{$val}) {
                                if (ref($handler) ne 'CODE') {
                                        carp "Handler for event type \'$event\' must be an anonymous subroutine or a subroutine reference" if warnings::enabled('Asterisk::AMI');
                                        return;
                                }
                        }
                }

                $self->{CONFIG}->{$opt} = $val;
        }


        #Check for required options
        foreach my $req (@required) {
                if (!exists $self->{CONFIG}->{$req}) {
                        carp "Must supply a username and secret for connecting to asterisk" if warnings::enabled('Asterisk::AMI');
                        return;
                }
        }

        #Change default port if using ssl
        if ($self->{CONFIG}->{USESSL}) {
                $defaults{PEERPORT} = 5039;
        }

        #Assign defaults for any missing options
        while (my ($opt, $val) = each(%defaults)) {
                if (!defined $self->{CONFIG}->{$opt}) {
                        $self->{CONFIG}->{$opt} = $val;
                }
        }

        #Make adjustments for Originate Async bullscrap
        if ($self->{CONFIG}->{ORIGINATEHACK}) {
                #Turn on call events, otherwise we wont get the Async response
                if (lc($self->{CONFIG}->{EVENTS}) eq 'off') {
                        $self->{CONFIG}->{EVENTS} = 'call';
                        #Fake event type so that we will discard events, else by turning on events our event buffer 
                        #Will just continue to fill up.
                        $self->{CONFIG}->{HANDLERS} = { 'JUSTMAKETHEHASHNOTEMPTY' => sub {} } unless ($self->{CONFIG}->{HANDLERS});
                #They already turned events on, just add call types to it, assume they are doing something with events 
                #and don't mess with the handlers
                } elsif (lc($self->{CONFIG}->{EVENTS}) !~ /on|call/x) {
                        $self->{CONFIG}->{EVENTS} .= ',call';
                }
        }

        #Initialize the seq number
        $self->{idseq} = 1;

        #Weaken reference for use in anonsub
        weaken($self);

        #Set keepalive
        $self->{CONFIG}->{KEEPALIVE} = AE::timer($self->{CONFIG}->{KEEPALIVE}, $self->{CONFIG}->{KEEPALIVE}, sub { $self->_send_keepalive }) if ($self->{CONFIG}->{KEEPALIVE});
        
        return 1;
}

#Handles connection failures (includes login failure);
sub _on_connect_err {

        my ($self, $message) = @_;

        warnings::warnif('Asterisk::AMI', "Failed to connect to asterisk - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
        warnings::warnif('Asterisk::AMI', "Error Message: $message");

        #Dispatch all callbacks as if they timed out
        $self->_clear_cbs();

        if (exists $self->{CONFIG}->{ON_CONNECT_ERR}) {
                $self->{CONFIG}->{ON_CONNECT_ERR}->($self, $message);
        } elsif (exists $self->{CONFIG}->{ON_ERROR}) {
                $self->{CONFIG}->{ON_ERROR}->($self, $message);
        }

        $self->{SOCKERR} = 1;

        $self->destroy();

        return;
}

#Handles other errors on the socket
sub _on_error {

        my ($self, $message) = @_;

        warnings::warnif('Asterisk::AMI', "Received Error on socket - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
        warnings::warnif('Asterisk::AMI', "Error Message: $message");
        
        #Call all cbs as if they had timed out
        $self->_clear_cbs();

        $self->{CONFIG}->{ON_ERROR}->($self, $message) if (exists $self->{CONFIG}->{ON_ERROR});
        
        $self->{SOCKERR} = 1;

        $self->destroy();

        return;
}

#Handles the remote end disconnecting
sub _on_disconnect {

        my ($self) = @_;

        my $message = "Remote end disconnected - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}";
        warnings::warnif('Asterisk::AMI', "Remote Asterisk Server ended connection - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");

        #Call all callbacks as if they had timed out
        _
        $self->_clear_cbs();

        if (exists $self->{CONFIG}->{ON_DISCONNECT}) {
                $self->{CONFIG}->{ON_DISCONNECT}->($self, $message);
        } elsif (exists $self->{CONFIG}->{ON_ERROR}) {
                $self->{CONFIG}->{ON_ERROR}->($self, $message);
        }

        $self->{SOCKERR} = 1;

        $self->destroy();

        return;
}

#What happens if our keep alive times out
sub _on_timeout {
        my ($self, $message) = @_;

        warnings::warnif('Asterisk::AMI', $message);

        if (exists $self->{CONFIG}->{ON_TIMEOUT}) {
                $self->{CONFIG}->{ON_TIMEOUT}->($self, $message);
        } elsif (exists $self->{CONFIG}->{ON_ERROR}) {
                $self->{CONFIG}->{ON_ERROR}->($self, $message);
        }

        $self->{SOCKERR} = 1;

        return;
}

#Things to do after our initial connect
sub _on_connect {

        my ($self, $fh, $line) = @_;

        if ($line =~ /^Asterisk\ Call\ Manager\/([0-9]\.[0-9])$/ox) {
                $self->{AMIVER} = $1;
        } else {
                warnings::warnif('Asterisk::AMI', "Unknown Protocol/AMI Version from $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
        }

        #Weak reference for us in anonysub
        weaken($self);

        $self->{handle}->push_read( 'Asterisk::AMI' => sub { $self->_handle_packet(@_); } );

        return 1;
}

#Connects to the AMI Returns 1 on success, 0 on failure
sub _connect {
        my ($self) = @_;

        #Weaken ref for use in anonysub
        weaken($self);

        #Build a hash of our anyevent::handle options
        my %hdl = (     connect => [$self->{CONFIG}->{PEERADDR} => $self->{CONFIG}->{PEERPORT}],
                        on_connect_err => sub { $self->_on_connect_err($_[1]); },
                        on_error => sub { $self->_on_error($_[2]) },
                        on_eof => sub { $self->_on_disconnect; },
                        on_connect => sub { $self->{handle}->push_read( line => sub { $self->_on_connect(@_); } ); });

        #TLS stuff
        $hdl{'tls'} = 'connect' if ($self->{CONFIG}->{USESSL});
        #TCP Keepalive
        $hdl{'keeplive'} = 1 if ($self->{CONFIG}->{TCP_KEEPALIVE});

        #Make connection/create handle
        $self->{handle} = AnyEvent::Handle->new(%hdl);

        #Return login status if blocking
        return $self->_login if ($self->{CONFIG}->{BLOCKING});

        #Queue our login
        $self->_login;

        #If we have a handle, SUCCESS!
        return 1 if (defined $self->{handle});

        return;
}

sub _handle_packet {
        my ($self, $hdl, $buffer) = @_;

        foreach my $packet (split /\015\012\015\012/ox, $buffer) {
                my %parsed;

                foreach my $line (split /\015\012/ox, $packet) {
                        #Is this our command output?
                        if ($line =~ s/--END\ COMMAND--$//ox) {
                                $parsed{'COMPLETED'} = 1;

                                push(@{$parsed{'CMD'}},split(/\x20*\x0A/ox, $line));
                        } else {
                                #Regular output, split on :\
                                my ($key, $value) = split /:\ /x, $line, 2;

                                $parsed{$key} = $value;

                        }
                }

                #Dispatch depending on packet type
                if (exists $parsed{'ActionID'}) {
                        $self->_handle_action(\%parsed);
                } elsif (exists $parsed{'Event'}) {
                        $self->_handle_event(\%parsed);
                }
        }

        return 1;
}

#Used once and action completes
#Determines goodness and performs any oustanding callbacks
sub _action_complete {
        my ($self, $actionid) = @_;

        #Determine 'Goodness'
        if (defined $self->{RESPONSEBUFFER}->{$actionid}->{'Response'}
                && $self->{RESPONSEBUFFER}->{$actionid}->{'Response'} =~ /^(?:Success|Follows|Goodbye|Events Off|Pong)$/ox) {
                $self->{RESPONSEBUFFER}->{$actionid}->{'GOOD'} = 1;
        }

lib/Asterisk/AMI.pm  view on Meta::CPAN


        $self->destroy();

        #No socket? No Problem.
        return 1;
}

#Pops the topmost event out of the buffer and returns it Events are hash references
sub get_event {
        my ($self, $timeout) = @_;
        #my $timeout = $_[1];

        $timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);

        unless (defined $self->{EVENTBUFFER}->[0]) {

                my $process = AE::cv;

                $self->{CALLBACKS}->{'EVENT'}->{'cb'} = sub { $process->send($_[0]) };
                $self->{CALLBACKS}->{'EVENT'}->{'timeout'} = sub { warnings::warnif('Asterisk::AMI', "Timed out waiting for event"); $process->send(undef); };

                $timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);

                if ($timeout) {

                        #Make sure event loop is up to date in case of sleeps
                        AE::now_update;

                        $self->{CALLBACKS}->{'EVENT'}->{'timer'} = AE::timer $timeout, 0, $self->{CALLBACKS}->{'EVENT'}->{'timeout'};
                }

                return $process->recv;
        }

        return shift @{$self->{EVENTBUFFER}};
}

#Returns server AMI version
sub amiver {
        my ($self) = @_;
        return $self->{AMIVER};
}

#Checks the connection, returns 1 if the connection is good
sub connected {
        my ($self, $timeout) = @_;
        
        if ($self && $self->simple_action({ Action => 'Ping'}, $timeout)) {
                return 1;
        } 

        return 0;
}

#Check whether there was an error on the socket
sub error {
        my ($self) = @_;
        return $self->{SOCKERR};
}

#Sends a keep alive
sub _send_keepalive {
        my ($self) = @_;
        #Weaken ref for use in anonysub
        weaken($self);
        my $cb = sub { 
                        unless ($_[1]->{'GOOD'}) {
                                $self->_on_timeout("Asterisk failed to respond to keepalive - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
                        };
                 };

        my $timeout = $self->{CONFIG}->{TIMEOUT} || 5;
        
        return $self->send_action({ Action => 'Ping' }, $cb, $timeout);
}

#Calls all callbacks as if they had timed out Used when an error has occured on the socket
sub _clear_cbs {
        my ($self) = @_;

        foreach my $id (keys %{$self->{CALLBACKS}}) {
                my $response = $self->{RESPONSEBUFFER}->{$id};
                my $callback = $self->{CALLBACKS}->{$id}->{'cb'};
                my $store = $self->{CALLBACKS}->{$id}->{'store'};
                delete $self->{RESPONSEBUFFER}->{$id};
                delete $self->{CALLBACKS}->{$id};
                delete $self->{EXPECTED}->{$id};
                $callback->($self, $response, $store);
        }

        return 1;
}

#Cleans up
sub destroy {
        my ($self) = @_;

        $self->DESTROY;

        bless $self, "Asterisk::AMI::destroyed";

        return 1;
}

#Run event loop via anyevent
sub loop {
	$_[0]->{tmp_loop} = AnyEvent->condvar;
	my $exit = $_[0]->{tmp_loop}->recv;
	delete $_[0]->{tmp_loop};
	return $exit;
}

#Ends exits loops
sub break {
	if (defined $_[0]->{tmp_loop}) {
		$_[0]->{tmp_loop}->send(1);
	}
}

#Bye bye
sub DESTROY {
        my ($self) = @_;

        #Logoff if we are not in error
        if (!$self->{SOCKERR} && $self->{LOGGEDIN}) {
                $self->send_action({ Action => 'Logoff' });
                undef $self->{LOGGEDIN};
        }



( run in 0.937 second using v1.01-cache-2.11-cpan-df04353d9ac )