summaryrefslogtreecommitdiff
path: root/cel
diff options
context:
space:
mode:
authorRussell Bryant <russell@russellbryant.com>2009-06-26 15:28:53 +0000
committerRussell Bryant <russell@russellbryant.com>2009-06-26 15:28:53 +0000
commit0264eef1156b8ef7369884dd5c663646f1b2b429 (patch)
treea28e9113cf1daf97e45a8fc6d41a52c76ac69836 /cel
parente06c6f97c4c222b4c802ac2b85f76a331991dffb (diff)
Merge the new Channel Event Logging (CEL) subsystem.
CEL is the new system for logging channel events. This was inspired after facing many problems trying to represent what is possible to happen to a call in Asterisk using CDR records. For more information on CEL, see the built in HTML or PDF documentation generated from the files in doc/tex/. Many thanks to Steve Murphy (murf) and Brian Degenhardt (bmd) for their hard work developing this code. Also, thanks to Matt Nicholson (mnicholson) and Sean Bright (seanbright) for their assistance in the final push to get this code ready for Asterisk trunk. Review: https://reviewboard.asterisk.org/r/239/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@203638 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'cel')
-rw-r--r--cel/Makefile20
-rw-r--r--cel/cel_adaptive_odbc.c771
-rw-r--r--cel/cel_custom.c216
-rw-r--r--cel/cel_manager.c175
-rw-r--r--cel/cel_pgsql.c565
-rw-r--r--cel/cel_radius.c254
-rw-r--r--cel/cel_sqlite3_custom.c364
-rw-r--r--cel/cel_tds.c587
8 files changed, 2952 insertions, 0 deletions
diff --git a/cel/Makefile b/cel/Makefile
new file mode 100644
index 000000000..5ac6d89af
--- /dev/null
+++ b/cel/Makefile
@@ -0,0 +1,20 @@
+#
+# Asterisk -- A telephony toolkit for Linux.
+#
+# Makefile for CEL backends
+#
+# Copyright (C) 1999-2008, Digium, Inc.
+#
+# This program is free software, distributed under the terms of
+# the GNU General Public License
+#
+
+-include $(ASTTOPDIR)/menuselect.makeopts $(ASTTOPDIR)/menuselect.makedeps
+
+MODULE_PREFIX=cel
+MENUSELECT_CATEGORY=CEL
+MENUSELECT_DESCRIPTION=Channel Event Logging
+
+all: _all
+
+include $(ASTTOPDIR)/Makefile.moddir_rules
diff --git a/cel/cel_adaptive_odbc.c b/cel/cel_adaptive_odbc.c
new file mode 100644
index 000000000..984f0590f
--- /dev/null
+++ b/cel/cel_adaptive_odbc.c
@@ -0,0 +1,771 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2008 Digium
+ *
+ * Adapted from cdr_adaptive_odbc:
+ * Tilghman Lesher <cdr_adaptive_odbc__v1@the-tilghman.com>
+ * by Steve Murphy
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Adaptive ODBC CEL backend
+ *
+ * \author Tilghman Lesher <cdr_adaptive_odbc__v1@the-tilghman.com>
+ * \ingroup cel_drivers
+ */
+
+/*** MODULEINFO
+ <depend>generic_odbc</depend>
+ <depend>ltdl</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <sys/types.h>
+#include <time.h>
+
+#include <sql.h>
+#include <sqlext.h>
+#include <sqltypes.h>
+
+#include "asterisk/config.h"
+#include "asterisk/channel.h"
+#include "asterisk/lock.h"
+#include "asterisk/linkedlists.h"
+#include "asterisk/res_odbc.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+
+#define CONFIG "cel_adaptive_odbc.conf"
+static struct ast_event_sub *event_sub = NULL;
+
+/* Optimization to reduce number of memory allocations */
+static int maxsize = 512, maxsize2 = 512;
+
+struct columns {
+ char *name;
+ char *celname;
+ char *filtervalue;
+ char *staticvalue;
+ SQLSMALLINT type;
+ SQLINTEGER size;
+ SQLSMALLINT decimals;
+ SQLSMALLINT radix;
+ SQLSMALLINT nullable;
+ SQLINTEGER octetlen;
+ AST_LIST_ENTRY(columns) list;
+};
+
+struct tables {
+ char *connection;
+ char *table;
+ unsigned int usegmtime:1;
+ AST_LIST_HEAD_NOLOCK(odbc_columns, columns) columns;
+ AST_RWLIST_ENTRY(tables) list;
+};
+
+static AST_RWLIST_HEAD_STATIC(odbc_tables, tables);
+
+static int load_config(void)
+{
+ struct ast_config *cfg;
+ struct ast_variable *var;
+ const char *tmp, *catg;
+ struct tables *tableptr;
+ struct columns *entry;
+ struct odbc_obj *obj;
+ char columnname[80];
+ char connection[40];
+ char table[40];
+ int lenconnection, lentable, usegmtime = 0;
+ SQLLEN sqlptr;
+ int res = 0;
+ SQLHSTMT stmt = NULL;
+ struct ast_flags config_flags = { 0 }; /* Part of our config comes from the database */
+
+ cfg = ast_config_load(CONFIG, config_flags);
+ if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
+ ast_log(LOG_WARNING, "Unable to load " CONFIG ". No adaptive ODBC CEL records!\n");
+ return -1;
+ }
+
+ for (catg = ast_category_browse(cfg, NULL); catg; catg = ast_category_browse(cfg, catg)) {
+ var = ast_variable_browse(cfg, catg);
+ if (!var)
+ continue;
+
+ if (ast_strlen_zero(tmp = ast_variable_retrieve(cfg, catg, "connection"))) {
+ ast_log(LOG_WARNING, "No connection parameter found in '%s'. Skipping.\n", catg);
+ continue;
+ }
+ ast_copy_string(connection, tmp, sizeof(connection));
+ lenconnection = strlen(connection);
+
+ if (!ast_strlen_zero(tmp = ast_variable_retrieve(cfg, catg, "usegmtime"))) {
+ usegmtime = ast_true(tmp);
+ }
+
+ /* When loading, we want to be sure we can connect. */
+ obj = ast_odbc_request_obj(connection, 1);
+ if (!obj) {
+ ast_log(LOG_WARNING, "No such connection '%s' in the '%s' section of " CONFIG ". Check res_odbc.conf.\n", connection, catg);
+ continue;
+ }
+
+ if (ast_strlen_zero(tmp = ast_variable_retrieve(cfg, catg, "table"))) {
+ ast_log(LOG_NOTICE, "No table name found. Assuming 'cel'.\n");
+ tmp = "cel";
+ }
+ ast_copy_string(table, tmp, sizeof(table));
+ lentable = strlen(table);
+
+ res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt);
+ if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
+ ast_log(LOG_WARNING, "SQL Alloc Handle failed on connection '%s'!\n", connection);
+ ast_odbc_release_obj(obj);
+ continue;
+ }
+
+ res = SQLColumns(stmt, NULL, 0, NULL, 0, (unsigned char *)table, SQL_NTS, (unsigned char *)"%", SQL_NTS);
+ if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
+ ast_log(LOG_ERROR, "Unable to query database columns on connection '%s'. Skipping.\n", connection);
+ ast_odbc_release_obj(obj);
+ continue;
+ }
+
+ tableptr = ast_calloc(sizeof(char), sizeof(*tableptr) + lenconnection + 1 + lentable + 1);
+ if (!tableptr) {
+ ast_log(LOG_ERROR, "Out of memory creating entry for table '%s' on connection '%s'\n", table, connection);
+ ast_odbc_release_obj(obj);
+ res = -1;
+ break;
+ }
+
+ tableptr->usegmtime = usegmtime;
+ tableptr->connection = (char *)tableptr + sizeof(*tableptr);
+ tableptr->table = (char *)tableptr + sizeof(*tableptr) + lenconnection + 1;
+ ast_copy_string(tableptr->connection, connection, lenconnection + 1);
+ ast_copy_string(tableptr->table, table, lentable + 1);
+
+ ast_verb(3, "Found adaptive CEL table %s@%s.\n", tableptr->table, tableptr->connection);
+
+ /* Check for filters first */
+ for (var = ast_variable_browse(cfg, catg); var; var = var->next) {
+ if (strncmp(var->name, "filter", 6) == 0) {
+ char *celvar = ast_strdupa(var->name + 6);
+ celvar = ast_strip(celvar);
+ ast_verb(3, "Found filter %s for cel variable %s in %s@%s\n", var->value, celvar, tableptr->table, tableptr->connection);
+
+ entry = ast_calloc(sizeof(char), sizeof(*entry) + strlen(celvar) + 1 + strlen(var->value) + 1);
+ if (!entry) {
+ ast_log(LOG_ERROR, "Out of memory creating filter entry for CEL variable '%s' in table '%s' on connection '%s'\n", celvar, table, connection);
+ res = -1;
+ break;
+ }
+
+ /* NULL column entry means this isn't a column in the database */
+ entry->name = NULL;
+ entry->celname = (char *)entry + sizeof(*entry);
+ entry->filtervalue = (char *)entry + sizeof(*entry) + strlen(celvar) + 1;
+ strcpy(entry->celname, celvar);
+ strcpy(entry->filtervalue, var->value);
+
+ AST_LIST_INSERT_TAIL(&(tableptr->columns), entry, list);
+ }
+ }
+
+ while ((res = SQLFetch(stmt)) != SQL_NO_DATA && res != SQL_ERROR) {
+ char *celvar = "", *staticvalue = "";
+
+ SQLGetData(stmt, 4, SQL_C_CHAR, columnname, sizeof(columnname), &sqlptr);
+
+ /* Is there an alias for this column? */
+
+ /* NOTE: This seems like a non-optimal parse method, but I'm going
+ * for user configuration readability, rather than fast parsing. We
+ * really don't parse this file all that often, anyway.
+ */
+ for (var = ast_variable_browse(cfg, catg); var; var = var->next) {
+ if (strncmp(var->name, "alias", 5) == 0 && strcasecmp(var->value, columnname) == 0) {
+ char *alias = ast_strdupa(var->name + 5);
+ celvar = ast_strip(alias);
+ ast_verb(3, "Found alias %s for column %s in %s@%s\n", celvar, columnname, tableptr->table, tableptr->connection);
+ break;
+ } else if (strncmp(var->name, "static", 6) == 0 && strcasecmp(var->value, columnname) == 0) {
+ char *item = ast_strdupa(var->name + 6);
+ item = ast_strip(item);
+ if (item[0] == '"' && item[strlen(item) - 1] == '"') {
+ /* Remove surrounding quotes */
+ item[strlen(item) - 1] = '\0';
+ item++;
+ }
+ staticvalue = item;
+ }
+ }
+
+ entry = ast_calloc(sizeof(char), sizeof(*entry) + strlen(columnname) + 1 + strlen(celvar) + 1 + strlen(staticvalue) + 1);
+ if (!entry) {
+ ast_log(LOG_ERROR, "Out of memory creating entry for column '%s' in table '%s' on connection '%s'\n", columnname, table, connection);
+ res = -1;
+ break;
+ }
+ entry->name = (char *)entry + sizeof(*entry);
+ strcpy(entry->name, columnname);
+
+ if (!ast_strlen_zero(celvar)) {
+ entry->celname = entry->name + strlen(columnname) + 1;
+ strcpy(entry->celname, celvar);
+ } else { /* Point to same place as the column name */
+ entry->celname = (char *)entry + sizeof(*entry);
+ }
+
+ if (!ast_strlen_zero(staticvalue)) {
+ entry->staticvalue = entry->celname + strlen(entry->celname) + 1;
+ strcpy(entry->staticvalue, staticvalue);
+ }
+
+ SQLGetData(stmt, 5, SQL_C_SHORT, &entry->type, sizeof(entry->type), NULL);
+ SQLGetData(stmt, 7, SQL_C_LONG, &entry->size, sizeof(entry->size), NULL);
+ SQLGetData(stmt, 9, SQL_C_SHORT, &entry->decimals, sizeof(entry->decimals), NULL);
+ SQLGetData(stmt, 10, SQL_C_SHORT, &entry->radix, sizeof(entry->radix), NULL);
+ SQLGetData(stmt, 11, SQL_C_SHORT, &entry->nullable, sizeof(entry->nullable), NULL);
+ SQLGetData(stmt, 16, SQL_C_LONG, &entry->octetlen, sizeof(entry->octetlen), NULL);
+
+ /* Specification states that the octenlen should be the maximum number of bytes
+ * returned in a char or binary column, but it seems that some drivers just set
+ * it to NULL. (Bad Postgres! No biscuit!) */
+ if (entry->octetlen == 0)
+ entry->octetlen = entry->size;
+
+ ast_verb(10, "Found %s column with type %hd with len %ld, octetlen %ld, and numlen (%hd,%hd)\n", entry->name, entry->type, (long) entry->size, (long) entry->octetlen, entry->decimals, entry->radix);
+ /* Insert column info into column list */
+ AST_LIST_INSERT_TAIL(&(tableptr->columns), entry, list);
+ res = 0;
+ }
+
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ ast_odbc_release_obj(obj);
+
+ if (AST_LIST_FIRST(&(tableptr->columns)))
+ AST_RWLIST_INSERT_TAIL(&odbc_tables, tableptr, list);
+ else
+ ast_free(tableptr);
+ }
+ return res;
+}
+
+static int free_config(void)
+{
+ struct tables *table;
+ struct columns *entry;
+ while ((table = AST_RWLIST_REMOVE_HEAD(&odbc_tables, list))) {
+ while ((entry = AST_LIST_REMOVE_HEAD(&(table->columns), list))) {
+ ast_free(entry);
+ }
+ ast_free(table);
+ }
+ return 0;
+}
+
+static SQLHSTMT generic_prepare(struct odbc_obj *obj, void *data)
+{
+ int res, i;
+ char *sql = data;
+ SQLHSTMT stmt;
+ SQLINTEGER nativeerror = 0, numfields = 0;
+ SQLSMALLINT diagbytes = 0;
+ unsigned char state[10], diagnostic[256];
+
+ res = SQLAllocHandle (SQL_HANDLE_STMT, obj->con, &stmt);
+ if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
+ ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
+ return NULL;
+ }
+
+ res = SQLPrepare(stmt, (unsigned char *)sql, SQL_NTS);
+ if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
+ ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", sql);
+ SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes);
+ for (i = 0; i < numfields; i++) {
+ SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes);
+ ast_log(LOG_WARNING, "SQL Execute returned an error %d: %s: %s (%d)\n", res, state, diagnostic, diagbytes);
+ if (i > 10) {
+ ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields);
+ break;
+ }
+ }
+ SQLFreeHandle (SQL_HANDLE_STMT, stmt);
+ return NULL;
+ }
+
+ return stmt;
+}
+
+#define LENGTHEN_BUF1(size) \
+ do { \
+ /* Lengthen buffer, if necessary */ \
+ if (ast_str_strlen(sql) + size + 1 > ast_str_size(sql)) { \
+ if (ast_str_make_space(&sql, ((ast_str_size(sql) + size + 1) / 512 + 1) * 512) != 0) { \
+ ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CEL '%s:%s' failed.\n", tableptr->connection, tableptr->table); \
+ ast_free(sql); \
+ ast_free(sql2); \
+ AST_RWLIST_UNLOCK(&odbc_tables); \
+ return; \
+ } \
+ } \
+ } while (0)
+
+#define LENGTHEN_BUF2(size) \
+ do { \
+ if (ast_str_strlen(sql2) + size + 1 > ast_str_size(sql2)) { \
+ if (ast_str_make_space(&sql2, ((ast_str_size(sql2) + size + 3) / 512 + 1) * 512) != 0) { \
+ ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CEL '%s:%s' failed.\n", tableptr->connection, tableptr->table); \
+ ast_free(sql); \
+ ast_free(sql2); \
+ AST_RWLIST_UNLOCK(&odbc_tables); \
+ return; \
+ } \
+ } \
+ } while (0)
+
+static void odbc_log(const struct ast_event *event, void *userdata)
+{
+ struct tables *tableptr;
+ struct columns *entry;
+ struct odbc_obj *obj;
+ struct ast_str *sql = ast_str_create(maxsize), *sql2 = ast_str_create(maxsize2);
+ char *tmp;
+ char colbuf[1024], *colptr;
+ SQLHSTMT stmt = NULL;
+ SQLLEN rows = 0;
+ struct ast_cel_event_record record = {
+ .version = AST_CEL_EVENT_RECORD_VERSION,
+ };
+
+ if (ast_cel_fill_record(event, &record)) {
+ return;
+ }
+
+ if (!sql || !sql2) {
+ if (sql)
+ ast_free(sql);
+ if (sql2)
+ ast_free(sql2);
+ return;
+ }
+
+ if (AST_RWLIST_RDLOCK(&odbc_tables)) {
+ ast_log(LOG_ERROR, "Unable to lock table list. Insert CEL(s) failed.\n");
+ ast_free(sql);
+ ast_free(sql2);
+ return;
+ }
+
+ AST_LIST_TRAVERSE(&odbc_tables, tableptr, list) {
+ int first = 1;
+ ast_str_set(&sql, 0, "INSERT INTO %s (", tableptr->table);
+ ast_str_set(&sql2, 0, " VALUES (");
+
+ /* No need to check the connection now; we'll handle any failure in prepare_and_execute */
+ if (!(obj = ast_odbc_request_obj(tableptr->connection, 0))) {
+ ast_log(LOG_WARNING, "cel_adaptive_odbc: Unable to retrieve database handle for '%s:%s'. CEL failed: %s\n", tableptr->connection, tableptr->table, ast_str_buffer(sql));
+ continue;
+ }
+
+ AST_LIST_TRAVERSE(&(tableptr->columns), entry, list) {
+ int datefield = 0;
+ if (strcasecmp(entry->celname, "eventtime") == 0) {
+ datefield = 1;
+ }
+
+ /* Check if we have a similarly named variable */
+ if (entry->staticvalue) {
+ colptr = ast_strdupa(entry->staticvalue);
+ } else if (datefield) {
+ struct timeval date_tv = record.event_time;
+ struct ast_tm tm = { 0, };
+ ast_localtime(&date_tv, &tm, tableptr->usegmtime ? "UTC" : NULL);
+ ast_strftime(colbuf, sizeof(colbuf), "%Y-%m-%d %H:%M:%S", &tm);
+ colptr = colbuf;
+ } else {
+ if (strcmp(entry->celname, "userdeftype") == 0) {
+ strncpy(colbuf, record.user_defined_name, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "cid_name") == 0) {
+ strncpy(colbuf, record.caller_id_name, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "cid_num") == 0) {
+ strncpy(colbuf, record.caller_id_num, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "cid_ani") == 0) {
+ strncpy(colbuf, record.caller_id_ani, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "cid_rdnis") == 0) {
+ strncpy(colbuf, record.caller_id_rdnis, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "cid_dnid") == 0) {
+ strncpy(colbuf, record.caller_id_dnid, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "exten") == 0) {
+ strncpy(colbuf, record.extension, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "context") == 0) {
+ strncpy(colbuf, record.context, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "channame") == 0) {
+ strncpy(colbuf, record.channel_name, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "appname") == 0) {
+ strncpy(colbuf, record.application_name, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "appdata") == 0) {
+ strncpy(colbuf, record.application_data, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "accountcode") == 0) {
+ strncpy(colbuf, record.account_code, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "peeraccount") == 0) {
+ strncpy(colbuf, record.peer_account, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "uniqueid") == 0) {
+ strncpy(colbuf, record.unique_id, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "linkedid") == 0) {
+ strncpy(colbuf, record.linked_id, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "userfield") == 0) {
+ strncpy(colbuf, record.user_field, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "peer") == 0) {
+ strncpy(colbuf, record.peer, sizeof(colbuf));
+ } else if (strcmp(entry->celname, "amaflags") == 0) {
+ snprintf(colbuf, sizeof(colbuf), "%d", record.amaflag);
+ } else {
+ colbuf[0] = 0;
+ }
+ colptr = colbuf;
+ }
+
+ if (colptr) {
+ /* Check first if the column filters this entry. Note that this
+ * is very specifically NOT ast_strlen_zero(), because the filter
+ * could legitimately specify that the field is blank, which is
+ * different from the field being unspecified (NULL). */
+ if (entry->filtervalue && strcasecmp(colptr, entry->filtervalue) != 0) {
+ ast_verb(4, "CEL column '%s' with value '%s' does not match filter of"
+ " '%s'. Cancelling this CEL.\n",
+ entry->celname, colptr, entry->filtervalue);
+ goto early_release;
+ }
+
+ /* Only a filter? */
+ if (ast_strlen_zero(entry->name))
+ continue;
+
+ LENGTHEN_BUF1(strlen(entry->name));
+
+ switch (entry->type) {
+ case SQL_CHAR:
+ case SQL_VARCHAR:
+ case SQL_LONGVARCHAR:
+ case SQL_BINARY:
+ case SQL_VARBINARY:
+ case SQL_LONGVARBINARY:
+ case SQL_GUID:
+ /* For these two field names, get the rendered form, instead of the raw
+ * form (but only when we're dealing with a character-based field).
+ */
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ snprintf(colbuf, sizeof(colbuf), "%s", record.event_name);
+ }
+
+ /* Truncate too-long fields */
+ if (entry->type != SQL_GUID) {
+ if (strlen(colptr) > entry->octetlen) {
+ colptr[entry->octetlen] = '\0';
+ }
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(strlen(colptr));
+
+ /* Encode value, with escaping */
+ ast_str_append(&sql2, 0, "%s'", first ? "" : ",");
+ for (tmp = colptr; *tmp; tmp++) {
+ if (*tmp == '\'') {
+ ast_str_append(&sql2, 0, "''");
+ } else if (*tmp == '\\' && ast_odbc_backslash_is_escape(obj)) {
+ ast_str_append(&sql2, 0, "\\\\");
+ } else {
+ ast_str_append(&sql2, 0, "%c", *tmp);
+ }
+ }
+ ast_str_append(&sql2, 0, "'");
+ break;
+ case SQL_TYPE_DATE:
+ {
+ int year = 0, month = 0, day = 0;
+ if (sscanf(colptr, "%d-%d-%d", &year, &month, &day) != 3 || year <= 0 ||
+ month <= 0 || month > 12 || day < 0 || day > 31 ||
+ ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31) ||
+ (month == 2 && year % 400 == 0 && day > 29) ||
+ (month == 2 && year % 100 == 0 && day > 28) ||
+ (month == 2 && year % 4 == 0 && day > 29) ||
+ (month == 2 && year % 4 != 0 && day > 28)) {
+ ast_log(LOG_WARNING, "CEL variable %s is not a valid date ('%s').\n", entry->name, colptr);
+ continue;
+ }
+
+ if (year > 0 && year < 100) {
+ year += 2000;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(17);
+ ast_str_append(&sql2, 0, "%s{ d '%04d-%02d-%02d' }", first ? "" : ",", year, month, day);
+ }
+ break;
+ case SQL_TYPE_TIME:
+ {
+ int hour = 0, minute = 0, second = 0;
+ int count = sscanf(colptr, "%d:%d:%d", &hour, &minute, &second);
+
+ if ((count != 2 && count != 3) || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
+ ast_log(LOG_WARNING, "CEL variable %s is not a valid time ('%s').\n", entry->name, colptr);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(15);
+ ast_str_append(&sql2, 0, "%s{ t '%02d:%02d:%02d' }", first ? "" : ",", hour, minute, second);
+ }
+ break;
+ case SQL_TYPE_TIMESTAMP:
+ case SQL_TIMESTAMP:
+ {
+ int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0;
+ int count = sscanf(colptr, "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second);
+
+ if ((count != 3 && count != 5 && count != 6) || year <= 0 ||
+ month <= 0 || month > 12 || day < 0 || day > 31 ||
+ ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31) ||
+ (month == 2 && year % 400 == 0 && day > 29) ||
+ (month == 2 && year % 100 == 0 && day > 28) ||
+ (month == 2 && year % 4 == 0 && day > 29) ||
+ (month == 2 && year % 4 != 0 && day > 28) ||
+ hour > 23 || minute > 59 || second > 59 || hour < 0 || minute < 0 || second < 0) {
+ ast_log(LOG_WARNING, "CEL variable %s is not a valid timestamp ('%s').\n", entry->name, colptr);
+ continue;
+ }
+
+ if (year > 0 && year < 100) {
+ year += 2000;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(26);
+ ast_str_append(&sql2, 0, "%s{ ts '%04d-%02d-%02d %02d:%02d:%02d' }", first ? "" : ",", year, month, day, hour, minute, second);
+ }
+ break;
+ case SQL_INTEGER:
+ {
+ int integer = 0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ integer = (int) record.event_type;
+ } else if (sscanf(colptr, "%d", &integer) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an integer.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(12);
+ ast_str_append(&sql2, 0, "%s%d", first ? "" : ",", integer);
+ }
+ break;
+ case SQL_BIGINT:
+ {
+ long long integer = 0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ integer = (long long) record.event_type;
+ } else if (sscanf(colptr, "%lld", &integer) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an integer.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(24);
+ ast_str_append(&sql2, 0, "%s%lld", first ? "" : ",", integer);
+ }
+ break;
+ case SQL_SMALLINT:
+ {
+ short integer = 0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ integer = (short) record.event_type;
+ } else if (sscanf(colptr, "%hd", &integer) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an integer.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(6);
+ ast_str_append(&sql2, 0, "%s%d", first ? "" : ",", integer);
+ }
+ break;
+ case SQL_TINYINT:
+ {
+ char integer = 0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ integer = (char) record.event_type;
+ } else if (sscanf(colptr, "%hhd", &integer) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an integer.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(4);
+ ast_str_append(&sql2, 0, "%s%d", first ? "" : ",", integer);
+ }
+ break;
+ case SQL_BIT:
+ {
+ char integer = 0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ integer = (char) record.event_type;
+ } else if (sscanf(colptr, "%hhd", &integer) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an integer.\n", entry->name);
+ continue;
+ }
+ if (integer != 0)
+ integer = 1;
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(2);
+ ast_str_append(&sql2, 0, "%s%d", first ? "" : ",", integer);
+ }
+ break;
+ case SQL_NUMERIC:
+ case SQL_DECIMAL:
+ {
+ double number = 0.0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ number = (double)record.event_type;
+ } else if (sscanf(colptr, "%lf", &number) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an numeric type.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(entry->decimals);
+ ast_str_append(&sql2, 0, "%s%*.*lf", first ? "" : ",", entry->decimals, entry->radix, number);
+ }
+ break;
+ case SQL_FLOAT:
+ case SQL_REAL:
+ case SQL_DOUBLE:
+ {
+ double number = 0.0;
+ if (strcasecmp(entry->name, "eventtype") == 0) {
+ number = (double) record.event_type;
+ } else if (sscanf(colptr, "%lf", &number) != 1) {
+ ast_log(LOG_WARNING, "CEL variable %s is not an numeric type.\n", entry->name);
+ continue;
+ }
+
+ ast_str_append(&sql, 0, "%s%s", first ? "" : ",", entry->name);
+ LENGTHEN_BUF2(entry->decimals);
+ ast_str_append(&sql2, 0, "%s%lf", first ? "" : ",", number);
+ }
+ break;
+ default:
+ ast_log(LOG_WARNING, "Column type %d (field '%s:%s:%s') is unsupported at this time.\n", entry->type, tableptr->connection, tableptr->table, entry->name);
+ continue;
+ }
+ first = 0;
+ }
+ }
+
+ /* Concatenate the two constructed buffers */
+ LENGTHEN_BUF1(ast_str_strlen(sql2));
+ ast_str_append(&sql, 0, ")");
+ ast_str_append(&sql2, 0, ")");
+ ast_str_append(&sql, 0, "%s", ast_str_buffer(sql2));
+
+ ast_verb(11, "[%s]\n", ast_str_buffer(sql));
+
+ stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, ast_str_buffer(sql));
+ if (stmt) {
+ SQLRowCount(stmt, &rows);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ }
+ if (rows == 0) {
+ ast_log(LOG_WARNING, "cel_adaptive_odbc: Insert failed on '%s:%s'. CEL failed: %s\n", tableptr->connection, tableptr->table, ast_str_buffer(sql));
+ }
+early_release:
+ ast_odbc_release_obj(obj);
+ }
+ AST_RWLIST_UNLOCK(&odbc_tables);
+
+ /* Next time, just allocate buffers that are that big to start with. */
+ if (ast_str_strlen(sql) > maxsize) {
+ maxsize = ast_str_strlen(sql);
+ }
+ if (ast_str_strlen(sql2) > maxsize2) {
+ maxsize2 = ast_str_strlen(sql2);
+ }
+
+ ast_free(sql);
+ ast_free(sql2);
+}
+
+static int unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+ if (AST_RWLIST_WRLOCK(&odbc_tables)) {
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, odbc_log, "Adaptive ODBC CEL backend", NULL, AST_EVENT_IE_END);
+ if (!event_sub) {
+ ast_log(LOG_ERROR, "cel_adaptive_odbc: Unable to subscribe to CEL events\n");
+ }
+ ast_log(LOG_ERROR, "Unable to lock column list. Unload failed.\n");
+ return -1;
+ }
+
+ free_config();
+ AST_RWLIST_UNLOCK(&odbc_tables);
+ return 0;
+}
+
+static int load_module(void)
+{
+ if (AST_RWLIST_WRLOCK(&odbc_tables)) {
+ ast_log(LOG_ERROR, "Unable to lock column list. Load failed.\n");
+ return 0;
+ }
+ load_config();
+ AST_RWLIST_UNLOCK(&odbc_tables);
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, odbc_log, "Adaptive ODBC CEL backend", NULL, AST_EVENT_IE_END);
+ if (!event_sub) {
+ ast_log(LOG_ERROR, "cel_odbc: Unable to subscribe to CEL events\n");
+ }
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int reload(void)
+{
+ if (AST_RWLIST_WRLOCK(&odbc_tables)) {
+ ast_log(LOG_ERROR, "Unable to lock column list. Reload failed.\n");
+ return -1;
+ }
+
+ free_config();
+ load_config();
+ AST_RWLIST_UNLOCK(&odbc_tables);
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Adaptive ODBC CEL backend",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+);
+
diff --git a/cel/cel_custom.c b/cel/cel_custom.c
new file mode 100644
index 000000000..37a741935
--- /dev/null
+++ b/cel/cel_custom.c
@@ -0,0 +1,216 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2009, Digium, Inc.
+ *
+ * Steve Murphy <murf@digium.com>
+ * much borrowed from cdr code (cdr_custom.c), author Mark Spencer
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Custom Comma Separated Value CEL records.
+ *
+ * \author Steve Murphy <murf@digium.com>
+ *
+ * \arg See also \ref AstCEL
+ *
+ * Logs in LOG_DIR/cel_custom
+ * \ingroup cel_drivers
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/paths.h"
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/config.h"
+#include "asterisk/pbx.h"
+#include "asterisk/utils.h"
+#include "asterisk/lock.h"
+#include "asterisk/threadstorage.h"
+#include "asterisk/strings.h"
+
+#define CUSTOM_LOG_DIR "/cel_custom"
+#define CONFIG "cel_custom.conf"
+
+AST_THREADSTORAGE(custom_buf);
+
+static const char name[] = "cel-custom";
+
+struct cel_config {
+ AST_DECLARE_STRING_FIELDS(
+ AST_STRING_FIELD(filename);
+ AST_STRING_FIELD(format);
+ );
+ ast_mutex_t lock;
+ AST_RWLIST_ENTRY(cel_config) list;
+};
+
+static struct ast_event_sub *event_sub = NULL;
+
+static AST_RWLIST_HEAD_STATIC(sinks, cel_config);
+
+static void free_config(void)
+{
+ struct cel_config *sink;
+ while ((sink = AST_RWLIST_REMOVE_HEAD(&sinks, list))) {
+ ast_mutex_destroy(&sink->lock);
+ ast_free(sink);
+ }
+}
+
+static int load_config(void)
+{
+ struct ast_config *cfg;
+ struct ast_variable *var;
+ struct ast_flags config_flags = { 0 };
+ int res = 0;
+
+ cfg = ast_config_load(CONFIG, config_flags);
+ if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
+ ast_log(LOG_ERROR, "Unable to load " CONFIG ". Not logging CEL to custom CSVs.\n");
+ return -1;
+ }
+
+ var = ast_variable_browse(cfg, "mappings");
+ while (var) {
+ if (!ast_strlen_zero(var->name) && !ast_strlen_zero(var->value)) {
+ struct cel_config *sink = ast_calloc_with_stringfields(1, struct cel_config, 1024);
+
+ if (!sink) {
+ ast_log(LOG_ERROR, "Unable to allocate memory for configuration settings.\n");
+ res = -2;
+ break;
+ }
+
+ ast_string_field_build(sink, format, "%s\n", var->value);
+ ast_string_field_build(sink, filename, "%s/%s/%s", ast_config_AST_LOG_DIR, name, var->name);
+ ast_mutex_init(&sink->lock);
+
+ AST_RWLIST_INSERT_TAIL(&sinks, sink, list);
+ } else {
+ ast_log(LOG_NOTICE, "Mapping must have both a filename and a format at line %d\n", var->lineno);
+ }
+ var = var->next;
+ }
+ ast_config_destroy(cfg);
+
+ return res;
+}
+
+static void custom_log(const struct ast_event *event, void *userdata)
+{
+ struct ast_channel *dummy;
+ struct ast_str *str;
+ struct cel_config *config;
+
+ /* Batching saves memory management here. Otherwise, it's the same as doing an allocation and free each time. */
+ if (!(str = ast_str_thread_get(&custom_buf, 16))) {
+ return;
+ }
+
+ dummy = ast_cel_fabricate_channel_from_event(event);
+
+ if (!dummy) {
+ ast_log(LOG_ERROR, "Unable to fabricate channel from CEL event.\n");
+ return;
+ }
+
+ AST_RWLIST_RDLOCK(&sinks);
+
+ AST_LIST_TRAVERSE(&sinks, config, list) {
+ FILE *out;
+
+ ast_str_substitute_variables(&str, 0, dummy, config->format);
+
+ /* Even though we have a lock on the list, we could be being chased by
+ another thread and this lock ensures that we won't step on anyone's
+ toes. Once each CEL backend gets it's own thread, this lock can be
+ removed. */
+ ast_mutex_lock(&config->lock);
+
+ /* Because of the absolutely unconditional need for the
+ highest reliability possible in writing billing records,
+ we open write and close the log file each time */
+ if ((out = fopen(config->filename, "a"))) {
+ fputs(ast_str_buffer(str), out);
+ fflush(out); /* be particularly anal here */
+ fclose(out);
+ } else {
+ ast_log(LOG_ERROR, "Unable to re-open master file %s : %s\n", config->filename, strerror(errno));
+ }
+
+ ast_mutex_unlock(&config->lock);
+ }
+
+ AST_RWLIST_UNLOCK(&sinks);
+
+ ast_channel_release(dummy);
+}
+
+static int unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+
+ if (AST_RWLIST_WRLOCK(&sinks)) {
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, custom_log, "CEL Custom CSV Logging",
+ NULL, AST_EVENT_IE_END);
+ ast_log(LOG_ERROR, "Unable to lock sink list. Unload failed.\n");
+ return -1;
+ }
+
+ free_config();
+ AST_RWLIST_UNLOCK(&sinks);
+ return 0;
+}
+
+static enum ast_module_load_result load_module(void)
+{
+ if (AST_RWLIST_WRLOCK(&sinks)) {
+ ast_log(LOG_ERROR, "Unable to lock sink list. Load failed.\n");
+ return AST_MODULE_LOAD_FAILURE;
+ }
+
+ load_config();
+ AST_RWLIST_UNLOCK(&sinks);
+
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, custom_log, "CEL Custom CSV Logging",
+ NULL, AST_EVENT_IE_END);
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int reload(void)
+{
+ if (AST_RWLIST_WRLOCK(&sinks)) {
+ ast_log(LOG_ERROR, "Unable to lock sink list. Load failed.\n");
+ return AST_MODULE_LOAD_FAILURE;
+ }
+
+ free_config();
+ load_config();
+ AST_RWLIST_UNLOCK(&sinks);
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Customizable Comma Separated Values CEL Backend",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+ );
+
diff --git a/cel/cel_manager.c b/cel/cel_manager.c
new file mode 100644
index 000000000..a57c31ebe
--- /dev/null
+++ b/cel/cel_manager.c
@@ -0,0 +1,175 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2008 - 2009, Digium, Inc.
+ *
+ * Steve Murphy <murf@digium.com>
+ * who freely borrowed code from the cdr equivalents
+ * (see cdr/cdr_manager.c)
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Asterisk Channel Event records.
+ *
+ * See also
+ * \arg \ref AstCDR
+ * \arg \ref AstAMI
+ * \arg \ref Config_ami
+ * \ingroup cel_drivers
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+#include "asterisk/utils.h"
+#include "asterisk/manager.h"
+#include "asterisk/config.h"
+
+static const char DATE_FORMAT[] = "%Y-%m-%d %T";
+
+static const char CONF_FILE[] = "cel.conf";
+
+static int enablecel;
+
+static struct ast_event_sub *event_sub;
+
+static void manager_log(const struct ast_event *event, void *userdata)
+{
+ struct ast_tm timeresult;
+ char start_time[80] = "";
+ struct ast_cel_event_record record = {
+ .version = AST_CEL_EVENT_RECORD_VERSION,
+ };
+
+ if (ast_cel_fill_record(event, &record)) {
+ return;
+ }
+
+ if (!enablecel) {
+ return;
+ }
+
+ ast_localtime(&record.event_time, &timeresult, NULL);
+ ast_strftime(start_time, sizeof(start_time), DATE_FORMAT, &timeresult);
+
+ manager_event(EVENT_FLAG_CALL, "CEL",
+ "EventName: %s\r\n"
+ "AccountCode: %s\r\n"
+ "CallerIDnum: %s\r\n"
+ "CallerIDname: %s\r\n"
+ "CallerIDani: %s\r\n"
+ "CallerIDrdnis: %s\r\n"
+ "CallerIDdnid: %s\r\n"
+ "Exten: %s\r\n"
+ "Context: %s\r\n"
+ "Channel: %s\r\n"
+ "Application: %s\r\n"
+ "AppData: %s\r\n"
+ "EventTime: %s\r\n"
+ "AMAFlags: %s\r\n"
+ "UniqueID: %s\r\n"
+ "LinkedID: %s\r\n"
+ "Userfield: %s\r\n"
+ "Peer: %s\r\n",
+ record.event_name, record.account_code, record.caller_id_num,
+ record.caller_id_name, record.caller_id_ani, record.caller_id_rdnis,
+ record.caller_id_dnid, record.extension, record.context, record.channel_name,
+ record.application_name, record.application_data, start_time,
+ ast_cel_get_ama_flag_name(record.amaflag), record.unique_id, record.linked_id,
+ record.user_field, record.peer);
+}
+
+static int load_config(int reload)
+{
+ const char *cat = NULL;
+ struct ast_config *cfg;
+ struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
+ struct ast_variable *v;
+ int newenablecel = 0;
+
+ cfg = ast_config_load(CONF_FILE, config_flags);
+ if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
+ return 0;
+ }
+
+ if (!cfg) {
+ ast_log(LOG_WARNING, "Failed to load configuration file. CEL manager Module not activated.\n");
+ enablecel = 0;
+ return -1;
+ }
+
+ while ((cat = ast_category_browse(cfg, cat))) {
+ if (strcasecmp(cat, "manager")) {
+ continue;
+ }
+
+ for (v = ast_variable_browse(cfg, cat); v; v = v->next) {
+ if (!strcasecmp(v->name, "enabled")) {
+ newenablecel = ast_true(v->value);
+ } else {
+ ast_log(LOG_NOTICE, "Unknown option '%s' specified "
+ "for cel_manager.\n", v->name);
+ }
+ }
+ }
+
+ ast_config_destroy(cfg);
+
+ if (enablecel && !newenablecel) {
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+ } else if (!enablecel && newenablecel) {
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, manager_log, "Manager Event Logging", NULL, AST_EVENT_IE_END);
+ if (!event_sub) {
+ ast_log(LOG_ERROR, "Unable to register Asterisk Call Manager CEL handling\n");
+ }
+ }
+ enablecel = newenablecel;
+
+ return 0;
+}
+
+static int unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+ return 0;
+}
+
+static int load_module(void)
+{
+ if (load_config(0)) {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int reload(void)
+{
+ return load_config(1);
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk Manager Interface CEL Backend",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+);
diff --git a/cel/cel_pgsql.c b/cel/cel_pgsql.c
new file mode 100644
index 000000000..8b10261ee
--- /dev/null
+++ b/cel/cel_pgsql.c
@@ -0,0 +1,565 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2008
+ *
+ * Steve Murphy - adapted to CEL, from:
+ * Matthew D. Hardeman <mhardemn@papersoft.com>
+ * Adapted from the MySQL CDR logger originally by James Sharp
+ *
+ * Modified April, 2007; Dec, 2008
+ * Steve Murphy <murf@digium.com>
+
+ * Modified September 2003
+ * Matthew D. Hardeman <mhardemn@papersoft.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief PostgreSQL CEL logger
+ *
+ * \author Steve Murphy <murf@digium.com>
+ * \extref PostgreSQL http://www.postgresql.org/
+ *
+ * See also
+ * \arg \ref Config_cel
+ * \arg http://www.postgresql.org/
+ * \ingroup cel_drivers
+ */
+
+/*** MODULEINFO
+ <depend>pgsql</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <libpq-fe.h>
+
+#include "asterisk/config.h"
+#include "asterisk/options.h"
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+#include "asterisk.h"
+
+#define DATE_FORMAT "%Y-%m-%d %T"
+
+static char *config = "cel_pgsql.conf";
+static char *pghostname = NULL, *pgdbname = NULL, *pgdbuser = NULL, *pgpassword = NULL, *pgdbport = NULL, *table = NULL;
+static int connected = 0;
+static int maxsize = 512, maxsize2 = 512;
+
+AST_MUTEX_DEFINE_STATIC(pgsql_lock);
+
+static PGconn *conn = NULL;
+static PGresult *result = NULL;
+static struct ast_event_sub *event_sub = NULL;
+
+struct columns {
+ char *name;
+ char *type;
+ int len;
+ unsigned int notnull:1;
+ unsigned int hasdefault:1;
+ AST_RWLIST_ENTRY(columns) list;
+};
+
+static AST_RWLIST_HEAD_STATIC(psql_columns, columns);
+
+#define LENGTHEN_BUF1(size) \
+ do { \
+ /* Lengthen buffer, if necessary */ \
+ if (ast_str_strlen(sql) + size + 1 > ast_str_size(sql)) { \
+ if (ast_str_make_space(&sql, ((ast_str_size(sql) + size + 3) / 512 + 1) * 512) != 0) { \
+ ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CDR failed.\n"); \
+ ast_free(sql); \
+ ast_free(sql2); \
+ AST_RWLIST_UNLOCK(&psql_columns); \
+ return; \
+ } \
+ } \
+ } while (0)
+
+#define LENGTHEN_BUF2(size) \
+ do { \
+ if (ast_str_strlen(sql2) + size + 1 > ast_str_size(sql2)) { \
+ if (ast_str_make_space(&sql2, ((ast_str_size(sql2) + size + 3) / 512 + 1) * 512) != 0) { \
+ ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CDR failed.\n"); \
+ ast_free(sql); \
+ ast_free(sql2); \
+ AST_RWLIST_UNLOCK(&psql_columns); \
+ return; \
+ } \
+ } \
+ } while (0)
+
+static void pgsql_log(const struct ast_event *event, void *userdata)
+{
+ struct ast_tm tm;
+ char timestr[128];
+ char *pgerror;
+ struct ast_cel_event_record record = {
+ .version = AST_CEL_EVENT_RECORD_VERSION,
+ };
+
+ if (ast_cel_fill_record(event, &record)) {
+ return;
+ }
+
+ ast_mutex_lock(&pgsql_lock);
+
+ ast_localtime(&record.event_time, &tm, NULL);
+ ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, &tm);
+
+ if ((!connected) && pghostname && pgdbuser && pgpassword && pgdbname) {
+ conn = PQsetdbLogin(pghostname, pgdbport, NULL, NULL, pgdbname, pgdbuser, pgpassword);
+ if (PQstatus(conn) != CONNECTION_BAD) {
+ connected = 1;
+ } else {
+ pgerror = PQerrorMessage(conn);
+ ast_log(LOG_ERROR, "cel_pgsql: Unable to connect to database server %s. Calls will not be logged!\n", pghostname);
+ ast_log(LOG_ERROR, "cel_pgsql: Reason: %s\n", pgerror);
+ PQfinish(conn);
+ conn = NULL;
+ }
+ }
+ if (connected) {
+ struct columns *cur;
+ struct ast_str *sql = ast_str_create(maxsize), *sql2 = ast_str_create(maxsize2);
+ char buf[257], escapebuf[513];
+ const char *value;
+ int first = 1;
+
+ if (!sql || !sql2) {
+ if (sql) {
+ ast_free(sql);
+ }
+ if (sql2) {
+ ast_free(sql2);
+ }
+ return;
+ }
+
+ ast_str_set(&sql, 0, "INSERT INTO %s (", table);
+ ast_str_set(&sql2, 0, " VALUES (");
+
+#define SEP (first ? "" : ",")
+
+ AST_RWLIST_RDLOCK(&psql_columns);
+ AST_RWLIST_TRAVERSE(&psql_columns, cur, list) {
+ LENGTHEN_BUF1(strlen(cur->name) + 2);
+ ast_str_append(&sql, 0, "%s\"%s\"", first ? "" : ",", cur->name);
+
+ if (strcmp(cur->name, "eventtime") == 0) {
+ if (strncmp(cur->type, "int", 3) == 0) {
+ LENGTHEN_BUF2(13);
+ ast_str_append(&sql2, 0, "%s%ld", SEP, record.event_time.tv_sec);
+ } else if (strncmp(cur->type, "float", 5) == 0) {
+ LENGTHEN_BUF2(31);
+ ast_str_append(&sql2, 0, "%s%f",
+ SEP,
+ (double) record.event_time.tv_sec +
+ (double) record.event_time.tv_usec / 1000000.0);
+ } else {
+ /* char, hopefully */
+ LENGTHEN_BUF2(31);
+ ast_localtime(&record.event_time, &tm, NULL);
+ ast_strftime(buf, sizeof(buf), DATE_FORMAT, &tm);
+ ast_str_append(&sql2, 0, "%s'%s'", SEP, buf);
+ }
+ } else if (strcmp(cur->name, "eventtype") == 0) {
+ if (cur->type[0] == 'i') {
+ /* Get integer, no need to escape anything */
+ LENGTHEN_BUF2(5);
+ ast_str_append(&sql2, 0, "%s%d", SEP, (int) record.event_type);
+ } else if (strncmp(cur->type, "float", 5) == 0) {
+ LENGTHEN_BUF2(31);
+ ast_str_append(&sql2, 0, "%s%f", SEP, (double) record.event_type);
+ } else {
+ /* Char field, probably */
+ LENGTHEN_BUF2(strlen(record.event_name) + 1);
+ ast_str_append(&sql2, 0, "%s'%s'", SEP, record.event_name);
+ }
+ } else if (strcmp(cur->name, "amaflags") == 0) {
+ if (strncmp(cur->type, "int", 3) == 0) {
+ /* Integer, no need to escape anything */
+ LENGTHEN_BUF2(13);
+ ast_str_append(&sql2, 0, "%s%d", SEP, record.amaflag);
+ } else {
+ /* Although this is a char field, there are no special characters in the values for these fields */
+ LENGTHEN_BUF2(31);
+ ast_str_append(&sql2, 0, "%s'%d'", SEP, record.amaflag);
+ }
+ } else {
+ /* Arbitrary field, could be anything */
+ if (strcmp(cur->name, "userdeftype") == 0) {
+ value = record.user_defined_name;
+ } else if (strcmp(cur->name, "cid_name") == 0) {
+ value = record.caller_id_name;
+ } else if (strcmp(cur->name, "cid_num") == 0) {
+ value = record.caller_id_num;
+ } else if (strcmp(cur->name, "cid_ani") == 0) {
+ value = record.caller_id_ani;
+ } else if (strcmp(cur->name, "cid_rdnis") == 0) {
+ value = record.caller_id_rdnis;
+ } else if (strcmp(cur->name, "cid_dnid") == 0) {
+ value = record.caller_id_dnid;
+ } else if (strcmp(cur->name, "exten") == 0) {
+ value = record.extension;
+ } else if (strcmp(cur->name, "context") == 0) {
+ value = record.context;
+ } else if (strcmp(cur->name, "channame") == 0) {
+ value = record.channel_name;
+ } else if (strcmp(cur->name, "appname") == 0) {
+ value = record.application_name;
+ } else if (strcmp(cur->name, "appdata") == 0) {
+ value = record.application_data;
+ } else if (strcmp(cur->name, "accountcode") == 0) {
+ value = record.account_code;
+ } else if (strcmp(cur->name, "peeraccount") == 0) {
+ value = record.peer_account;
+ } else if (strcmp(cur->name, "uniqueid") == 0) {
+ value = record.unique_id;
+ } else if (strcmp(cur->name, "linkedid") == 0) {
+ value = record.linked_id;
+ } else if (strcmp(cur->name, "userfield") == 0) {
+ value = record.user_field;
+ } else if (strcmp(cur->name, "peer") == 0) {
+ value = record.peer;
+ } else {
+ value = "";
+ }
+ if (strncmp(cur->type, "int", 3) == 0) {
+ long long whatever;
+ if (value && sscanf(value, "%lld", &whatever) == 1) {
+ LENGTHEN_BUF2(26);
+ ast_str_append(&sql2, 0, "%s%lld", SEP, whatever);
+ } else {
+ LENGTHEN_BUF2(2);
+ ast_str_append(&sql2, 0, "%s0", SEP);
+ }
+ } else if (strncmp(cur->type, "float", 5) == 0) {
+ long double whatever;
+ if (value && sscanf(value, "%Lf", &whatever) == 1) {
+ LENGTHEN_BUF2(51);
+ ast_str_append(&sql2, 0, "%s%30Lf", SEP, whatever);
+ } else {
+ LENGTHEN_BUF2(2);
+ ast_str_append(&sql2, 0, "%s0", SEP);
+ }
+ /* XXX Might want to handle dates, times, and other misc fields here XXX */
+ } else {
+ if (value) {
+ PQescapeStringConn(conn, escapebuf, value, strlen(value), NULL);
+ } else {
+ escapebuf[0] = '\0';
+ }
+ LENGTHEN_BUF2(strlen(escapebuf) + 3);
+ ast_str_append(&sql2, 0, "%s'%s'", SEP, escapebuf);
+ }
+ }
+ first = 0;
+ }
+ AST_RWLIST_UNLOCK(&psql_columns);
+ LENGTHEN_BUF1(ast_str_strlen(sql2) + 2);
+ ast_str_append(&sql, 0, ")%s)", ast_str_buffer(sql2));
+ ast_verb(11, "[%s]\n", ast_str_buffer(sql));
+
+ ast_debug(2, "inserting a CEL record.\n");
+ /* Test to be sure we're still connected... */
+ /* If we're connected, and connection is working, good. */
+ /* Otherwise, attempt reconnect. If it fails... sorry... */
+ if (PQstatus(conn) == CONNECTION_OK) {
+ connected = 1;
+ } else {
+ ast_log(LOG_ERROR, "Connection was lost... attempting to reconnect.\n");
+ PQreset(conn);
+ if (PQstatus(conn) == CONNECTION_OK) {
+ ast_log(LOG_ERROR, "Connection reestablished.\n");
+ connected = 1;
+ } else {
+ pgerror = PQerrorMessage(conn);
+ ast_log(LOG_ERROR, "Unable to reconnect to database server %s. Calls will not be logged!\n", pghostname);
+ ast_log(LOG_ERROR, "Reason: %s\n", pgerror);
+ PQfinish(conn);
+ conn = NULL;
+ connected = 0;
+ ast_mutex_unlock(&pgsql_lock);
+ ast_free(sql);
+ ast_free(sql2);
+ return;
+ }
+ }
+ result = PQexec(conn, ast_str_buffer(sql));
+ if (PQresultStatus(result) != PGRES_COMMAND_OK) {
+ pgerror = PQresultErrorMessage(result);
+ ast_log(LOG_ERROR, "Failed to insert call detail record into database!\n");
+ ast_log(LOG_ERROR, "Reason: %s\n", pgerror);
+ ast_log(LOG_ERROR, "Connection may have been lost... attempting to reconnect.\n");
+ PQreset(conn);
+ if (PQstatus(conn) == CONNECTION_OK) {
+ ast_log(LOG_ERROR, "Connection reestablished.\n");
+ connected = 1;
+ PQclear(result);
+ result = PQexec(conn, ast_str_buffer(sql));
+ if (PQresultStatus(result) != PGRES_COMMAND_OK) {
+ pgerror = PQresultErrorMessage(result);
+ ast_log(LOG_ERROR, "HARD ERROR! Attempted reconnection failed. DROPPING CALL RECORD!\n");
+ ast_log(LOG_ERROR, "Reason: %s\n", pgerror);
+ }
+ }
+ ast_mutex_unlock(&pgsql_lock);
+ PQclear(result);
+ ast_free(sql);
+ ast_free(sql2);
+ return;
+ }
+ ast_mutex_unlock(&pgsql_lock);
+ }
+}
+
+static int my_unload_module(void)
+{
+ struct columns *current;
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+ if (conn) {
+ PQfinish(conn);
+ }
+ if (pghostname) {
+ ast_free(pghostname);
+ }
+ if (pgdbname) {
+ ast_free(pgdbname);
+ }
+ if (pgdbuser) {
+ ast_free(pgdbuser);
+ }
+ if (pgpassword) {
+ ast_free(pgpassword);
+ }
+ if (pgdbport) {
+ ast_free(pgdbport);
+ }
+ if (table) {
+ ast_free(table);
+ }
+ AST_RWLIST_WRLOCK(&psql_columns);
+ while ((current = AST_RWLIST_REMOVE_HEAD(&psql_columns, list))) {
+ ast_free(current);
+ }
+ AST_RWLIST_UNLOCK(&psql_columns);
+ return 0;
+}
+
+static int unload_module(void)
+{
+ return my_unload_module();
+}
+
+static int process_my_load_module(struct ast_config *cfg)
+{
+ struct ast_variable *var;
+ char *pgerror;
+ const char *tmp;
+ PGresult *result;
+ struct columns *cur;
+
+ if (!(var = ast_variable_browse(cfg, "global"))) {
+ ast_log(LOG_WARNING,"CEL pgsql config file missing global section.\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg,"global","hostname"))) {
+ ast_log(LOG_WARNING,"PostgreSQL server hostname not specified. Assuming unix socket connection\n");
+ tmp = ""; /* connect via UNIX-socket by default */
+ }
+ if (pghostname)
+ ast_free(pghostname);
+ if (!(pghostname = ast_strdup(tmp))) {
+ ast_log(LOG_WARNING,"PostgreSQL Ran out of memory copying host info\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg, "global", "dbname"))) {
+ ast_log(LOG_WARNING,"PostgreSQL database not specified. Assuming asterisk\n");
+ tmp = "asteriskceldb";
+ }
+ if (pgdbname)
+ ast_free(pgdbname);
+ if (!(pgdbname = ast_strdup(tmp))) {
+ ast_log(LOG_WARNING,"PostgreSQL Ran out of memory copying dbname info\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg, "global", "user"))) {
+ ast_log(LOG_WARNING,"PostgreSQL database user not specified. Assuming asterisk\n");
+ tmp = "asterisk";
+ }
+ if (pgdbuser)
+ ast_free(pgdbuser);
+ if (!(pgdbuser = ast_strdup(tmp))) {
+ ast_log(LOG_WARNING,"PostgreSQL Ran out of memory copying user info\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg, "global", "password"))) {
+ ast_log(LOG_WARNING, "PostgreSQL database password not specified. Assuming blank\n");
+ tmp = "";
+ }
+ if (pgpassword)
+ ast_free(pgpassword);
+ if (!(pgpassword = ast_strdup(tmp))) {
+ ast_log(LOG_WARNING,"PostgreSQL Ran out of memory copying password info\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg,"global","port"))) {
+ ast_log(LOG_WARNING,"PostgreSQL database port not specified. Using default 5432.\n");
+ tmp = "5432";
+ }
+ if (pgdbport)
+ ast_free(pgdbport);
+ if (!(pgdbport = ast_strdup(tmp))) {
+ ast_log(LOG_WARNING,"PostgreSQL Ran out of memory copying port info\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (!(tmp = ast_variable_retrieve(cfg, "global", "table"))) {
+ ast_log(LOG_WARNING,"CEL table not specified. Assuming cel\n");
+ tmp = "cel";
+ }
+ if (table)
+ ast_free(table);
+ if (!(table = ast_strdup(tmp))) {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ if (option_debug) {
+ if (ast_strlen_zero(pghostname)) {
+ ast_debug(3, "cel_pgsql: using default unix socket\n");
+ } else {
+ ast_debug(3, "cel_pgsql: got hostname of %s\n", pghostname);
+ }
+ ast_debug(3, "cel_pgsql: got port of %s\n", pgdbport);
+ ast_debug(3, "cel_pgsql: got user of %s\n", pgdbuser);
+ ast_debug(3, "cel_pgsql: got dbname of %s\n", pgdbname);
+ ast_debug(3, "cel_pgsql: got password of %s\n", pgpassword);
+ ast_debug(3, "cel_pgsql: got sql table name of %s\n", table);
+ }
+
+ conn = PQsetdbLogin(pghostname, pgdbport, NULL, NULL, pgdbname, pgdbuser, pgpassword);
+ if (PQstatus(conn) != CONNECTION_BAD) {
+ char sqlcmd[512];
+ char *fname, *ftype, *flen, *fnotnull, *fdef;
+ char *tableptr;
+ int i, rows;
+
+ ast_debug(1, "Successfully connected to PostgreSQL database.\n");
+ connected = 1;
+
+ /* Remove any schema name from the table */
+ if ((tableptr = strrchr(table, '.'))) {
+ tableptr++;
+ } else {
+ tableptr = table;
+ }
+
+ /* Query the columns */
+ snprintf(sqlcmd, sizeof(sqlcmd), "select a.attname, t.typname, a.attlen, a.attnotnull, d.adsrc from pg_class c, pg_type t, pg_attribute a left outer join pg_attrdef d on a.atthasdef and d.adrelid = a.attrelid and d.adnum = a.attnum where c.oid = a.attrelid and a.atttypid = t.oid and (a.attnum > 0) and c.relname = '%s' order by c.relname, attnum", tableptr);
+ result = PQexec(conn, sqlcmd);
+ if (PQresultStatus(result) != PGRES_TUPLES_OK) {
+ pgerror = PQresultErrorMessage(result);
+ ast_log(LOG_ERROR, "Failed to query database columns: %s\n", pgerror);
+ PQclear(result);
+ unload_module();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ rows = PQntuples(result);
+ for (i = 0; i < rows; i++) {
+ fname = PQgetvalue(result, i, 0);
+ ftype = PQgetvalue(result, i, 1);
+ flen = PQgetvalue(result, i, 2);
+ fnotnull = PQgetvalue(result, i, 3);
+ fdef = PQgetvalue(result, i, 4);
+ ast_verb(4, "Found column '%s' of type '%s'\n", fname, ftype);
+ cur = ast_calloc(1, sizeof(*cur) + strlen(fname) + strlen(ftype) + 2);
+ if (cur) {
+ sscanf(flen, "%d", &cur->len);
+ cur->name = (char *)cur + sizeof(*cur);
+ cur->type = (char *)cur + sizeof(*cur) + strlen(fname) + 1;
+ strcpy(cur->name, fname);
+ strcpy(cur->type, ftype);
+ if (*fnotnull == 't') {
+ cur->notnull = 1;
+ } else {
+ cur->notnull = 0;
+ }
+ if (!ast_strlen_zero(fdef)) {
+ cur->hasdefault = 1;
+ } else {
+ cur->hasdefault = 0;
+ }
+ AST_RWLIST_INSERT_TAIL(&psql_columns, cur, list);
+ }
+ }
+ PQclear(result);
+ } else {
+ pgerror = PQerrorMessage(conn);
+ ast_log(LOG_ERROR, "cel_pgsql: Unable to connect to database server %s. CALLS WILL NOT BE LOGGED!!\n", pghostname);
+ ast_log(LOG_ERROR, "cel_pgsql: Reason: %s\n", pgerror);
+ connected = 0;
+ }
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int my_load_module(int reload)
+{
+ struct ast_config *cfg;
+ int res;
+ struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
+
+ if ((cfg = ast_config_load(config, config_flags)) == NULL || cfg == CONFIG_STATUS_FILEINVALID) {
+ ast_log(LOG_WARNING, "Unable to load config for PostgreSQL CEL's: %s\n", config);
+ return AST_MODULE_LOAD_DECLINE;
+ } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
+ return AST_MODULE_LOAD_SUCCESS;
+ }
+
+ res = process_my_load_module(cfg);
+ ast_config_destroy(cfg);
+
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, pgsql_log, "CEL PGSQL backend", NULL, AST_EVENT_IE_END);
+
+ if (!event_sub) {
+ ast_log(LOG_WARNING, "Unable to subscribe to CEL events for pgsql\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int load_module(void)
+{
+ return my_load_module(0);
+}
+
+static int reload(void)
+{
+ my_unload_module();
+ return my_load_module(1);
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "PostgreSQL CEL Backend",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+);
diff --git a/cel/cel_radius.c b/cel/cel_radius.c
new file mode 100644
index 000000000..c44044f6e
--- /dev/null
+++ b/cel/cel_radius.c
@@ -0,0 +1,254 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 1999 - 2005, Digium, Inc.
+ *
+ * Mark Spencer <markster@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief RADIUS CEL Support
+ * \author Philippe Sultan
+ * \extref The Radius Client Library - http://developer.berlios.de/projects/radiusclient-ng/
+ *
+ * \arg See also \ref AstCEL
+ * \ingroup cel_drivers
+ */
+
+/*** MODULEINFO
+ <depend>radius</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Rev$")
+
+#include <radiusclient-ng.h>
+
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+#include "asterisk/utils.h"
+#include "asterisk/options.h"
+
+/*! ISO 8601 standard format */
+#define DATE_FORMAT "%Y-%m-%d %T %z"
+
+#define VENDOR_CODE 22736
+
+enum {
+ PW_AST_ACCT_CODE = 101,
+ PW_AST_CIDNUM = 102,
+ PW_AST_CIDNAME = 103,
+ PW_AST_CIDANI = 104,
+ PW_AST_CIDRDNIS = 105,
+ PW_AST_CIDDNID = 106,
+ PW_AST_EXTEN = 107,
+ PW_AST_CONTEXT = 108,
+ PW_AST_CHANNAME = 109,
+ PW_AST_APPNAME = 110,
+ PW_AST_APPDATA = 111,
+ PW_AST_EVENT_TIME = 112,
+ PW_AST_AMA_FLAGS = 113,
+ PW_AST_UNIQUE_ID = 114,
+ PW_AST_USER_NAME = 115,
+ PW_AST_LINKED_ID = 116,
+};
+
+enum {
+ /*! Log dates and times in UTC */
+ RADIUS_FLAG_USEGMTIME = (1 << 0),
+ /*! Log Unique ID */
+ RADIUS_FLAG_LOGUNIQUEID = (1 << 1),
+ /*! Log User Field */
+ RADIUS_FLAG_LOGUSERFIELD = (1 << 2)
+};
+
+static char *cel_config = "cel.conf";
+
+static char radiuscfg[PATH_MAX] = "/etc/radiusclient-ng/radiusclient.conf";
+
+static struct ast_flags global_flags = { RADIUS_FLAG_USEGMTIME | RADIUS_FLAG_LOGUNIQUEID | RADIUS_FLAG_LOGUSERFIELD };
+
+static rc_handle *rh = NULL;
+static struct ast_event_sub *event_sub = NULL;
+
+#define ADD_VENDOR_CODE(x,y) (rc_avpair_add(rh, send, x, &y, strlen(y), VENDOR_CODE))
+
+static int build_radius_record(VALUE_PAIR **send, struct ast_cel_event_record *record)
+{
+ int recordtype = PW_STATUS_STOP;
+ struct ast_tm tm;
+ char timestr[128];
+ char *amaflags;
+
+ if (!rc_avpair_add(rh, send, PW_ACCT_STATUS_TYPE, &recordtype, 0, 0)) {
+ return -1;
+ }
+ /* Account code */
+ if (!ADD_VENDOR_CODE(PW_AST_ACCT_CODE, record->account_code)) {
+ return -1;
+ }
+ /* Source */
+ if (!ADD_VENDOR_CODE(PW_AST_CIDNUM, record->caller_id_num)) {
+ return -1;
+ }
+ /* Destination */
+ if (!ADD_VENDOR_CODE(PW_AST_EXTEN, record->extension)) {
+ return -1;
+ }
+ /* Destination context */
+ if (!ADD_VENDOR_CODE(PW_AST_CONTEXT, record->context)) {
+ return -1;
+ }
+ /* Caller ID */
+ if (!ADD_VENDOR_CODE(PW_AST_CIDNAME, record->caller_id_name)) {
+ return -1;
+ }
+ /* Caller ID ani */
+ if (!ADD_VENDOR_CODE(PW_AST_CIDANI, record->caller_id_ani)) {
+ return -1;
+ }
+ /* Caller ID rdnis */
+ if (!ADD_VENDOR_CODE(PW_AST_CIDRDNIS, record->caller_id_rdnis)) {
+ return -1;
+ }
+ /* Caller ID dnid */
+ if (!ADD_VENDOR_CODE(PW_AST_CIDDNID, record->caller_id_dnid)) {
+ return -1;
+ }
+ /* Channel */
+ if (!ADD_VENDOR_CODE(PW_AST_CHANNAME, record->channel_name)) {
+ return -1;
+ }
+ /* Last Application */
+ if (!ADD_VENDOR_CODE(PW_AST_APPNAME, record->application_name)) {
+ return -1;
+ }
+ /* Last Data */
+ if (!ADD_VENDOR_CODE(PW_AST_APPDATA, record->application_data)) {
+ return -1;
+ }
+ /* Event Time */
+ ast_localtime(&record->event_time, &tm,
+ ast_test_flag(&global_flags, RADIUS_FLAG_USEGMTIME) ? "GMT" : NULL);
+ ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, &tm);
+ if (!rc_avpair_add(rh, send, PW_AST_EVENT_TIME, timestr, strlen(timestr), VENDOR_CODE)) {
+ return -1;
+ }
+ /* AMA Flags */
+ amaflags = ast_strdupa(ast_cel_get_ama_flag_name(record->amaflag));
+ if (!rc_avpair_add(rh, send, PW_AST_AMA_FLAGS, amaflags, strlen(amaflags), VENDOR_CODE)) {
+ return -1;
+ }
+ if (ast_test_flag(&global_flags, RADIUS_FLAG_LOGUNIQUEID)) {
+ /* Unique ID */
+ if (!ADD_VENDOR_CODE(PW_AST_UNIQUE_ID, record->unique_id)) {
+ return -1;
+ }
+ }
+ /* LinkedID */
+ if (!ADD_VENDOR_CODE(PW_AST_LINKED_ID, record->linked_id)) {
+ return -1;
+ }
+ /* Setting Acct-Session-Id & User-Name attributes for proper generation
+ of Acct-Unique-Session-Id on server side */
+ /* Channel */
+ if (!rc_avpair_add(rh, send, PW_USER_NAME, &record->channel_name,
+ strlen(record->channel_name), 0)) {
+ return -1;
+ }
+ return 0;
+}
+
+static void radius_log(const struct ast_event *event, void *userdata)
+{
+ int result = ERROR_RC;
+ VALUE_PAIR *send = NULL;
+ struct ast_cel_event_record record = {
+ .version = AST_CEL_EVENT_RECORD_VERSION,
+ };
+
+ if (ast_cel_fill_record(event, &record)) {
+ return;
+ }
+
+ if (build_radius_record(&send, &record)) {
+ if (option_debug) {
+ ast_log(LOG_DEBUG, "Unable to create RADIUS record. CEL not recorded!\n");
+ }
+ goto return_cleanup;
+ }
+
+ result = rc_acct(rh, 0, send);
+ if (result != OK_RC) {
+ ast_log(LOG_ERROR, "Failed to record Radius CEL record!\n");
+ }
+
+return_cleanup:
+ if (send) {
+ rc_avpair_free(send);
+ }
+}
+
+static int unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int load_module(void)
+{
+ struct ast_config *cfg;
+ struct ast_flags config_flags = { 0 };
+ const char *tmp;
+
+ if ((cfg = ast_config_load(cel_config, config_flags))) {
+ ast_set2_flag(&global_flags, ast_true(ast_variable_retrieve(cfg, "radius", "usegmtime")), RADIUS_FLAG_USEGMTIME);
+ if ((tmp = ast_variable_retrieve(cfg, "radius", "radiuscfg"))) {
+ ast_copy_string(radiuscfg, tmp, sizeof(radiuscfg));
+ }
+ ast_config_destroy(cfg);
+ } else {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /* start logging */
+ rc_openlog("asterisk");
+
+ /* read radiusclient-ng config file */
+ if (!(rh = rc_read_config(radiuscfg))) {
+ ast_log(LOG_NOTICE, "Cannot load radiusclient-ng configuration file %s.\n", radiuscfg);
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /* read radiusclient-ng dictionaries */
+ if (rc_read_dictionary(rh, rc_conf_str(rh, "dictionary"))) {
+ ast_log(LOG_NOTICE, "Cannot load radiusclient-ng dictionary file.\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, radius_log, "CEL Radius Logging", NULL, AST_EVENT_IE_END);
+
+ if (!event_sub) {
+ return AST_MODULE_LOAD_DECLINE;
+ } else {
+ return AST_MODULE_LOAD_SUCCESS;
+ }
+}
+
+AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "RADIUS CEL Backend");
diff --git a/cel/cel_sqlite3_custom.c b/cel/cel_sqlite3_custom.c
new file mode 100644
index 000000000..c37641ba8
--- /dev/null
+++ b/cel/cel_sqlite3_custom.c
@@ -0,0 +1,364 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2007, Digium, Inc.
+ *
+ * Steve Murphy <murf@digium.com> borrowed code from cdr,
+ * Mark Spencer <markster@digium.com> and others.
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Custom SQLite3 CEL records.
+ *
+ * \author Adapted by Steve Murphy <murf@digium.com> from
+ * Alejandro Rios <alejandro.rios@avatar.com.co> and
+ * Russell Bryant <russell@digium.com> from
+ * cdr_mysql_custom by Edward Eastman <ed@dm3.co.uk>,
+ * and cdr_sqlite by Holger Schurig <hs4233@mail.mn-solutions.de>
+ *
+ *
+ * \arg See also \ref AstCEL
+ *
+ *
+ * \ingroup cel_drivers
+ */
+
+/*** MODULEINFO
+ <depend>sqlite3</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <sqlite3.h>
+
+#include "asterisk/paths.h"
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/config.h"
+#include "asterisk/pbx.h"
+#include "asterisk/logger.h"
+#include "asterisk/utils.h"
+#include "asterisk/cli.h"
+#include "asterisk/options.h"
+#include "asterisk/stringfields.h"
+
+AST_MUTEX_DEFINE_STATIC(lock);
+
+static const char config_file[] = "cel_sqlite3_custom.conf";
+
+static const char name[] = "cel_sqlite3_custom";
+static sqlite3 *db = NULL;
+
+static char table[80];
+/*! XXX \bug Handling of this var is crash prone on reloads */
+static char *columns;
+static struct ast_event_sub *event_sub = NULL;
+
+struct values {
+ char *expression;
+ AST_LIST_ENTRY(values) list;
+};
+
+static AST_LIST_HEAD_STATIC(sql_values, values);
+
+static void free_config(void);
+
+static int load_column_config(const char *tmp)
+{
+ char *col = NULL;
+ char *cols = NULL, *save = NULL;
+ char *escaped = NULL;
+ struct ast_str *column_string = NULL;
+
+ if (ast_strlen_zero(tmp)) {
+ ast_log(LOG_WARNING, "Column names not specified. Module not loaded.\n");
+ return -1;
+ }
+ if (!(column_string = ast_str_create(1024))) {
+ ast_log(LOG_ERROR, "Out of memory creating temporary buffer for column list for table '%s.'\n", table);
+ return -1;
+ }
+ if (!(save = cols = ast_strdup(tmp))) {
+ ast_log(LOG_ERROR, "Out of memory creating temporary buffer for column list for table '%s.'\n", table);
+ ast_free(column_string);
+ return -1;
+ }
+ while ((col = strsep(&cols, ","))) {
+ col = ast_strip(col);
+ escaped = sqlite3_mprintf("%q", col);
+ if (!escaped) {
+ ast_log(LOG_ERROR, "Out of memory creating entry for column '%s' in table '%s.'\n", col, table);
+ ast_free(column_string);
+ ast_free(save);
+ return -1;
+ }
+ ast_str_append(&column_string, 0, "%s%s", ast_str_strlen(column_string) ? "," : "", escaped);
+ sqlite3_free(escaped);
+ }
+ if (!(columns = ast_strdup(ast_str_buffer(column_string)))) {
+ ast_log(LOG_ERROR, "Out of memory copying columns string for table '%s.'\n", table);
+ ast_free(column_string);
+ ast_free(save);
+ return -1;
+ }
+ ast_free(column_string);
+ ast_free(save);
+
+ return 0;
+}
+
+static int load_values_config(const char *tmp)
+{
+ char *val = NULL;
+ char *vals = NULL, *save = NULL;
+ struct values *value = NULL;
+
+ if (ast_strlen_zero(tmp)) {
+ ast_log(LOG_WARNING, "Values not specified. Module not loaded.\n");
+ return -1;
+ }
+ if (!(save = vals = ast_strdup(tmp))) {
+ ast_log(LOG_ERROR, "Out of memory creating temporary buffer for value '%s'\n", tmp);
+ return -1;
+ }
+ while ((val = strsep(&vals, ","))) {
+ /* Strip the single quotes off if they are there */
+ val = ast_strip_quoted(val, "'", "'");
+ value = ast_calloc(sizeof(char), sizeof(*value) + strlen(val) + 1);
+ if (!value) {
+ ast_log(LOG_ERROR, "Out of memory creating entry for value '%s'\n", val);
+ ast_free(save);
+ return -1;
+ }
+ value->expression = (char *) value + sizeof(*value);
+ ast_copy_string(value->expression, val, strlen(val) + 1);
+ AST_LIST_INSERT_TAIL(&sql_values, value, list);
+ }
+ ast_free(save);
+
+ return 0;
+}
+
+static int load_config(int reload)
+{
+ struct ast_config *cfg;
+ struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
+ struct ast_variable *mappingvar;
+ const char *tmp;
+
+ if ((cfg = ast_config_load(config_file, config_flags)) == CONFIG_STATUS_FILEMISSING || cfg == CONFIG_STATUS_FILEINVALID) {
+ ast_log(LOG_WARNING, "Failed to %sload configuration file. %s\n",
+ reload ? "re" : "", reload ? "" : "Module not activated.");
+ return -1;
+ } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
+ return 0;
+ }
+
+ if (reload) {
+ free_config();
+ }
+
+ if (!(mappingvar = ast_variable_browse(cfg, "master"))) {
+ /* Nothing configured */
+ ast_config_destroy(cfg);
+ return -1;
+ }
+
+ /* Mapping must have a table name */
+ if (!ast_strlen_zero(tmp = ast_variable_retrieve(cfg, "master", "table"))) {
+ ast_copy_string(table, tmp, sizeof(table));
+ } else {
+ ast_log(LOG_WARNING, "Table name not specified. Assuming cel.\n");
+ strcpy(table, "cel");
+ }
+
+ /* Columns */
+ if (load_column_config(ast_variable_retrieve(cfg, "master", "columns"))) {
+ ast_config_destroy(cfg);
+ free_config();
+ return -1;
+ }
+
+ /* Values */
+ if (load_values_config(ast_variable_retrieve(cfg, "master", "values"))) {
+ ast_config_destroy(cfg);
+ free_config();
+ return -1;
+ }
+
+ ast_verb(3, "Logging CEL records to table '%s' in 'master.db'\n", table);
+
+ ast_config_destroy(cfg);
+
+ return 0;
+}
+
+static void free_config(void)
+{
+ struct values *value;
+
+ if (db) {
+ sqlite3_close(db);
+ db = NULL;
+ }
+
+ if (columns) {
+ ast_free(columns);
+ columns = NULL;
+ }
+
+ while ((value = AST_LIST_REMOVE_HEAD(&sql_values, list))) {
+ ast_free(value);
+ }
+}
+
+static void sqlite3_log(const struct ast_event *event, void *userdata)
+{
+ char *error = NULL;
+ char *sql = NULL;
+ int count = 0;
+
+ if (db == NULL) {
+ /* Should not have loaded, but be failsafe. */
+ return;
+ }
+
+ ast_mutex_lock(&lock);
+
+ { /* Make it obvious that only sql should be used outside of this block */
+ char *escaped;
+ char subst_buf[2048];
+ struct values *value;
+ struct ast_channel *dummy;
+ struct ast_str *value_string = ast_str_create(1024);
+
+ dummy = ast_cel_fabricate_channel_from_event(event);
+ if (!dummy) {
+ ast_log(LOG_ERROR, "Unable to fabricate channel from CEL event.\n");
+ ast_free(value_string);
+ ast_mutex_unlock(&lock);
+ return;
+ }
+ AST_LIST_TRAVERSE(&sql_values, value, list) {
+ pbx_substitute_variables_helper(dummy, value->expression, subst_buf, sizeof(subst_buf) - 1);
+ escaped = sqlite3_mprintf("%q", subst_buf);
+ ast_str_append(&value_string, 0, "%s'%s'", ast_str_strlen(value_string) ? "," : "", escaped);
+ sqlite3_free(escaped);
+ }
+ sql = sqlite3_mprintf("INSERT INTO %q (%s) VALUES (%s)", table, columns, ast_str_buffer(value_string));
+ ast_debug(1, "About to log: %s\n", sql);
+ dummy = ast_channel_release(dummy);
+ ast_free(value_string);
+ }
+
+ /* XXX This seems awful arbitrary... */
+ for (count = 0; count < 5; count++) {
+ int res = sqlite3_exec(db, sql, NULL, NULL, &error);
+ if (res != SQLITE_BUSY && res != SQLITE_LOCKED) {
+ break;
+ }
+ usleep(200);
+ }
+
+ ast_mutex_unlock(&lock);
+
+ if (error) {
+ ast_log(LOG_ERROR, "%s. SQL: %s.\n", error, sql);
+ sqlite3_free(error);
+ }
+
+ if (sql) {
+ sqlite3_free(sql);
+ }
+
+ return;
+}
+
+static int unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+
+ free_config();
+
+ return 0;
+}
+
+static int load_module(void)
+{
+ char *error;
+ char filename[PATH_MAX];
+ int res;
+ char *sql;
+
+ if (load_config(0)) {
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /* is the database there? */
+ snprintf(filename, sizeof(filename), "%s/master.db", ast_config_AST_LOG_DIR);
+ res = sqlite3_open(filename, &db);
+ if (res != SQLITE_OK) {
+ ast_log(LOG_ERROR, "Could not open database %s.\n", filename);
+ free_config();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /* is the table there? */
+ sql = sqlite3_mprintf("SELECT COUNT(AcctId) FROM %q;", table);
+ res = sqlite3_exec(db, sql, NULL, NULL, NULL);
+ sqlite3_free(sql);
+ if (res != SQLITE_OK) {
+ /* We don't use %q for the column list here since we already escaped when building it */
+ sql = sqlite3_mprintf("CREATE TABLE %q (AcctId INTEGER PRIMARY KEY, %s)", table, columns);
+ res = sqlite3_exec(db, sql, NULL, NULL, &error);
+ sqlite3_free(sql);
+ if (res != SQLITE_OK) {
+ ast_log(LOG_WARNING, "Unable to create table '%s': %s.\n", table, error);
+ sqlite3_free(error);
+ free_config();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+ }
+
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, sqlite3_log, "CEL sqlite3 custom backend", NULL, AST_EVENT_IE_END);
+ if (!event_sub) {
+ ast_log(LOG_ERROR, "Unable to register custom SQLite3 CEL handling\n");
+ free_config();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int reload(void)
+{
+ int res = 0;
+
+ ast_mutex_lock(&lock);
+ res = load_config(1);
+ ast_mutex_lock(&lock);
+
+ return res;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "SQLite3 Custom CEL Module",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+);
diff --git a/cel/cel_tds.c b/cel/cel_tds.c
new file mode 100644
index 000000000..138a9cdd3
--- /dev/null
+++ b/cel/cel_tds.c
@@ -0,0 +1,587 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2008, Digium, Inc.
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief FreeTDS CEL logger
+ *
+ * See also
+ * \arg \ref Config_cdr
+ * \arg http://www.freetds.org/
+ * \ingroup cdr_drivers
+ */
+
+/*! \verbatim
+ *
+ * Table Structure for `cdr`
+ *
+
+CREATE TABLE [dbo].[cdr] (
+ [accountcode] [varchar] (20) NULL ,
+ [cidname] [varchar] (80) NULL ,
+ [cidnum] [varchar] (80) NULL ,
+ [cidani] [varchar] (80) NULL ,
+ [cidrdnis] [varchar] (80) NULL ,
+ [ciddnid] [varchar] (80) NULL ,
+ [exten] [varchar] (80) NULL ,
+ [context] [varchar] (80) NULL ,
+ [channame] [varchar] (80) NULL ,
+ [appname] [varchar] (80) NULL ,
+ [appdata] [varchar] (80) NULL ,
+ [eventtime] [datetime] NULL ,
+ [eventtype] [varchar] (32) NULL ,
+ [uniqueid] [varchar] (32) NULL ,
+ [linkedid] [varchar] (32) NULL ,
+ [amaflags] [varchar] (16) NULL ,
+ [userfield] [varchar] (32) NULL ,
+ [peer] [varchar] (32) NULL
+) ON [PRIMARY]
+
+\endverbatim
+
+*/
+
+/*** MODULEINFO
+ <depend>freetds</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <time.h>
+#include <math.h>
+
+#include "asterisk/config.h"
+#include "asterisk/channel.h"
+#include "asterisk/cel.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+
+#include <sqlfront.h>
+#include <sybdb.h>
+
+#ifdef FREETDS_PRE_0_62
+#warning "You have older TDS, you should upgrade!"
+#endif
+
+#define DATE_FORMAT "%Y/%m/%d %T"
+
+static char *config = "cel_tds.conf";
+
+static struct ast_event_sub *event_sub = NULL;
+
+struct cel_tds_config {
+ AST_DECLARE_STRING_FIELDS(
+ AST_STRING_FIELD(connection);
+ AST_STRING_FIELD(database);
+ AST_STRING_FIELD(username);
+ AST_STRING_FIELD(password);
+ AST_STRING_FIELD(table);
+ AST_STRING_FIELD(charset);
+ AST_STRING_FIELD(language);
+ );
+ DBPROCESS *dbproc;
+ unsigned int connected:1;
+};
+
+AST_MUTEX_DEFINE_STATIC(tds_lock);
+
+static struct cel_tds_config *settings;
+
+static char *anti_injection(const char *, int);
+static void get_date(char *, size_t len, struct timeval);
+
+static int execute_and_consume(DBPROCESS *dbproc, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+static int mssql_connect(void);
+static int mssql_disconnect(void);
+
+static void tds_log(const struct ast_event *event, void *userdata)
+{
+ char start[80];
+ char *accountcode_ai, *clidnum_ai, *exten_ai, *context_ai, *clid_ai, *channel_ai, *app_ai, *appdata_ai, *uniqueid_ai, *linkedid_ai, *cidani_ai, *cidrdnis_ai, *ciddnid_ai, *peer_ai, *userfield_ai;
+ RETCODE erc;
+ int attempt = 1;
+ struct ast_cel_event_record record = {
+ .version = AST_CEL_EVENT_RECORD_VERSION,
+ };
+
+ if (ast_cel_fill_record(event, &record)) {
+ return;
+ }
+
+ ast_mutex_lock(&tds_lock);
+
+ accountcode_ai = anti_injection(record.account_code, 20);
+ clidnum_ai = anti_injection(record.caller_id_num, 80);
+ clid_ai = anti_injection(record.caller_id_name, 80);
+ cidani_ai = anti_injection(record.caller_id_ani, 80);
+ cidrdnis_ai = anti_injection(record.caller_id_rdnis, 80);
+ ciddnid_ai = anti_injection(record.caller_id_dnid, 80);
+ exten_ai = anti_injection(record.extension, 80);
+ context_ai = anti_injection(record.context, 80);
+ channel_ai = anti_injection(record.channel_name, 80);
+ app_ai = anti_injection(record.application_name, 80);
+ appdata_ai = anti_injection(record.application_data, 80);
+ uniqueid_ai = anti_injection(record.unique_id, 32);
+ linkedid_ai = anti_injection(record.linked_id, 32);
+ userfield_ai = anti_injection(record.user_field, 32);
+ peer_ai = anti_injection(record.peer, 32);
+
+ get_date(start, sizeof(start), record.event_time);
+
+retry:
+ /* Ensure that we are connected */
+ if (!settings->connected) {
+ ast_log(LOG_NOTICE, "Attempting to reconnect to %s (Attempt %d)\n", settings->connection, attempt);
+ if (mssql_connect()) {
+ /* Connect failed */
+ if (attempt++ < 3) {
+ goto retry;
+ }
+ goto done;
+ }
+ }
+
+ erc = dbfcmd(settings->dbproc,
+ "INSERT INTO %s "
+ "("
+ "accountcode,"
+ "cidnum,"
+ "cidname,"
+ "cidani,"
+ "cidrdnis,"
+ "ciddnid,"
+ "exten,"
+ "context,"
+ "channel,"
+ "appname,"
+ "appdata,"
+ "eventtime,"
+ "eventtype,"
+ "amaflags, "
+ "uniqueid,"
+ "linkedid,"
+ "userfield,"
+ "peer"
+ ") "
+ "VALUES "
+ "("
+ "'%s'," /* accountcode */
+ "'%s'," /* clidnum */
+ "'%s'," /* clid */
+ "'%s'," /* cid-ani */
+ "'%s'," /* cid-rdnis */
+ "'%s'," /* cid-dnid */
+ "'%s'," /* exten */
+ "'%s'," /* context */
+ "'%s'," /* channel */
+ "'%s'," /* app */
+ "'%s'," /* appdata */
+ "%s, " /* eventtime */
+ "'%s'," /* eventtype */
+ "'%s'," /* amaflags */
+ "'%s'," /* uniqueid */
+ "'%s'," /* linkedid */
+ "'%s'," /* userfield */
+ "'%s'" /* peer */
+ ")",
+ settings->table, accountcode_ai, clidnum_ai, clid_ai, cidani_ai, cidrdnis_ai,
+ ciddnid_ai, exten_ai, context_ai, channel_ai, app_ai, appdata_ai, start,
+ record.event_name, ast_cel_get_ama_flag_name(record.amaflag), uniqueid_ai, linkedid_ai,
+ userfield_ai, peer_ai);
+
+ if (erc == FAIL) {
+ if (attempt++ < 3) {
+ ast_log(LOG_NOTICE, "Failed to build INSERT statement, retrying...\n");
+ mssql_disconnect();
+ goto retry;
+ } else {
+ ast_log(LOG_ERROR, "Failed to build INSERT statement, no CEL was logged.\n");
+ goto done;
+ }
+ }
+
+ if (dbsqlexec(settings->dbproc) == FAIL) {
+ if (attempt++ < 3) {
+ ast_log(LOG_NOTICE, "Failed to execute INSERT statement, retrying...\n");
+ mssql_disconnect();
+ goto retry;
+ } else {
+ ast_log(LOG_ERROR, "Failed to execute INSERT statement, no CEL was logged.\n");
+ goto done;
+ }
+ }
+
+ /* Consume any results we might get back (this is more of a sanity check than
+ * anything else, since an INSERT shouldn't return results). */
+ while (dbresults(settings->dbproc) != NO_MORE_RESULTS) {
+ while (dbnextrow(settings->dbproc) != NO_MORE_ROWS);
+ }
+
+done:
+ ast_mutex_unlock(&tds_lock);
+
+ free(accountcode_ai);
+ free(clidnum_ai);
+ free(clid_ai);
+ free(cidani_ai);
+ free(cidrdnis_ai);
+ free(ciddnid_ai);
+ free(exten_ai);
+ free(context_ai);
+ free(channel_ai);
+ free(app_ai);
+ free(appdata_ai);
+ free(uniqueid_ai);
+ free(linkedid_ai);
+ free(userfield_ai);
+ free(peer_ai);
+
+ return;
+}
+
+static char *anti_injection(const char *str, int len)
+{
+ /* Reference to http://www.nextgenss.com/papers/advanced_sql_injection.pdf */
+ char *buf;
+ char *buf_ptr, *srh_ptr;
+ char *known_bad[] = {"select", "insert", "update", "delete", "drop", ";", "--", "\0"};
+ int idx;
+
+ if (!(buf = ast_calloc(1, len + 1))) {
+ ast_log(LOG_ERROR, "Out of memory\n");
+ return NULL;
+ }
+
+ buf_ptr = buf;
+
+ /* Escape single quotes */
+ for (; *str && strlen(buf) < len; str++) {
+ if (*str == '\'') {
+ *buf_ptr++ = '\'';
+ }
+ *buf_ptr++ = *str;
+ }
+ *buf_ptr = '\0';
+
+ /* Erase known bad input */
+ for (idx = 0; *known_bad[idx]; idx++) {
+ while ((srh_ptr = strcasestr(buf, known_bad[idx]))) {
+ memmove(srh_ptr, srh_ptr + strlen(known_bad[idx]), strlen(srh_ptr + strlen(known_bad[idx])) + 1);
+ }
+ }
+ return buf;
+}
+
+static void get_date(char *dateField, size_t len, struct timeval when)
+{
+ /* To make sure we have date variable if not insert null to SQL */
+ if (!ast_tvzero(when)) {
+ struct ast_tm tm;
+ ast_localtime(&when, &tm, NULL);
+ ast_strftime(dateField, len, "'" DATE_FORMAT "'", &tm);
+ } else {
+ ast_copy_string(dateField, "null", len);
+ }
+}
+
+static int execute_and_consume(DBPROCESS *dbproc, const char *fmt, ...)
+{
+ va_list ap;
+ char *buffer;
+
+ va_start(ap, fmt);
+ if (ast_vasprintf(&buffer, fmt, ap) < 0) {
+ va_end(ap);
+ return 1;
+ }
+ va_end(ap);
+
+ if (dbfcmd(dbproc, buffer) == FAIL) {
+ free(buffer);
+ return 1;
+ }
+
+ free(buffer);
+
+ if (dbsqlexec(dbproc) == FAIL) {
+ return 1;
+ }
+
+ /* Consume the result set (we don't really care about the result, though) */
+ while (dbresults(dbproc) != NO_MORE_RESULTS) {
+ while (dbnextrow(dbproc) != NO_MORE_ROWS);
+ }
+
+ return 0;
+}
+
+static int mssql_disconnect(void)
+{
+ if (settings->dbproc) {
+ dbclose(settings->dbproc);
+ settings->dbproc = NULL;
+ }
+ settings->connected = 0;
+
+ return 0;
+}
+
+static int mssql_connect(void)
+{
+ LOGINREC *login;
+
+ if ((login = dblogin()) == NULL) {
+ ast_log(LOG_ERROR, "Unable to allocate login structure for db-lib\n");
+ return -1;
+ }
+
+ DBSETLAPP(login, "TSQL");
+ DBSETLUSER(login, (char *) settings->username);
+ DBSETLPWD(login, (char *) settings->password);
+
+ if (!ast_strlen_zero(settings->charset)) {
+ DBSETLCHARSET(login, (char *) settings->charset);
+ }
+
+ if (!ast_strlen_zero(settings->language)) {
+ DBSETLNATLANG(login, (char *) settings->language);
+ }
+
+ if ((settings->dbproc = dbopen(login, (char *) settings->connection)) == NULL) {
+ ast_log(LOG_ERROR, "Unable to connect to %s\n", settings->connection);
+ dbloginfree(login);
+ return -1;
+ }
+
+ dbloginfree(login);
+
+ if (dbuse(settings->dbproc, (char *) settings->database) == FAIL) {
+ ast_log(LOG_ERROR, "Unable to select database %s\n", settings->database);
+ goto failed;
+ }
+
+ if (execute_and_consume(settings->dbproc, "SELECT 1 FROM [%s]", settings->table)) {
+ ast_log(LOG_ERROR, "Unable to find table '%s'\n", settings->table);
+ goto failed;
+ }
+
+ settings->connected = 1;
+
+ return 0;
+
+failed:
+ dbclose(settings->dbproc);
+ settings->dbproc = NULL;
+ return -1;
+}
+
+static int tds_unload_module(void)
+{
+ if (event_sub) {
+ event_sub = ast_event_unsubscribe(event_sub);
+ }
+
+ if (settings) {
+ ast_mutex_lock(&tds_lock);
+ mssql_disconnect();
+ ast_mutex_unlock(&tds_lock);
+
+ ast_string_field_free_memory(settings);
+ ast_free(settings);
+ }
+
+ dbexit();
+
+ return 0;
+}
+
+static int tds_error_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr)
+{
+ ast_log(LOG_ERROR, "%s (%d)\n", dberrstr, dberr);
+
+ if (oserr != DBNOERR) {
+ ast_log(LOG_ERROR, "%s (%d)\n", oserrstr, oserr);
+ }
+
+ return INT_CANCEL;
+}
+
+static int tds_message_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severity, char *msgtext, char *srvname, char *procname, int line)
+{
+ ast_debug(1, "Msg %d, Level %d, State %d, Line %d\n", msgno, severity, msgstate, line);
+ ast_log(LOG_NOTICE, "%s\n", msgtext);
+
+ return 0;
+}
+
+static int tds_load_module(int reload)
+{
+ struct ast_config *cfg;
+ const char *ptr = NULL;
+ struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
+
+ cfg = ast_config_load(config, config_flags);
+ if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
+ ast_log(LOG_NOTICE, "Unable to load TDS config for CELs: %s\n", config);
+ return 0;
+ } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
+ return 0;
+ }
+
+ if (!ast_variable_browse(cfg, "global")) {
+ /* nothing configured */
+ ast_config_destroy(cfg);
+ ast_log(LOG_NOTICE, "cel_tds has no global category, nothing to configure.\n");
+ return 0;
+ }
+
+ ast_mutex_lock(&tds_lock);
+
+ /* Clear out any existing settings */
+ ast_string_field_init(settings, 0);
+
+ ptr = ast_variable_retrieve(cfg, "global", "connection");
+ if (ptr) {
+ ast_string_field_set(settings, connection, ptr);
+ } else {
+ ast_log(LOG_ERROR, "Failed to connect: Database connection name not specified.\n");
+ goto failed;
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "dbname");
+ if (ptr) {
+ ast_string_field_set(settings, database, ptr);
+ } else {
+ ast_log(LOG_ERROR, "Failed to connect: Database dbname not specified.\n");
+ goto failed;
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "user");
+ if (ptr) {
+ ast_string_field_set(settings, username, ptr);
+ } else {
+ ast_log(LOG_ERROR, "Failed to connect: Database dbuser not specified.\n");
+ goto failed;
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "password");
+ if (ptr) {
+ ast_string_field_set(settings, password, ptr);
+ } else {
+ ast_log(LOG_ERROR, "Failed to connect: Database password not specified.\n");
+ goto failed;
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "charset");
+ if (ptr) {
+ ast_string_field_set(settings, charset, ptr);
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "language");
+ if (ptr) {
+ ast_string_field_set(settings, language, ptr);
+ }
+
+ ptr = ast_variable_retrieve(cfg, "global", "table");
+ if (ptr) {
+ ast_string_field_set(settings, table, ptr);
+ } else {
+ ast_log(LOG_NOTICE, "Table name not specified, using 'cel' by default.\n");
+ ast_string_field_set(settings, table, "cel");
+ }
+
+ mssql_disconnect();
+
+ if (mssql_connect()) {
+ /* We failed to connect (mssql_connect takes care of logging it) */
+ goto failed;
+ }
+
+ ast_mutex_unlock(&tds_lock);
+ ast_config_destroy(cfg);
+
+ return 1;
+
+failed:
+ ast_mutex_unlock(&tds_lock);
+ ast_config_destroy(cfg);
+
+ return 0;
+}
+
+static int reload(void)
+{
+ return tds_load_module(1);
+}
+
+static int load_module(void)
+{
+ if (dbinit() == FAIL) {
+ ast_log(LOG_ERROR, "Failed to initialize FreeTDS db-lib\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ dberrhandle(tds_error_handler);
+ dbmsghandle(tds_message_handler);
+
+ settings = ast_calloc(1, sizeof(*settings));
+
+ if (!settings || ast_string_field_init(settings, 256)) {
+ if (settings) {
+ ast_free(settings);
+ settings = NULL;
+ }
+ dbexit();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ if (!tds_load_module(0)) {
+ ast_string_field_free_memory(settings);
+ ast_free(settings);
+ settings = NULL;
+ dbexit();
+ ast_log(LOG_WARNING,"cel_tds module had config problems; declining load\n");
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ /* Register MSSQL CEL handler */
+ event_sub = ast_event_subscribe(AST_EVENT_CEL, tds_log, "CEL TDS logging backend", NULL, AST_EVENT_IE_END);
+ if (!event_sub) {
+ ast_log(LOG_ERROR, "Unable to register MSSQL CEL handling\n");
+ ast_string_field_free_memory(settings);
+ ast_free(settings);
+ settings = NULL;
+ dbexit();
+ return AST_MODULE_LOAD_DECLINE;
+ }
+
+ return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+ return tds_unload_module();
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "FreeTDS CEL Backend",
+ .load = load_module,
+ .unload = unload_module,
+ .reload = reload,
+);