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.