Articles

Bami: A Proof of concept asterisk manager interface (AMI) client written in bash


Introduction

In this article I explained the inners of the AMI protocol, and talked about actions, events, and responses.

As a complement of that article I wrote (just for fun) a little shell script called Bami, which stands for Bash Asterisk Manager Interface, which will give you a very quick start on actually doing/testing stuff. Moreover, it might even be useful from time to time! The complete source code is available at github.

But remember, this is just an example and a script written for fun. If you are looking for a more serious piece of work, I encourage you to take a look at PAMI (which is written in PHP), and Nami (in Javascript, for nodejs).

Configuring Bami

Edit bami.sh and setup log path, and AMI username and password.

Running Bami

You will need something like netcat. Example:

nc 127.0.0.1 5038 -e ./bami.sh

Logging

Doing a tail -f of the configured logfile will show something like:

09/08/11 21:46:40 - DEBUG - Event: Newstate Privilege: call,all Channel: DAHDI/x-1 ChannelState: 5 ChannelStateDesc: Ringing CallerIDNum: x CallerIDName: Uniqueid: 1315529123.320110
09/08/11 21:46:40 - INFO - === Unhandled event: Newstate ===
09/08/11 21:46:40 - DEBUG - Event: DTMF Privilege: dtmf,all Channel: DAHDI/x-1 Uniqueid: 1315529106.320100 Digit: 6 Direction: Received Begin: No End: Yes
09/08/11 21:46:40 - INFO - DTMF
09/08/11 21:46:40 - DEBUG - Event: DTMF Privilege: dtmf,all Channel: DAHDI/x-1 Uniqueid: 1315529097.320090 Digit: 0 Direction: Received Begin: Yes End: No
09/08/11 21:46:40 - INFO - DTMF

How it works

Using netcat allow us to communicate through a TCP/IP connection transparently. So the idea is to use netcat to connect to AMI and link the standard input and standard error (stdin and stdout) to this socket (connection).

We're going to read line by line, delimited by \r\n, so we're going to use bash read. Since End-Of-Message delimiter ir \r\n\r\n, this will result in read returning an empty line. So we use this as a sign that a complete message has been read.

We need to login, this means to send the Login action. We'll use bash printf for that, so we can properly write the needed \r\n's.

Once logged in, events start to show up, so we have to read them (once again, reading line by line, and stopping once the eom -empty line- is detected).

Then we can easily recognice the name of the event that has arrived, with a simple bash case.

I decided to stop there, because I think the point is made. So any optimizations and improvements (and actual real features) are an excercise to the reader :) (i.e: sending an action and receiving the response when having events associated to that response, etc).


The Code

First, the logging functions. We'll use these across Bami to log messages to a file:

# Generic function for logging
function log() {
    echo `date "+%D %T - "` "${@}" >> ${log}
}

# Will call log() with a INFO prefix
function info() {
    log "INFO - ${@}"
}

# Will call log() with a DEBUG prefix
function debug() {
    log "DEBUG - ${@}"
}

# Will call log() with a ERROR prefix
function error() {
    log "ERROR - ${@}"
}

Great, nothing special there. Let's now take a look at how to read line by line and a complete PDU (Protocol Data Unit, I felt like calling it this way, it should rather be just "data" or "message"):

# Reads a line from AMI (stdin), will strip \r
function readLine() {
    local line=""
    read line
    line=`echo ${line} | tr -d "\r"`
    echo ${line}
}

# Reads a full PDU from asterisk. Since Asterisk messages
# ends with a \r\n\r\n, and we use "read" to read line by
# line, this will translate to an empty line delimiting
# PDUs. So read up to an empty line, and return whatever
# read.
function readPdu() {
    local pdu=""
    local complete=0
    while [ ${complete} -eq "0" ]; do
        line=`readLine`
        # End Of Message detected
        if [ -z ${line} ]; then
            complete=1
        else
            # Concat line read
            pdu=`printf "${pdu}\\\n${line}"`
        fi
    done
    echo ${pdu}
}

Now, the code needed to actually send an action, in this case, Login

# Performs a Login action. Will terminate with error code 255 if
# the asterisk ami welcome message is not found.
function login() {
    local welcome=`readLine`
    if [ ${welcome} != "Asterisk Call Manager/1.1" ]; then
        error "Invalid peer. Not AMI."
        exit 255
    fi
    printf "Action: Login\r\nUsername: ${user}\r\nSecret: ${pass}\r\n\r\n"
    local response=`readPdu`
    if [[ ! ${response} =~ Success ]]; then
        error "Could not login: ${response}"
        exit 254
    fi
}
# Do login.
login

Now, the main reading loop, which reads event and acts accordingly:

# Main reading loop.
while [ true ]; do
    pdu=`readPdu`
    debug "${pdu}"
    $regex="Event: *"
    if [[ $pdu =~ $regex ]]; then
        eventName=`echo ${pdu} | cut -d' ' -f2`
        case ${eventName} in
            DTMF)
                info DTMF
            ;;
            VarSet)
            ;;
            Hangup)
            ;;
            Dial)
                info Dial
            ;;
            *)
                info "=== Unhandled event: ${eventName} ==="
            ;;
        esac
    else
        info "Response: ${pdu}"
    fi
done

Conclusions

As you can see, AMI is very easy to work with. It has it falls, but being a text protocol with a very simple spec (maybe too simple.. ) will allow you to actually do a client in whatever language you'd like.