Skip to content

Commit

Permalink
amicli.c: Add sample program for debugging AMI actions.
Browse files Browse the repository at this point in the history
This adds a sample program that can be used to easily
interactively run raw AMI commands and see the responses.

Some changes have also been made to libcami to increase
robustness when an action is provided with line endings
that do not conform to what is expected.
  • Loading branch information
InterLinked1 committed Dec 27, 2023
1 parent 84bdf33 commit 073bc59
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ jobs:
cd cami
make
make install
make example
make examples
./simpleami
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CC = gcc
CFLAGS = -Wall -Werror -Wno-unused-parameter -Wextra -Wstrict-prototypes -Wmissing-prototypes -Wdeclaration-after-statement -Wmissing-declarations -Wmissing-format-attribute -Wformat=2 -Wshadow -std=gnu99 -pthread -O3 -g -Wstack-protector -fno-omit-frame-pointer -D_FORTIFY_SOURCE=2
EXE = cami
SAMPEXES = simpleami amicli
LIBNAME = libcami
LIBS = -lm
RM = rm -f
Expand All @@ -28,11 +29,16 @@ install:
mkdir -p /usr/include/$(EXE)
$(INSTALL) -m 755 include/*.h "/usr/include/$(EXE)/"

example : library install simpleami.o
simpleami: library install simpleami.o
$(CC) $(CFLAGS) -o simpleami simpleami.o -l$(EXE) $(LIBS) -ldl

amicli: library install amicli.o
$(CC) $(CFLAGS) -o amicli amicli.o -l$(EXE) $(LIBS) -ldl

examples : $(SAMPEXES)

clean :
$(RM) *.i *.o $(EXE)
$(RM) *.i *.o $(EXE) $(SAMPEXES) $(LIBNAME).so

uninstall:
$(RM) /usr/lib/$(EXE).so
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ This program is an AMI library that is designed to be included in a C program to

For demonstration purposes, a simple standalone program is also included. It is recommended that you consult this for an overview of how C-AMI can be used.

Assuming you have `gcc`, simply clone the repository and run `make example` to compile the demo program with C-AMI.
Assuming you have `gcc`, simply clone the repository and run `make examples` to compile the demo programs with C-AMI.

(You will likely want to update the connection details for your Asterisk system). Then run `./cami` to run.
(You will likely want to update the connection details for your Asterisk system). Then run `./simpleami` to run.

## Contributions

Expand Down
214 changes: 214 additions & 0 deletions amicli.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* CAMI -- C Asterisk Manager Interface "Simple AMI" demo
*
* Copyright (C) 2022, Naveen Albert
*
* Naveen Albert <asterisk@phreaknet.org>
*
* This program is free software, distributed under the terms of
* the Mozilla Public License Version 2.
*/

/*! \file
*
* \brief C Asterisk Manager Interface "Simple CLI"
*
* \author Naveen Albert <asterisk@phreaknet.org>
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#include <cami/cami.h>

/*
* This is a simple program that will use C AMI to log in,
* and then accepts AMI commands on STDIN
* and outputs responses to STDOUT.
* This is useful for debugging AMI commands and responses.
* Raw AMI commands are accepted, but do not include ActionID.
*/

/*! \brief Callback function executing asynchronously when new events are available */
static void ami_callback(struct ami_event *event)
{
const char *eventname = ami_keyvalue(event, "Event");
printf("(Callback) Event Received: %s\n", eventname);
#ifdef PRINT_EVENTS
ami_dump_event(event); /* Do something with event */
#endif
ami_event_free(event); /* Free event when done with it */
}

static void ami_disconnect_callback(void)
{
printf("(Callback) AMI was forcibly disconnected...\n");
exit(EXIT_FAILURE);
}

static int single_ami_command(void)
{
char action[64];
char buf[8192];
char *pos;
size_t left;
ssize_t res;
struct ami_response *resp;

/* Read a full AMI command,
* then send it all at once with proper CR LF delimiters. */

/* Read Action name */
pos = action;
left = sizeof(action);
res = getline(&pos, &left, stdin);
if (res <= 0) {
return res;
}

/* Remove newline from action */
pos = strchr(pos, '\n');
if (!pos) {
return -1;
}
*pos = '\0';

/* Get rid of Action: */
pos = action;
pos = strchr(pos, ':');
if (!pos) {
fprintf(stderr, "Must begin with 'Action:'\n");
return -1;
}

pos++;
while (isspace(*pos)) {
pos++;
}

memmove(action, pos, strlen(pos) + 1);

/* Read remainder of command */
pos = buf;
left = sizeof(buf);
for (;;) {
res = getline(&pos, &left, stdin);
if (res <= 0) {
return res;
}
if (!strncasecmp(pos, "ActionID:", strlen("ActionID:"))) {
continue; /* Ignore ActionID, since CAMI will autoadd it */
}
pos += res;
left -= res;
if (*(pos - 1) == '\n' && res > 1 && *(pos - 2) != '\r') {
/* Change LF endings into CR LF endings */
*(pos - 1) = '\r';
*pos = '\n';
if (!--left) {
fprintf(stderr, "Buffer exhaustion\n");
return -1; /* No room for NUL */
}
pos++;
*pos = '\0';
}
if (res <= 2) {
break; /* Empty line signals end of action */
}
}

/* Remove final CR LF, since CAMI will add that. */
pos = strrchr(buf, '\r');
if (pos) {
*pos = '\0';
}

resp = ami_action(action, "%s", buf);
if (!resp) {
fprintf(stderr, "AMI action '%s' failed\n", action);
return -1;
}
ami_dump_response(resp);
ami_resp_free(resp); /* Free response when done with it (just LF or CR LF) */
return 1;
}

int main(int argc,char *argv[])
{
char c;
static const char *getopt_settings = "?dhl:p:u:";
char ami_host[92] = "127.0.0.1"; /* Default to localhost */
char ami_username[64] = "";
char ami_password[64] = "";
int debug = 0;

ami_set_debug(STDERR_FILENO);

while ((c = getopt(argc, argv, getopt_settings)) != -1) {
switch (c) {
case '?':
case 'd':
debug++;
break;
case 'h':
fprintf(stderr, "amicli [options]\n");
fprintf(stderr, " -l hostname (default: 127.0.0.1)\n");
fprintf(stderr, " -p password (default: read from /etc/asterisk/manager.conf if loopback)\n");
fprintf(stderr, " -u username\n");
return 0;
case 'l':
strncpy(ami_host, optarg, sizeof(ami_host) - 1);
ami_host[sizeof(ami_host) - 1] = '\0';
break;
case 'p':
strncpy(ami_password, optarg, sizeof(ami_password) - 1);
ami_password[sizeof(ami_password) - 1] = '\0';
break;
case 'u':
strncpy(ami_username, optarg, sizeof(ami_username) - 1);
ami_username[sizeof(ami_username) - 1] = '\0';
break;
default:
fprintf(stderr, "Invalid option: %c\n", c);
return -1;
}
}

if (debug > 10) {
debug = 10;
}
ami_set_debug_level(debug);

if (ami_username[0] && !ami_password[0] && !strcmp(ami_host, "127.0.0.1")) {
/* If we're running as a privileged user with access to manager.conf, grab the password ourselves, which is more
* secure than getting as a command line arg from the user (and kind of convenient)
* Not that running as a user with access to the Asterisk config is great either, but, hey...
*/
if (ami_auto_detect_ami_pass(ami_username, ami_password, sizeof(ami_password))) {
fprintf(stderr, "No password specified, and failed to autodetect from /etc/asterisk/manager.conf\n");
return -1;
}
}

if (!ami_username[0]) {
fprintf(stderr, "No username provided (use -u flag)\n");
return -1;
}

if (ami_connect(ami_host, 0, ami_callback, ami_disconnect_callback)) {
return -1;
}
if (ami_action_login(ami_username, ami_password)) {
fprintf(stderr, "Failed to log in with username %s\n", ami_username);
return -1;
}

fprintf(stderr, "*** Successfully logged in to AMI on %s (%s) ***\n", ami_host, ami_username);
while (single_ami_command() > 0);
ami_disconnect();

return 0;
}
10 changes: 10 additions & 0 deletions cami.c
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,16 @@ static int __attribute__ ((format (gnu_printf, 3, 4))) __ami_send(va_list ap, co
/* Shouldn't happen if everything else is correct, but if message wasn't properly terminated, it won't get processed. Fix it to force it to go through. */
ami_debug(1, "Yikes! AMI action wasn't properly terminated!\n"); /* This means there's a bug somewhere else. */
}
while (len >= 5 && (*(fullbuf + len - 5) == '\r' || *(fullbuf + len - 5) == '\n')) {
/* Asterisk will stop parsing this message after two CR LF sequences,
* and anything afterwards will (fail to) be interpreted as the next message.
* This will throw off synchronization so is very bad.
* This is user error, but we can correct it by removing the erroneous CR LF's,
* or other possibly malformed line endings, including stray LFs, etc. */
ami_debug(1, "WARNING: Too many line endings at end of action. Autocorrecting...\n");
len--;
strcpy(fullbuf + len - 4, AMI_EOM); /* Safe */
}

ami_debug(4, "==> AMI Action:\n%s", fullbuf); /* There's already (multiple) new lines at the end, don't add more */
bytes = write(ami_pipe[1], fullbuf, len);
Expand Down

0 comments on commit 073bc59

Please sign in to comment.