diff options
author | russell <russell@f38db490-d61c-443f-a65b-d21fe96a405b> | 2008-01-19 00:19:29 +0000 |
---|---|---|
committer | russell <russell@f38db490-d61c-443f-a65b-d21fe96a405b> | 2008-01-19 00:19:29 +0000 |
commit | f8247040e6231c4b3b5099ea3a526348b7941566 (patch) | |
tree | 0cc92ad6ebf6ae49a62f6e7ef8ec819121d63630 /trunk/cdr | |
parent | d88e56c61ce2042544c1a8a71c93b69ab2e6ffba (diff) |
Creating tag for the release of asterisk-1.6.0-beta1v1.6.0-beta1
git-svn-id: http://svn.digium.com/svn/asterisk/tags/1.6.0-beta1@99163 f38db490-d61c-443f-a65b-d21fe96a405b
Diffstat (limited to 'trunk/cdr')
-rw-r--r-- | trunk/cdr/Makefile | 20 | ||||
-rw-r--r-- | trunk/cdr/cdr_adaptive_odbc.c | 674 | ||||
-rw-r--r-- | trunk/cdr/cdr_csv.c | 346 | ||||
-rw-r--r-- | trunk/cdr/cdr_custom.c | 169 | ||||
-rw-r--r-- | trunk/cdr/cdr_manager.c | 204 | ||||
-rw-r--r-- | trunk/cdr/cdr_odbc.c | 264 | ||||
-rw-r--r-- | trunk/cdr/cdr_pgsql.c | 335 | ||||
-rw-r--r-- | trunk/cdr/cdr_radius.c | 260 | ||||
-rw-r--r-- | trunk/cdr/cdr_sqlite.c | 214 | ||||
-rw-r--r-- | trunk/cdr/cdr_sqlite3_custom.c | 357 | ||||
-rw-r--r-- | trunk/cdr/cdr_tds.c | 531 |
11 files changed, 3374 insertions, 0 deletions
diff --git a/trunk/cdr/Makefile b/trunk/cdr/Makefile new file mode 100644 index 000000000..a2767b31e --- /dev/null +++ b/trunk/cdr/Makefile @@ -0,0 +1,20 @@ +# +# Asterisk -- A telephony toolkit for Linux. +# +# Makefile for CDR backends +# +# Copyright (C) 1999-2006, 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=cdr +MENUSELECT_CATEGORY=CDR +MENUSELECT_DESCRIPTION=Call Detail Recording + +all: _all + +include $(ASTTOPDIR)/Makefile.moddir_rules diff --git a/trunk/cdr/cdr_adaptive_odbc.c b/trunk/cdr/cdr_adaptive_odbc.c new file mode 100644 index 000000000..9daca2cf8 --- /dev/null +++ b/trunk/cdr/cdr_adaptive_odbc.c @@ -0,0 +1,674 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2007, Tilghman Lesher + * + * Tilghman Lesher <cdr_adaptive_odbc__v1@the-tilghman.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 Adaptive ODBC CDR backend + * + * \author Tilghman Lesher <cdr_adaptive_odbc__v1@the-tilghman.com> + * \ingroup cdr_drivers + */ + +/*** MODULEINFO + <depend>unixodbc</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/cdr.h" +#include "asterisk/module.h" + +#define CONFIG "cdr_adaptive_odbc.conf" + +static char *name = "Adaptive ODBC"; +/* Optimization to reduce number of memory allocations */ +static int maxsize = 512, maxsize2 = 512; + +struct columns { + char *name; + char *cdrname; + char *filtervalue; + SQLSMALLINT type; + SQLINTEGER size; + SQLSMALLINT decimals; + SQLSMALLINT radix; + SQLSMALLINT nullable; + SQLINTEGER octetlen; + AST_LIST_ENTRY(columns) list; +}; + +struct tables { + char *connection; + char *table; + 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; + 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) { + ast_log(LOG_WARNING, "Unable to load " CONFIG ". No adaptive ODBC CDRs.\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); + + /* 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 'cdr'.\n"); + tmp = "cdr"; + } + 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->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 CDR 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 *cdrvar = ast_strdupa(var->name + 6); + cdrvar = ast_strip(cdrvar); + ast_verb(3, "Found filter %s for cdr variable %s in %s@%s\n", var->value, cdrvar, tableptr->table, tableptr->connection); + + entry = ast_calloc(sizeof(char), sizeof(*entry) + strlen(cdrvar) + 1 + strlen(var->value) + 1); + if (!entry) { + ast_log(LOG_ERROR, "Out of memory creating filter entry for CDR variable '%s' in table '%s' on connection '%s'\n", cdrvar, table, connection); + res = -1; + break; + } + + /* NULL column entry means this isn't a column in the database */ + entry->name = NULL; + entry->cdrname = (char *)entry + sizeof(*entry); + entry->filtervalue = (char *)entry + sizeof(*entry) + strlen(cdrvar) + 1; + strcpy(entry->cdrname, cdrvar); + strcpy(entry->filtervalue, var->value); + + AST_LIST_INSERT_TAIL(&(tableptr->columns), entry, list); + } + } + + while ((res = SQLFetch(stmt)) != SQL_NO_DATA && res != SQL_ERROR) { + char *cdrvar = ""; + + 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 *tmp = ast_strdupa(var->name + 5); + cdrvar = ast_strip(tmp); + ast_verb(3, "Found alias %s for column %s in %s@%s\n", cdrvar, columnname, tableptr->table, tableptr->connection); + break; + } + } + + entry = ast_calloc(sizeof(char), sizeof(*entry) + strlen(columnname) + 1 + strlen(cdrvar) + 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(cdrvar)) { + entry->cdrname = entry->name + strlen(columnname) + 1; + strcpy(entry->cdrname, cdrvar); + } else /* Point to same place as the column name */ + entry->cdrname = (char *)entry + sizeof(*entry); + + 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 ((newsize = lensql + (size) + 3) > sizesql) { \ + if ((tmp = ast_realloc(sql, (newsize / 512 + 1) * 512))) { \ + sql = tmp; \ + sizesql = (newsize / 512 + 1) * 512; \ + } else { \ + ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CDR '%s:%s' failed.\n", tableptr->connection, tableptr->table); \ + ast_free(sql); \ + ast_free(sql2); \ + AST_RWLIST_UNLOCK(&odbc_tables); \ + return -1; \ + } \ + } \ + } while (0) + +#define LENGTHEN_BUF2(size) \ + do { \ + if ((newsize = lensql2 + (size) + 3) > sizesql2) { \ + if ((tmp = ast_realloc(sql2, (newsize / 512 + 1) * 512))) { \ + sql2 = tmp; \ + sizesql2 = (newsize / 512 + 1) * 512; \ + } else { \ + ast_log(LOG_ERROR, "Unable to allocate sufficient memory. Insert CDR '%s:%s' failed.\n", tableptr->connection, tableptr->table); \ + ast_free(sql); \ + ast_free(sql2); \ + AST_RWLIST_UNLOCK(&odbc_tables); \ + return -1; \ + } \ + } \ + } while (0) + +static int odbc_log(struct ast_cdr *cdr) +{ + struct tables *tableptr; + struct columns *entry; + struct odbc_obj *obj; + int lensql, lensql2, sizesql = maxsize, sizesql2 = maxsize2, newsize; + /* Allocated, so we can realloc() */ + char *sql = ast_calloc(sizeof(char), sizesql), *sql2 = ast_calloc(sizeof(char), sizesql2), *tmp; + char colbuf[1024], *colptr; + SQLHSTMT stmt = NULL; + SQLLEN rows = 0; + + if (!sql || !sql2) { + if (sql) + ast_free(sql); + if (sql2) + ast_free(sql2); + return -1; + } + + if (AST_RWLIST_RDLOCK(&odbc_tables)) { + ast_log(LOG_ERROR, "Unable to lock table list. Insert CDR(s) failed.\n"); + ast_free(sql); + ast_free(sql2); + return -1; + } + + AST_LIST_TRAVERSE(&odbc_tables, tableptr, list) { + lensql = snprintf(sql, sizesql, "INSERT INTO %s (", tableptr->table); + lensql2 = snprintf(sql2, sizesql2, " 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, "cdr_adaptive_odbc: Unable to retrieve database handle for '%s:%s'. CDR failed: %s\n", tableptr->connection, tableptr->table, sql); + continue; + } + + AST_LIST_TRAVERSE(&(tableptr->columns), entry, list) { + /* Check if we have a similarly named variable */ + ast_cdr_getvar(cdr, entry->cdrname, &colptr, colbuf, sizeof(colbuf), 0, + (strcasecmp(entry->cdrname, "start") == 0 || + strcasecmp(entry->cdrname, "answer") == 0 || + strcasecmp(entry->cdrname, "end") == 0) ? 0 : 1); + + 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, "CDR column '%s' with value '%s' does not match filter of" + " '%s'. Cancelling this CDR.\n", + entry->cdrname, 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, "disposition") == 0) + ast_cdr_getvar(cdr, entry->name, &colptr, colbuf, sizeof(colbuf), 0, 0); + else if (strcasecmp(entry->name, "amaflags") == 0) + ast_cdr_getvar(cdr, entry->name, &colptr, colbuf, sizeof(colbuf), 0, 0); + + /* Truncate too-long fields */ + if (entry->type != SQL_GUID) { + if (strlen(colptr) > entry->octetlen) + colptr[entry->octetlen] = '\0'; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(strlen(colptr)); + + /* Encode value, with escaping */ + strcpy(sql2 + lensql2, "'"); + lensql2++; + for (tmp = colptr; *tmp; tmp++) { + if (*tmp == '\'') { + strcpy(sql2 + lensql2, "''"); + lensql2 += 2; + } else if (*tmp == '\\' && ast_odbc_backslash_is_escape(obj)) { + strcpy(sql2 + lensql2, "\\\\"); + lensql2 += 2; + } else { + sql2[lensql2++] = *tmp; + sql2[lensql2] = '\0'; + } + } + strcpy(sql2 + lensql2, "',"); + lensql2 += 2; + 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, "CDR variable %s is not a valid date ('%s').\n", entry->name, colptr); + break; + } + + if (year > 0 && year < 100) + year += 2000; + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(17); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "{ d '%04d-%02d-%02d' },", 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, "CDR variable %s is not a valid time ('%s').\n", entry->name, colptr); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(15); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "{ t '%02d:%02d:%02d' },", 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, "CDR variable %s is not a valid timestamp ('%s').\n", entry->name, colptr); + break; + } + + if (year > 0 && year < 100) + year += 2000; + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(26); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "{ ts '%04d-%02d-%02d %02d:%02d:%02d' },", year, month, day, hour, minute, second); + } + break; + case SQL_INTEGER: + { + int integer = 0; + if (sscanf(colptr, "%d", &integer) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an integer.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(12); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%d,", integer); + } + break; + case SQL_BIGINT: + { + long long integer = 0; + if (sscanf(colptr, "%lld", &integer) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an integer.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(24); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%lld,", integer); + } + break; + case SQL_SMALLINT: + { + short integer = 0; + if (sscanf(colptr, "%hd", &integer) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an integer.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(6); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%d,", integer); + } + break; + case SQL_TINYINT: + { + char integer = 0; + if (sscanf(colptr, "%hhd", &integer) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an integer.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(4); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%d,", integer); + } + break; + case SQL_BIT: + { + char integer = 0; + if (sscanf(colptr, "%hhd", &integer) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an integer.\n", entry->name); + break; + } + if (integer != 0) + integer = 1; + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(2); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%d,", integer); + } + break; + case SQL_NUMERIC: + case SQL_DECIMAL: + { + double number = 0.0; + if (sscanf(colptr, "%lf", &number) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an numeric type.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(entry->decimals); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%*.*lf,", entry->decimals, entry->radix, number); + } + break; + case SQL_FLOAT: + case SQL_REAL: + case SQL_DOUBLE: + { + double number = 0.0; + if (sscanf(colptr, "%lf", &number) != 1) { + ast_log(LOG_WARNING, "CDR variable %s is not an numeric type.\n", entry->name); + break; + } + + lensql += snprintf(sql + lensql, sizesql - lensql, "%s,", entry->name); + LENGTHEN_BUF2(entry->decimals); + lensql2 += snprintf(sql2 + lensql2, sizesql2 - lensql2, "%lf,", 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); + } + } + } + + /* Concatenate the two constructed buffers */ + LENGTHEN_BUF1(lensql2); + sql[lensql - 1] = ')'; + sql2[lensql2 - 1] = ')'; + strcat(sql + lensql, sql2); + + ast_verb(11, "[%s]\n", sql); + + stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, sql); + if (stmt) { + SQLRowCount(stmt, &rows); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + } + if (rows == 0) { + ast_log(LOG_WARNING, "cdr_adaptive_odbc: Insert failed on '%s:%s'. CDR failed: %s\n", tableptr->connection, tableptr->table, 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 (sizesql > maxsize) + maxsize = sizesql; + if (sizesql2 > maxsize2) + maxsize2 = sizesql2; + + ast_free(sql); + ast_free(sql2); + return 0; +} + +static int unload_module(void) +{ + ast_cdr_unregister(name); + usleep(1); + if (AST_RWLIST_WRLOCK(&odbc_tables)) { + ast_cdr_register(name, ast_module_info->description, odbc_log); + 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); + ast_cdr_register(name, ast_module_info->description, odbc_log); + return 0; +} + +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 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Adaptive ODBC CDR backend", + .load = load_module, + .unload = unload_module, + .reload = reload, +); + diff --git a/trunk/cdr/cdr_csv.c b/trunk/cdr/cdr_csv.c new file mode 100644 index 000000000..cca1e8714 --- /dev/null +++ b/trunk/cdr/cdr_csv.c @@ -0,0 +1,346 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 1999 - 2005, Digium, Inc. + * + * Mark Spencer <markster@digium.com> + * + * Includes code and algorithms from the Zapata library. + * + * 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 Comma Separated Value CDR records. + * + * \author Mark Spencer <markster@digium.com> + * + * \arg See also \ref AstCDR + * \ingroup cdr_drivers + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> + +#include "asterisk/paths.h" /* use ast_config_AST_LOG_DIR */ +#include "asterisk/config.h" +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/utils.h" +#include "asterisk/lock.h" + +#define CSV_LOG_DIR "/cdr-csv" +#define CSV_MASTER "/Master.csv" + +#define DATE_FORMAT "%Y-%m-%d %T" + +static int usegmtime = 0; +static int loguniqueid = 0; +static int loguserfield = 0; +static char *config = "cdr.conf"; + +/* #define CSV_LOGUNIQUEID 1 */ +/* #define CSV_LOGUSERFIELD 1 */ + +/*---------------------------------------------------- + The values are as follows: + + + "accountcode", accountcode is the account name of detail records, Master.csv contains all records * + Detail records are configured on a channel basis, IAX and SIP are determined by user * + Zap is determined by channel in zaptel.conf + "source", + "destination", + "destination context", + "callerid", + "channel", + "destination channel", (if applicable) + "last application", Last application run on the channel + "last app argument", argument to the last channel + "start time", + "answer time", + "end time", + duration, Duration is the whole length that the entire call lasted. ie. call rx'd to hangup + "end time" minus "start time" + billable seconds, the duration that a call was up after other end answered which will be <= to duration + "end time" minus "answer time" + "disposition", ANSWERED, NO ANSWER, BUSY + "amaflags", DOCUMENTATION, BILL, IGNORE etc, specified on a per channel basis like accountcode. + "uniqueid", unique call identifier + "userfield" user field set via SetCDRUserField +----------------------------------------------------------*/ + +static char *name = "csv"; + +AST_MUTEX_DEFINE_STATIC(mf_lock); +AST_MUTEX_DEFINE_STATIC(acf_lock); + +static int load_config(int reload) +{ + struct ast_config *cfg; + struct ast_variable *var; + const char *tmp; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + + usegmtime = 0; + loguniqueid = 0; + loguserfield = 0; + + if (!(cfg = ast_config_load(config, config_flags))) { + ast_log(LOG_WARNING, "unable to load config: %s\n", config); + return 0; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + if (!(var = ast_variable_browse(cfg, "csv"))) { + ast_config_destroy(cfg); + return 0; + } + + if ((tmp = ast_variable_retrieve(cfg, "csv", "usegmtime"))) { + usegmtime = ast_true(tmp); + if (usegmtime) + ast_debug(1, "logging time in GMT\n"); + } + + if ((tmp = ast_variable_retrieve(cfg, "csv", "loguniqueid"))) { + loguniqueid = ast_true(tmp); + if (loguniqueid) + ast_debug(1, "logging CDR field UNIQUEID\n"); + } + + if ((tmp = ast_variable_retrieve(cfg, "csv", "loguserfield"))) { + loguserfield = ast_true(tmp); + if (loguserfield) + ast_debug(1, "logging CDR user-defined field\n"); + } + + ast_config_destroy(cfg); + return 1; +} + +static int append_string(char *buf, char *s, size_t bufsize) +{ + int pos = strlen(buf), spos = 0, error = -1; + + if (pos >= bufsize - 4) + return -1; + + buf[pos++] = '\"'; + + while(pos < bufsize - 3) { + if (!s[spos]) { + error = 0; + break; + } + if (s[spos] == '\"') + buf[pos++] = '\"'; + buf[pos++] = s[spos]; + spos++; + } + + buf[pos++] = '\"'; + buf[pos++] = ','; + buf[pos++] = '\0'; + + return error; +} + +static int append_int(char *buf, int s, size_t bufsize) +{ + char tmp[32]; + int pos = strlen(buf); + + snprintf(tmp, sizeof(tmp), "%d", s); + + if (pos + strlen(tmp) > bufsize - 3) + return -1; + + strncat(buf, tmp, bufsize - strlen(buf) - 1); + pos = strlen(buf); + buf[pos++] = ','; + buf[pos++] = '\0'; + + return 0; +} + +static int append_date(char *buf, struct timeval tv, size_t bufsize) +{ + char tmp[80] = ""; + struct ast_tm tm; + + if (strlen(buf) > bufsize - 3) + return -1; + + if (ast_tvzero(tv)) { + strncat(buf, ",", bufsize - strlen(buf) - 1); + return 0; + } + + ast_localtime(&tv, &tm, usegmtime ? "GMT" : NULL); + ast_strftime(tmp, sizeof(tmp), DATE_FORMAT, &tm); + + return append_string(buf, tmp, bufsize); +} + +static int build_csv_record(char *buf, size_t bufsize, struct ast_cdr *cdr) +{ + + buf[0] = '\0'; + /* Account code */ + append_string(buf, cdr->accountcode, bufsize); + /* Source */ + append_string(buf, cdr->src, bufsize); + /* Destination */ + append_string(buf, cdr->dst, bufsize); + /* Destination context */ + append_string(buf, cdr->dcontext, bufsize); + /* Caller*ID */ + append_string(buf, cdr->clid, bufsize); + /* Channel */ + append_string(buf, cdr->channel, bufsize); + /* Destination Channel */ + append_string(buf, cdr->dstchannel, bufsize); + /* Last Application */ + append_string(buf, cdr->lastapp, bufsize); + /* Last Data */ + append_string(buf, cdr->lastdata, bufsize); + /* Start Time */ + append_date(buf, cdr->start, bufsize); + /* Answer Time */ + append_date(buf, cdr->answer, bufsize); + /* End Time */ + append_date(buf, cdr->end, bufsize); + /* Duration */ + append_int(buf, cdr->duration, bufsize); + /* Billable seconds */ + append_int(buf, cdr->billsec, bufsize); + /* Disposition */ + append_string(buf, ast_cdr_disp2str(cdr->disposition), bufsize); + /* AMA Flags */ + append_string(buf, ast_cdr_flags2str(cdr->amaflags), bufsize); + /* Unique ID */ + if (loguniqueid) + append_string(buf, cdr->uniqueid, bufsize); + /* append the user field */ + if(loguserfield) + append_string(buf, cdr->userfield,bufsize); + /* If we hit the end of our buffer, log an error */ + if (strlen(buf) < bufsize - 5) { + /* Trim off trailing comma */ + buf[strlen(buf) - 1] = '\0'; + strncat(buf, "\n", bufsize - strlen(buf) - 1); + return 0; + } + return -1; +} + +static int writefile(char *s, char *acc) +{ + char tmp[PATH_MAX]; + FILE *f; + + if (strchr(acc, '/') || (acc[0] == '.')) { + ast_log(LOG_WARNING, "Account code '%s' insecure for writing file\n", acc); + return -1; + } + + snprintf(tmp, sizeof(tmp), "%s/%s/%s.csv", ast_config_AST_LOG_DIR,CSV_LOG_DIR, acc); + + ast_mutex_lock(&acf_lock); + if (!(f = fopen(tmp, "a"))) { + ast_mutex_unlock(&acf_lock); + ast_log(LOG_ERROR, "Unable to open file %s : %s\n", tmp, strerror(errno)); + return -1; + } + fputs(s, f); + fflush(f); + fclose(f); + ast_mutex_unlock(&acf_lock); + + return 0; +} + + +static int csv_log(struct ast_cdr *cdr) +{ + FILE *mf = NULL; + /* Make sure we have a big enough buf */ + char buf[1024]; + char csvmaster[PATH_MAX]; + snprintf(csvmaster, sizeof(csvmaster),"%s/%s/%s", ast_config_AST_LOG_DIR, CSV_LOG_DIR, CSV_MASTER); +#if 0 + printf("[CDR] %s ('%s' -> '%s') Dur: %ds Bill: %ds Disp: %s Flags: %s Account: [%s]\n", cdr->channel, cdr->src, cdr->dst, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), ast_cdr_flags2str(cdr->amaflags), cdr->accountcode); +#endif + if (build_csv_record(buf, sizeof(buf), cdr)) { + ast_log(LOG_WARNING, "Unable to create CSV record in %d bytes. CDR not recorded!\n", (int)sizeof(buf)); + return 0; + } + + /* 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 */ + ast_mutex_lock(&mf_lock); + if ((mf = fopen(csvmaster, "a"))) { + fputs(buf, mf); + fflush(mf); /* be particularly anal here */ + fclose(mf); + mf = NULL; + ast_mutex_unlock(&mf_lock); + } else { + ast_mutex_unlock(&mf_lock); + ast_log(LOG_ERROR, "Unable to re-open master file %s : %s\n", csvmaster, strerror(errno)); + } + + if (!ast_strlen_zero(cdr->accountcode)) { + if (writefile(buf, cdr->accountcode)) + ast_log(LOG_WARNING, "Unable to write CSV record to account file '%s' : %s\n", cdr->accountcode, strerror(errno)); + } + + return 0; +} + +static int unload_module(void) +{ + ast_cdr_unregister(name); + return 0; +} + +static int load_module(void) +{ + int res; + + if(!load_config(0)) + return AST_MODULE_LOAD_DECLINE; + + if ((res = ast_cdr_register(name, ast_module_info->description, csv_log))) + ast_log(LOG_ERROR, "Unable to register CSV CDR handling\n"); + + return res; +} + +static int reload(void) +{ + load_config(1); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Comma Separated Values CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); diff --git a/trunk/cdr/cdr_custom.c b/trunk/cdr/cdr_custom.c new file mode 100644 index 000000000..fbfb8d527 --- /dev/null +++ b/trunk/cdr/cdr_custom.c @@ -0,0 +1,169 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 1999 - 2005, Digium, Inc. + * + * Mark Spencer <markster@digium.com> + * + * Includes code and algorithms from the Zapata library. + * + * 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 CDR records. + * + * \author Mark Spencer <markster@digium.com> + * + * \arg See also \ref AstCDR + * + * Logs in LOG_DIR/cdr_custom + * \ingroup cdr_drivers + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> + +#include "asterisk/paths.h" /* use ast_config_AST_LOG_DIR */ +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/config.h" +#include "asterisk/pbx.h" +#include "asterisk/utils.h" + +#define CUSTOM_LOG_DIR "/cdr_custom" + +#define DATE_FORMAT "%Y-%m-%d %T" + +AST_MUTEX_DEFINE_STATIC(lock); + +static char *name = "cdr-custom"; + +static FILE *mf = NULL; + +static char master[PATH_MAX]; +static char format[1024]=""; + +static int load_config(int reload) +{ + struct ast_config *cfg; + struct ast_variable *var; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + int res = -1; + + if ((cfg = ast_config_load("cdr_custom.conf", config_flags)) == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + strcpy(format, ""); + strcpy(master, ""); + ast_mutex_lock(&lock); + if (cfg) { + var = ast_variable_browse(cfg, "mappings"); + while(var) { + if (!ast_strlen_zero(var->name) && !ast_strlen_zero(var->value)) { + if (strlen(var->value) > (sizeof(format) - 1)) + ast_log(LOG_WARNING, "Format string too long, will be truncated, at line %d\n", var->lineno); + ast_copy_string(format, var->value, sizeof(format) - 1); + strcat(format,"\n"); + snprintf(master, sizeof(master),"%s/%s/%s", ast_config_AST_LOG_DIR, name, var->name); + if (var->next) { + ast_log(LOG_NOTICE, "Sorry, only one mapping is supported at this time, mapping '%s' will be ignored at line %d.\n", var->next->name, var->next->lineno); + break; + } + } else + ast_log(LOG_NOTICE, "Mapping must have both filename and format at line %d\n", var->lineno); + var = var->next; + } + ast_config_destroy(cfg); + res = 0; + } else { + if (reload) + ast_log(LOG_WARNING, "Failed to reload configuration file.\n"); + else + ast_log(LOG_WARNING, "Failed to load configuration file. Module not activated.\n"); + } + ast_mutex_unlock(&lock); + + return res; +} + + + +static int custom_log(struct ast_cdr *cdr) +{ + /* Make sure we have a big enough buf */ + char buf[2048]; + struct ast_channel dummy; + + /* Abort if no master file is specified */ + if (ast_strlen_zero(master)) + return 0; + + /* Quite possibly the first use of a static struct ast_channel, we need it so the var funcs will work */ + memset(&dummy, 0, sizeof(dummy)); + dummy.cdr = cdr; + pbx_substitute_variables_helper(&dummy, format, buf, sizeof(buf) - 1); + + /* 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 */ + mf = fopen(master, "a"); + if (!mf) { + ast_log(LOG_ERROR, "Unable to re-open master file %s : %s\n", master, strerror(errno)); + } + if (mf) { + fputs(buf, mf); + fflush(mf); /* be particularly anal here */ + fclose(mf); + mf = NULL; + } + return 0; +} + +static int unload_module(void) +{ + if (mf) + fclose(mf); + ast_cdr_unregister(name); + return 0; +} + +static int load_module(void) +{ + int res = 0; + + if (!load_config(0)) { + res = ast_cdr_register(name, ast_module_info->description, custom_log); + if (res) + ast_log(LOG_ERROR, "Unable to register custom CDR handling\n"); + if (mf) + fclose(mf); + return res; + } else + return AST_MODULE_LOAD_DECLINE; +} + +static int reload(void) +{ + return load_config(1); +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Customizable Comma Separated Values CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); + diff --git a/trunk/cdr/cdr_manager.c b/trunk/cdr/cdr_manager.c new file mode 100644 index 000000000..8b2ab215d --- /dev/null +++ b/trunk/cdr/cdr_manager.c @@ -0,0 +1,204 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2004 - 2005 + * + * 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 Call Manager CDR records. + * + * See also + * \arg \ref AstCDR + * \arg \ref AstAMI + * \arg \ref Config_ami + * \ingroup cdr_drivers + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> + +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/utils.h" +#include "asterisk/manager.h" +#include "asterisk/config.h" +#include "asterisk/pbx.h" + +#define DATE_FORMAT "%Y-%m-%d %T" +#define CONF_FILE "cdr_manager.conf" +#define CUSTOM_FIELDS_BUF_SIZE 1024 + +static char *name = "cdr_manager"; + +static int enablecdr = 0; +struct ast_str *customfields; + +static int manager_log(struct ast_cdr *cdr); + +static int load_config(int reload) +{ + char *cat = NULL; + struct ast_config *cfg; + struct ast_variable *v; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + int newenablecdr = 0; + + cfg = ast_config_load(CONF_FILE, config_flags); + if (cfg == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + if (reload && customfields) { + ast_free(customfields); + } + customfields = NULL; + + if (!cfg) { + /* Standard configuration */ + ast_log(LOG_WARNING, "Failed to load configuration file. Module not activated.\n"); + if (enablecdr) + ast_cdr_unregister(name); + enablecdr = 0; + return 0; + } + + while ( (cat = ast_category_browse(cfg, cat)) ) { + if (!strcasecmp(cat, "general")) { + v = ast_variable_browse(cfg, cat); + while (v) { + if (!strcasecmp(v->name, "enabled")) + newenablecdr = ast_true(v->value); + + v = v->next; + } + } else if (!strcasecmp(cat, "mappings")) { + customfields = ast_str_create(CUSTOM_FIELDS_BUF_SIZE); + v = ast_variable_browse(cfg, cat); + while (v) { + if (customfields && !ast_strlen_zero(v->name) && !ast_strlen_zero(v->value)) { + if( (customfields->used + strlen(v->value) + strlen(v->name) + 14) < customfields->len) { + ast_str_append(&customfields, -1, "%s: ${CDR(%s)}\r\n", v->value, v->name); + ast_log(LOG_NOTICE, "Added mapping %s: ${CDR(%s)}\n", v->value, v->name); + } else { + ast_log(LOG_WARNING, "No more buffer space to add other custom fields\n"); + break; + } + + } + v = v->next; + } + } + } + + ast_config_destroy(cfg); + + if (enablecdr && !newenablecdr) + ast_cdr_unregister(name); + else if (!enablecdr && newenablecdr) + ast_cdr_register(name, "Asterisk Manager Interface CDR Backend", manager_log); + enablecdr = newenablecdr; + + return 1; +} + +static int manager_log(struct ast_cdr *cdr) +{ + struct ast_tm timeresult; + char strStartTime[80] = ""; + char strAnswerTime[80] = ""; + char strEndTime[80] = ""; + char buf[CUSTOM_FIELDS_BUF_SIZE]; + struct ast_channel dummy; + + if (!enablecdr) + return 0; + + ast_localtime(&cdr->start, &timeresult, NULL); + ast_strftime(strStartTime, sizeof(strStartTime), DATE_FORMAT, &timeresult); + + if (cdr->answer.tv_sec) { + ast_localtime(&cdr->answer, &timeresult, NULL); + ast_strftime(strAnswerTime, sizeof(strAnswerTime), DATE_FORMAT, &timeresult); + } + + ast_localtime(&cdr->end, &timeresult, NULL); + ast_strftime(strEndTime, sizeof(strEndTime), DATE_FORMAT, &timeresult); + + buf[0] = 0; + /* Custom fields handling */ + if (customfields != NULL && customfields->used > 0) { + memset(&dummy, 0, sizeof(dummy)); + dummy.cdr = cdr; + pbx_substitute_variables_helper(&dummy, customfields->str, buf, sizeof(buf) - 1); + } + + manager_event(EVENT_FLAG_CDR, "Cdr", + "AccountCode: %s\r\n" + "Source: %s\r\n" + "Destination: %s\r\n" + "DestinationContext: %s\r\n" + "CallerID: %s\r\n" + "Channel: %s\r\n" + "DestinationChannel: %s\r\n" + "LastApplication: %s\r\n" + "LastData: %s\r\n" + "StartTime: %s\r\n" + "AnswerTime: %s\r\n" + "EndTime: %s\r\n" + "Duration: %ld\r\n" + "BillableSeconds: %ld\r\n" + "Disposition: %s\r\n" + "AMAFlags: %s\r\n" + "UniqueID: %s\r\n" + "UserField: %s\r\n" + "%s", + cdr->accountcode, cdr->src, cdr->dst, cdr->dcontext, cdr->clid, cdr->channel, + cdr->dstchannel, cdr->lastapp, cdr->lastdata, strStartTime, strAnswerTime, strEndTime, + cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), + ast_cdr_flags2str(cdr->amaflags), cdr->uniqueid, cdr->userfield,buf); + + return 0; +} + +static int unload_module(void) +{ + ast_cdr_unregister(name); + if (customfields) + ast_free(customfields); + + return 0; +} + +static int load_module(void) +{ + /* Configuration file */ + 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 CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); diff --git a/trunk/cdr/cdr_odbc.c b/trunk/cdr/cdr_odbc.c new file mode 100644 index 000000000..af02d393c --- /dev/null +++ b/trunk/cdr/cdr_odbc.c @@ -0,0 +1,264 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2003-2005, Digium, Inc. + * + * Brian K. West <brian@bkw.org> + * + * 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 ODBC CDR Backend + * + * \author Brian K. West <brian@bkw.org> + * + * See also: + * \arg http://www.unixodbc.org + * \arg \ref Config_cdr + * \ingroup cdr_drivers + */ + +/*** MODULEINFO + <depend>unixodbc</depend> + <depend>ltdl</depend> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> + +#include "asterisk/config.h" +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/res_odbc.h" + +#define DATE_FORMAT "%Y-%m-%d %T" + +static char *name = "ODBC"; +static char *config_file = "cdr_odbc.conf"; +static char *dsn = NULL, *table = NULL; + +enum { + CONFIG_LOGUNIQUEID = 1 << 0, + CONFIG_USEGMTIME = 1 << 1, + CONFIG_DISPOSITIONSTRING = 1 << 2, +}; + +static struct ast_flags config = { 0 }; + +static SQLHSTMT prepare_cb(struct odbc_obj *obj, void *data) +{ + struct ast_cdr *cdr = data; + SQLRETURN ODBC_res; + char sqlcmd[2048] = "", timestr[128]; + struct ast_tm tm; + SQLHSTMT stmt; + + ast_localtime(&cdr->start, &tm, ast_test_flag(&config, CONFIG_USEGMTIME) ? "GMT" : NULL); + ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, &tm); + + if (ast_test_flag(&config, CONFIG_LOGUNIQUEID)) { + snprintf(sqlcmd,sizeof(sqlcmd),"INSERT INTO %s " + "(calldate,clid,src,dst,dcontext,channel,dstchannel,lastapp," + "lastdata,duration,billsec,disposition,amaflags,accountcode,uniqueid,userfield) " + "VALUES ({ts '%s'},?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", table, timestr); + } else { + snprintf(sqlcmd,sizeof(sqlcmd),"INSERT INTO %s " + "(calldate,clid,src,dst,dcontext,channel,dstchannel,lastapp,lastdata," + "duration,billsec,disposition,amaflags,accountcode) " + "VALUES ({ts '%s'},?,?,?,?,?,?,?,?,?,?,?,?,?)", table, timestr); + } + + ODBC_res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt); + + if ((ODBC_res != SQL_SUCCESS) && (ODBC_res != SQL_SUCCESS_WITH_INFO)) { + ast_verb(11, "cdr_odbc: Failure in AllocStatement %d\n", ODBC_res); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + return NULL; + } + + ODBC_res = SQLPrepare(stmt, (unsigned char *)sqlcmd, SQL_NTS); + + if ((ODBC_res != SQL_SUCCESS) && (ODBC_res != SQL_SUCCESS_WITH_INFO)) { + ast_verb(11, "cdr_odbc: Error in PREPARE %d\n", ODBC_res); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + return NULL; + } + + SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->clid), 0, cdr->clid, 0, NULL); + SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->src), 0, cdr->src, 0, NULL); + SQLBindParameter(stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->dst), 0, cdr->dst, 0, NULL); + SQLBindParameter(stmt, 4, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->dcontext), 0, cdr->dcontext, 0, NULL); + SQLBindParameter(stmt, 5, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->channel), 0, cdr->channel, 0, NULL); + SQLBindParameter(stmt, 6, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->dstchannel), 0, cdr->dstchannel, 0, NULL); + SQLBindParameter(stmt, 7, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->lastapp), 0, cdr->lastapp, 0, NULL); + SQLBindParameter(stmt, 8, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->lastdata), 0, cdr->lastdata, 0, NULL); + SQLBindParameter(stmt, 9, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &cdr->duration, 0, NULL); + SQLBindParameter(stmt, 10, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &cdr->billsec, 0, NULL); + if (ast_test_flag(&config, CONFIG_DISPOSITIONSTRING)) + SQLBindParameter(stmt, 11, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(ast_cdr_disp2str(cdr->disposition)) + 1, 0, ast_cdr_disp2str(cdr->disposition), 0, NULL); + else + SQLBindParameter(stmt, 11, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &cdr->disposition, 0, NULL); + SQLBindParameter(stmt, 12, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &cdr->amaflags, 0, NULL); + SQLBindParameter(stmt, 13, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->accountcode), 0, cdr->accountcode, 0, NULL); + + if (ast_test_flag(&config, CONFIG_LOGUNIQUEID)) { + SQLBindParameter(stmt, 14, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->uniqueid), 0, cdr->uniqueid, 0, NULL); + SQLBindParameter(stmt, 15, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, sizeof(cdr->userfield), 0, cdr->userfield, 0, NULL); + } + + return stmt; +} + + +static int odbc_log(struct ast_cdr *cdr) +{ + struct odbc_obj *obj = ast_odbc_request_obj(dsn, 0); + SQLHSTMT stmt; + + if (!obj) { + ast_log(LOG_ERROR, "Unable to retrieve database handle. CDR failed.\n"); + return -1; + } + + stmt = ast_odbc_prepare_and_execute(obj, prepare_cb, cdr); + if (stmt) { + SQLLEN rows = 0; + + SQLRowCount(stmt, &rows); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + + if (rows == 0) + ast_log(LOG_WARNING, "CDR successfully ran, but inserted 0 rows?\n"); + } else + ast_log(LOG_ERROR, "CDR prepare or execute failed\n"); + ast_odbc_release_obj(obj); + return 0; +} + +static int odbc_load_module(int reload) +{ + int res = 0; + struct ast_config *cfg; + struct ast_variable *var; + const char *tmp; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + + do { + cfg = ast_config_load(config_file, config_flags); + if (!cfg) { + ast_log(LOG_WARNING, "cdr_odbc: Unable to load config for ODBC CDR's: %s\n", config_file); + res = AST_MODULE_LOAD_DECLINE; + break; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) + break; + + var = ast_variable_browse(cfg, "global"); + if (!var) { + /* nothing configured */ + break; + } + + if ((tmp = ast_variable_retrieve(cfg, "global", "dsn")) == NULL) { + ast_log(LOG_WARNING, "cdr_odbc: dsn not specified. Assuming asteriskdb\n"); + tmp = "asteriskdb"; + } + if (dsn) + ast_free(dsn); + dsn = ast_strdup(tmp); + if (dsn == NULL) { + res = -1; + break; + } + + if (((tmp = ast_variable_retrieve(cfg, "global", "dispositionstring"))) && ast_true(tmp)) + ast_set_flag(&config, CONFIG_DISPOSITIONSTRING); + else + ast_clear_flag(&config, CONFIG_DISPOSITIONSTRING); + + if (((tmp = ast_variable_retrieve(cfg, "global", "loguniqueid"))) && ast_true(tmp)) { + ast_set_flag(&config, CONFIG_LOGUNIQUEID); + ast_debug(1, "cdr_odbc: Logging uniqueid\n"); + } else { + ast_clear_flag(&config, CONFIG_LOGUNIQUEID); + ast_debug(1, "cdr_odbc: Not logging uniqueid\n"); + } + + if (((tmp = ast_variable_retrieve(cfg, "global", "usegmtime"))) && ast_true(tmp)) { + ast_set_flag(&config, CONFIG_USEGMTIME); + ast_debug(1, "cdr_odbc: Logging in GMT\n"); + } else { + ast_clear_flag(&config, CONFIG_USEGMTIME); + ast_debug(1, "cdr_odbc: Logging in local time\n"); + } + + if ((tmp = ast_variable_retrieve(cfg, "global", "table")) == NULL) { + ast_log(LOG_WARNING, "cdr_odbc: table not specified. Assuming cdr\n"); + tmp = "cdr"; + } + if (table) + ast_free(table); + table = ast_strdup(tmp); + if (table == NULL) { + res = -1; + break; + } + + ast_verb(3, "cdr_odbc: dsn is %s\n", dsn); + ast_verb(3, "cdr_odbc: table is %s\n", table); + + res = ast_cdr_register(name, ast_module_info->description, odbc_log); + if (res) { + ast_log(LOG_ERROR, "cdr_odbc: Unable to register ODBC CDR handling\n"); + } + } while (0); + + if (cfg && cfg != CONFIG_STATUS_FILEUNCHANGED) + ast_config_destroy(cfg); + return res; +} + +static int load_module(void) +{ + return odbc_load_module(0); +} + +static int unload_module(void) +{ + ast_cdr_unregister(name); + + if (dsn) { + ast_verb(11, "cdr_odbc: free dsn\n"); + ast_free(dsn); + } + if (table) { + ast_verb(11, "cdr_odbc: free table\n"); + ast_free(table); + } + + return 0; +} + +static int reload(void) +{ + return odbc_load_module(1); +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "ODBC CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); diff --git a/trunk/cdr/cdr_pgsql.c b/trunk/cdr/cdr_pgsql.c new file mode 100644 index 000000000..eea4416a5 --- /dev/null +++ b/trunk/cdr/cdr_pgsql.c @@ -0,0 +1,335 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2003 - 2006 + * + * Matthew D. Hardeman <mhardemn@papersoft.com> + * Adapted from the MySQL CDR logger originally by James Sharp + * + * 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 CDR logger + * + * \author Matthew D. Hardeman <mhardemn@papersoft.com> + * \extref PostgreSQL http://www.postgresql.org/ + * + * See also + * \arg \ref Config_cdr + * \arg http://www.postgresql.org/ + * \ingroup cdr_drivers + */ + +/*** MODULEINFO + <depend>pgsql</depend> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> + +#include <libpq-fe.h> + +#include "asterisk/config.h" +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" + +#define DATE_FORMAT "%Y-%m-%d %T" + +static char *name = "pgsql"; +static char *config = "cdr_pgsql.conf"; +static char *pghostname = NULL, *pgdbname = NULL, *pgdbuser = NULL, *pgpassword = NULL, *pgdbport = NULL, *table = NULL; +static int connected = 0; + +AST_MUTEX_DEFINE_STATIC(pgsql_lock); + +static PGconn *conn = NULL; + +static int pgsql_log(struct ast_cdr *cdr) +{ + struct ast_tm tm; + char sqlcmd[2048] = "", timestr[128]; + char *pgerror; + PGresult *result; + + ast_mutex_lock(&pgsql_lock); + + ast_localtime(&cdr->start, &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, "cdr_pgsql: Unable to connect to database server %s. Calls will not be logged!\n", pghostname); + ast_log(LOG_ERROR, "cdr_pgsql: Reason: %s\n", pgerror); + PQfinish(conn); + conn = NULL; + } + } + + if (connected) { + char *clid=NULL, *dcontext=NULL, *channel=NULL, *dstchannel=NULL, *lastapp=NULL, *lastdata=NULL; + char *src=NULL, *dst=NULL, *uniqueid=NULL, *userfield=NULL; + int pgerr; + + /* Maximum space needed would be if all characters needed to be escaped, plus a trailing NULL */ + if ((clid = alloca(strlen(cdr->clid) * 2 + 1)) != NULL) + PQescapeStringConn(conn, clid, cdr->clid, strlen(cdr->clid), &pgerr); + if ((dcontext = alloca(strlen(cdr->dcontext) * 2 + 1)) != NULL) + PQescapeStringConn(conn, dcontext, cdr->dcontext, strlen(cdr->dcontext), &pgerr); + if ((channel = alloca(strlen(cdr->channel) * 2 + 1)) != NULL) + PQescapeStringConn(conn, channel, cdr->channel, strlen(cdr->channel), &pgerr); + if ((dstchannel = alloca(strlen(cdr->dstchannel) * 2 + 1)) != NULL) + PQescapeStringConn(conn, dstchannel, cdr->dstchannel, strlen(cdr->dstchannel), &pgerr); + if ((lastapp = alloca(strlen(cdr->lastapp) * 2 + 1)) != NULL) + PQescapeStringConn(conn, lastapp, cdr->lastapp, strlen(cdr->lastapp), &pgerr); + if ((lastdata = alloca(strlen(cdr->lastdata) * 2 + 1)) != NULL) + PQescapeStringConn(conn, lastdata, cdr->lastdata, strlen(cdr->lastdata), &pgerr); + if ((uniqueid = alloca(strlen(cdr->uniqueid) * 2 + 1)) != NULL) + PQescapeStringConn(conn, uniqueid, cdr->uniqueid, strlen(cdr->uniqueid), &pgerr); + if ((userfield = alloca(strlen(cdr->userfield) * 2 + 1)) != NULL) + PQescapeStringConn(conn, userfield, cdr->userfield, strlen(cdr->userfield), &pgerr); + if ((src = alloca(strlen(cdr->src) * 2 + 1)) != NULL) + PQescapeStringConn(conn, src, cdr->src, strlen(cdr->src), &pgerr); + if ((dst = alloca(strlen(cdr->dst) * 2 + 1)) != NULL) + PQescapeStringConn(conn, dst, cdr->dst, strlen(cdr->dst), &pgerr); + + /* Check for all alloca failures above at once */ + if ((!clid) || (!dcontext) || (!channel) || (!dstchannel) || (!lastapp) || (!lastdata) || (!uniqueid) || (!userfield) || (!src) || (!dst)) { + ast_log(LOG_ERROR, "cdr_pgsql: Out of memory error (insert fails)\n"); + ast_mutex_unlock(&pgsql_lock); + return -1; + } + + ast_debug(2, "cdr_pgsql: inserting a CDR record.\n"); + + snprintf(sqlcmd,sizeof(sqlcmd),"INSERT INTO %s (calldate,clid,src,dst,dcontext,channel,dstchannel," + "lastapp,lastdata,duration,billsec,disposition,amaflags,accountcode,uniqueid,userfield) VALUES" + " ('%s','%s','%s','%s','%s', '%s','%s','%s','%s',%ld,%ld,'%s',%ld,'%s','%s','%s')", + table, timestr, clid, src, dst, dcontext, channel, dstchannel, lastapp, lastdata, + cdr->duration,cdr->billsec,ast_cdr_disp2str(cdr->disposition),cdr->amaflags, cdr->accountcode, uniqueid, userfield); + + ast_debug(3, "cdr_pgsql: SQL command executed: %s\n",sqlcmd); + + /* 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, "cdr_pgsql: Connection was lost... attempting to reconnect.\n"); + PQreset(conn); + if (PQstatus(conn) == CONNECTION_OK) { + ast_log(LOG_ERROR, "cdr_pgsql: Connection reestablished.\n"); + connected = 1; + } else { + pgerror = PQerrorMessage(conn); + ast_log(LOG_ERROR, "cdr_pgsql: Unable to reconnect to database server %s. Calls will not be logged!\n", pghostname); + ast_log(LOG_ERROR, "cdr_pgsql: Reason: %s\n", pgerror); + PQfinish(conn); + conn = NULL; + connected = 0; + ast_mutex_unlock(&pgsql_lock); + return -1; + } + } + result = PQexec(conn, sqlcmd); + if (PQresultStatus(result) != PGRES_COMMAND_OK) { + pgerror = PQresultErrorMessage(result); + ast_log(LOG_ERROR,"cdr_pgsql: Failed to insert call detail record into database!\n"); + ast_log(LOG_ERROR,"cdr_pgsql: Reason: %s\n", pgerror); + ast_log(LOG_ERROR,"cdr_pgsql: Connection may have been lost... attempting to reconnect.\n"); + PQreset(conn); + if (PQstatus(conn) == CONNECTION_OK) { + ast_log(LOG_ERROR, "cdr_pgsql: Connection reestablished.\n"); + connected = 1; + PQclear(result); + result = PQexec(conn, sqlcmd); + if (PQresultStatus(result) != PGRES_COMMAND_OK) { + pgerror = PQresultErrorMessage(result); + ast_log(LOG_ERROR,"cdr_pgsql: HARD ERROR! Attempted reconnection failed. DROPPING CALL RECORD!\n"); + ast_log(LOG_ERROR,"cdr_pgsql: Reason: %s\n", pgerror); + } + } + ast_mutex_unlock(&pgsql_lock); + PQclear(result); + return -1; + } + PQclear(result); + } + ast_mutex_unlock(&pgsql_lock); + return 0; +} + +static int unload_module(void) +{ + 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_cdr_unregister(name); + return 0; +} + +static int config_module(int reload) +{ + struct ast_variable *var; + char *pgerror; + const char *tmp; + struct ast_config *cfg; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + + if ((cfg = ast_config_load(config, config_flags)) == NULL) { + ast_log(LOG_WARNING, "Unable to load config for PostgreSQL CDR's: %s\n", config); + return -1; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + if (!(var = ast_variable_browse(cfg, "global"))) { + ast_config_destroy(cfg); + return 0; + } + + 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_config_destroy(cfg); + return -1; + } + + if (!(tmp = ast_variable_retrieve(cfg, "global", "dbname"))) { + ast_log(LOG_WARNING,"PostgreSQL database not specified. Assuming asterisk\n"); + tmp = "asteriskcdrdb"; + } + + if (pgdbname) + ast_free(pgdbname); + if (!(pgdbname = ast_strdup(tmp))) { + ast_config_destroy(cfg); + return -1; + } + + 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_config_destroy(cfg); + return -1; + } + + 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_config_destroy(cfg); + return -1; + } + + 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_config_destroy(cfg); + return -1; + } + + if (!(tmp = ast_variable_retrieve(cfg, "global", "table"))) { + ast_log(LOG_WARNING,"CDR table not specified. Assuming cdr\n"); + tmp = "cdr"; + } + + if (table) + ast_free(table); + if (!(table = ast_strdup(tmp))) { + ast_config_destroy(cfg); + return -1; + } + + if (option_debug) { + if (ast_strlen_zero(pghostname)) + ast_debug(1, "cdr_pgsql: using default unix socket\n"); + else + ast_debug(1, "cdr_pgsql: got hostname of %s\n", pghostname); + ast_debug(1, "cdr_pgsql: got port of %s\n", pgdbport); + ast_debug(1, "cdr_pgsql: got user of %s\n", pgdbuser); + ast_debug(1, "cdr_pgsql: got dbname of %s\n", pgdbname); + ast_debug(1, "cdr_pgsql: got password of %s\n", pgpassword); + ast_debug(1, "cdr_pgsql: got sql table name of %s\n", table); + } + + conn = PQsetdbLogin(pghostname, pgdbport, NULL, NULL, pgdbname, pgdbuser, pgpassword); + if (PQstatus(conn) != CONNECTION_BAD) { + ast_debug(1, "Successfully connected to PostgreSQL database.\n"); + connected = 1; + } else { + pgerror = PQerrorMessage(conn); + ast_log(LOG_ERROR, "cdr_pgsql: Unable to connect to database server %s. CALLS WILL NOT BE LOGGED!!\n", pghostname); + ast_log(LOG_ERROR, "cdr_pgsql: Reason: %s\n", pgerror); + connected = 0; + } + + ast_config_destroy(cfg); + + return ast_cdr_register(name, ast_module_info->description, pgsql_log); +} + +static int load_module(void) +{ + return config_module(0) ? AST_MODULE_LOAD_DECLINE : 0; +} + +static int reload(void) +{ + return config_module(1); +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "PostgreSQL CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); diff --git a/trunk/cdr/cdr_radius.c b/trunk/cdr/cdr_radius.c new file mode 100644 index 000000000..3f0b2c9f2 --- /dev/null +++ b/trunk/cdr/cdr_radius.c @@ -0,0 +1,260 @@ +/* + * 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 CDR Support + * \author Philippe Sultan + * \extref The Radius Client Library - http://developer.berlios.de/projects/radiusclient-ng/ + * + * \arg See also \ref AstCDR + * \ingroup cdr_drivers + */ + +/*** MODULEINFO + <depend>radius</depend> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Rev$") + +#include <time.h> +#include <radiusclient-ng.h> + +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/utils.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_SRC = 102, + PW_AST_DST = 103, + PW_AST_DST_CTX = 104, + PW_AST_CLID = 105, + PW_AST_CHAN = 106, + PW_AST_DST_CHAN = 107, + PW_AST_LAST_APP = 108, + PW_AST_LAST_DATA = 109, + PW_AST_START_TIME = 110, + PW_AST_ANSWER_TIME = 111, + PW_AST_END_TIME = 112, + PW_AST_DURATION = 113, + PW_AST_BILL_SEC = 114, + PW_AST_DISPOSITION = 115, + PW_AST_AMA_FLAGS = 116, + PW_AST_UNIQUE_ID = 117, + PW_AST_USER_FIELD = 118 +}; + +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 *desc = "RADIUS CDR Backend"; +static char *name = "radius"; +static char *cdr_config = "cdr.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 int build_radius_record(VALUE_PAIR **send, struct ast_cdr *cdr) +{ + int recordtype = PW_STATUS_STOP; + struct ast_tm tm; + char timestr[128]; + char *tmp; + + if (!rc_avpair_add(rh, send, PW_ACCT_STATUS_TYPE, &recordtype, 0, 0)) + return -1; + + /* Account code */ + if (!rc_avpair_add(rh, send, PW_AST_ACCT_CODE, &cdr->accountcode, strlen(cdr->accountcode), VENDOR_CODE)) + return -1; + + /* Source */ + if (!rc_avpair_add(rh, send, PW_AST_SRC, &cdr->src, strlen(cdr->src), VENDOR_CODE)) + return -1; + + /* Destination */ + if (!rc_avpair_add(rh, send, PW_AST_DST, &cdr->dst, strlen(cdr->dst), VENDOR_CODE)) + return -1; + + /* Destination context */ + if (!rc_avpair_add(rh, send, PW_AST_DST_CTX, &cdr->dcontext, strlen(cdr->dcontext), VENDOR_CODE)) + return -1; + + /* Caller ID */ + if (!rc_avpair_add(rh, send, PW_AST_CLID, &cdr->clid, strlen(cdr->clid), VENDOR_CODE)) + return -1; + + /* Channel */ + if (!rc_avpair_add(rh, send, PW_AST_CHAN, &cdr->channel, strlen(cdr->channel), VENDOR_CODE)) + return -1; + + /* Destination Channel */ + if (!rc_avpair_add(rh, send, PW_AST_DST_CHAN, &cdr->dstchannel, strlen(cdr->dstchannel), VENDOR_CODE)) + return -1; + + /* Last Application */ + if (!rc_avpair_add(rh, send, PW_AST_LAST_APP, &cdr->lastapp, strlen(cdr->lastapp), VENDOR_CODE)) + return -1; + + /* Last Data */ + if (!rc_avpair_add(rh, send, PW_AST_LAST_DATA, &cdr->lastdata, strlen(cdr->lastdata), VENDOR_CODE)) + return -1; + + + /* Start Time */ + ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, + ast_localtime(&cdr->start, &tm, + ast_test_flag(&global_flags, RADIUS_FLAG_USEGMTIME) ? "GMT" : NULL)); + if (!rc_avpair_add(rh, send, PW_AST_START_TIME, timestr, strlen(timestr), VENDOR_CODE)) + return -1; + + /* Answer Time */ + ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, + ast_localtime(&cdr->answer, &tm, + ast_test_flag(&global_flags, RADIUS_FLAG_USEGMTIME) ? "GMT" : NULL)); + if (!rc_avpair_add(rh, send, PW_AST_ANSWER_TIME, timestr, strlen(timestr), VENDOR_CODE)) + return -1; + + /* End Time */ + ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, + ast_localtime(&cdr->end, &tm, + ast_test_flag(&global_flags, RADIUS_FLAG_USEGMTIME) ? "GMT" : NULL)); + if (!rc_avpair_add(rh, send, PW_AST_END_TIME, timestr, strlen(timestr), VENDOR_CODE)) + return -1; + + /* Duration */ + if (!rc_avpair_add(rh, send, PW_AST_DURATION, &cdr->duration, 0, VENDOR_CODE)) + return -1; + + /* Billable seconds */ + if (!rc_avpair_add(rh, send, PW_AST_BILL_SEC, &cdr->billsec, 0, VENDOR_CODE)) + return -1; + + /* Disposition */ + tmp = ast_cdr_disp2str(cdr->disposition); + if (!rc_avpair_add(rh, send, PW_AST_DISPOSITION, tmp, strlen(tmp), VENDOR_CODE)) + return -1; + + /* AMA Flags */ + tmp = ast_cdr_flags2str(cdr->amaflags); + if (!rc_avpair_add(rh, send, PW_AST_AMA_FLAGS, tmp, strlen(tmp), VENDOR_CODE)) + return -1; + + if (ast_test_flag(&global_flags, RADIUS_FLAG_LOGUNIQUEID)) { + /* Unique ID */ + if (!rc_avpair_add(rh, send, PW_AST_UNIQUE_ID, &cdr->uniqueid, strlen(cdr->uniqueid), VENDOR_CODE)) + return -1; + } + + if (ast_test_flag(&global_flags, RADIUS_FLAG_LOGUSERFIELD)) { + /* append the user field */ + if (!rc_avpair_add(rh, send, PW_AST_USER_FIELD, &cdr->userfield, strlen(cdr->userfield), VENDOR_CODE)) + 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, &cdr->channel, strlen(cdr->channel), 0)) + return -1; + + /* Unique ID */ + if (!rc_avpair_add(rh, send, PW_ACCT_SESSION_ID, &cdr->uniqueid, strlen(cdr->uniqueid), 0)) + return -1; + + return 0; +} + +static int radius_log(struct ast_cdr *cdr) +{ + int result = ERROR_RC; + VALUE_PAIR *send = NULL; + + if (build_radius_record(&send, cdr)) { + ast_debug(1, "Unable to create RADIUS record. CDR not recorded!\n"); + return result; + } + + result = rc_acct(rh, 0, send); + if (result != OK_RC) + ast_log(LOG_ERROR, "Failed to record Radius CDR record!\n"); + + return result; +} + +static int unload_module(void) +{ + ast_cdr_unregister(name); + return 0; +} + +static int load_module(void) +{ + struct ast_config *cfg; + struct ast_flags config_flags = { 0 }; + int res; + const char *tmp; + + if ((cfg = ast_config_load(cdr_config, config_flags))) { + ast_set2_flag(&global_flags, ast_true(ast_variable_retrieve(cfg, "radius", "usegmtime")), RADIUS_FLAG_USEGMTIME); + ast_set2_flag(&global_flags, ast_true(ast_variable_retrieve(cfg, "radius", "loguniqueid")), RADIUS_FLAG_LOGUNIQUEID); + ast_set2_flag(&global_flags, ast_true(ast_variable_retrieve(cfg, "radius", "loguserfield")), RADIUS_FLAG_LOGUSERFIELD); + 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; + } + + res = ast_cdr_register(name, desc, radius_log); + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "RADIUS CDR Backend"); diff --git a/trunk/cdr/cdr_sqlite.c b/trunk/cdr/cdr_sqlite.c new file mode 100644 index 000000000..601e5b245 --- /dev/null +++ b/trunk/cdr/cdr_sqlite.c @@ -0,0 +1,214 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2004 - 2005, Holger Schurig + * + * + * Ideas taken from other cdr_*.c files + * + * 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 Store CDR records in a SQLite database. + * + * \author Holger Schurig <hs4233@mail.mn-solutions.de> + * \extref SQLite http://www.sqlite.org/ + * + * See also + * \arg \ref Config_cdr + * \arg http://www.sqlite.org/ + * + * Creates the database and table on-the-fly + * \ingroup cdr_drivers + * + * \note This module has been marked deprecated in favor for cdr_sqlite3_custom + */ + +/*** MODULEINFO + <depend>sqlite</depend> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <sqlite.h> + +#include "asterisk/channel.h" +#include "asterisk/module.h" +#include "asterisk/utils.h" +#include "asterisk/paths.h" + +#define LOG_UNIQUEID 0 +#define LOG_USERFIELD 0 + +/* When you change the DATE_FORMAT, be sure to change the CHAR(19) below to something else */ +#define DATE_FORMAT "%Y-%m-%d %T" + +static char *name = "sqlite"; +static sqlite* db = NULL; + +AST_MUTEX_DEFINE_STATIC(sqlite_lock); + +/*! \brief SQL table format */ +static char sql_create_table[] = "CREATE TABLE cdr (" +" AcctId INTEGER PRIMARY KEY," +" clid VARCHAR(80)," +" src VARCHAR(80)," +" dst VARCHAR(80)," +" dcontext VARCHAR(80)," +" channel VARCHAR(80)," +" dstchannel VARCHAR(80)," +" lastapp VARCHAR(80)," +" lastdata VARCHAR(80)," +" start CHAR(19)," +" answer CHAR(19)," +" end CHAR(19)," +" duration INTEGER," +" billsec INTEGER," +" disposition INTEGER," +" amaflags INTEGER," +" accountcode VARCHAR(20)" +#if LOG_UNIQUEID +" ,uniqueid VARCHAR(32)" +#endif +#if LOG_USERFIELD +" ,userfield VARCHAR(255)" +#endif +");"; + +static int sqlite_log(struct ast_cdr *cdr) +{ + int res = 0; + char *zErr = 0; + struct ast_tm tm; + char startstr[80], answerstr[80], endstr[80]; + int count; + + ast_mutex_lock(&sqlite_lock); + + ast_localtime(&cdr->start, &tm, NULL); + ast_strftime(startstr, sizeof(startstr), DATE_FORMAT, &tm); + + ast_localtime(&cdr->answer, &tm, NULL); + ast_strftime(answerstr, sizeof(answerstr), DATE_FORMAT, &tm); + + ast_localtime(&cdr->end, &tm, NULL); + ast_strftime(endstr, sizeof(endstr), DATE_FORMAT, &tm); + + for(count=0; count<5; count++) { + res = sqlite_exec_printf(db, + "INSERT INTO cdr (" + "clid,src,dst,dcontext," + "channel,dstchannel,lastapp,lastdata, " + "start,answer,end," + "duration,billsec,disposition,amaflags, " + "accountcode" +# if LOG_UNIQUEID + ",uniqueid" +# endif +# if LOG_USERFIELD + ",userfield" +# endif + ") VALUES (" + "'%q', '%q', '%q', '%q', " + "'%q', '%q', '%q', '%q', " + "'%q', '%q', '%q', " + "%d, %d, %d, %d, " + "'%q'" +# if LOG_UNIQUEID + ",'%q'" +# endif +# if LOG_USERFIELD + ",'%q'" +# endif + ")", NULL, NULL, &zErr, + cdr->clid, cdr->src, cdr->dst, cdr->dcontext, + cdr->channel, cdr->dstchannel, cdr->lastapp, cdr->lastdata, + startstr, answerstr, endstr, + cdr->duration, cdr->billsec, cdr->disposition, cdr->amaflags, + cdr->accountcode +# if LOG_UNIQUEID + ,cdr->uniqueid +# endif +# if LOG_USERFIELD + ,cdr->userfield +# endif + ); + if (res != SQLITE_BUSY && res != SQLITE_LOCKED) + break; + usleep(200); + } + + if (zErr) { + ast_log(LOG_ERROR, "cdr_sqlite: %s\n", zErr); + ast_free(zErr); + } + + ast_mutex_unlock(&sqlite_lock); + return res; +} + +static int unload_module(void) +{ + if (db) + sqlite_close(db); + ast_cdr_unregister(name); + return 0; +} + +static int load_module(void) +{ + char *zErr; + char fn[PATH_MAX]; + int res; + + ast_log(LOG_WARNING, "This module has been marked deprecated in favor of " + "using cdr_sqlite3_custom. (May be removed after Asterisk 1.6)\n"); + + /* is the database there? */ + snprintf(fn, sizeof(fn), "%s/cdr.db", ast_config_AST_LOG_DIR); + db = sqlite_open(fn, AST_FILE_MODE, &zErr); + if (!db) { + ast_log(LOG_ERROR, "cdr_sqlite: %s\n", zErr); + ast_free(zErr); + return -1; + } + + /* is the table there? */ + res = sqlite_exec(db, "SELECT COUNT(AcctId) FROM cdr;", NULL, NULL, NULL); + if (res) { + res = sqlite_exec(db, sql_create_table, NULL, NULL, &zErr); + if (res) { + ast_log(LOG_ERROR, "cdr_sqlite: Unable to create table 'cdr': %s\n", zErr); + ast_free(zErr); + goto err; + } + + /* TODO: here we should probably create an index */ + } + + res = ast_cdr_register(name, ast_module_info->description, sqlite_log); + if (res) { + ast_log(LOG_ERROR, "Unable to register SQLite CDR handling\n"); + return -1; + } + return 0; + +err: + if (db) + sqlite_close(db); + return -1; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "SQLite CDR Backend"); diff --git a/trunk/cdr/cdr_sqlite3_custom.c b/trunk/cdr/cdr_sqlite3_custom.c new file mode 100644 index 000000000..87718db59 --- /dev/null +++ b/trunk/cdr/cdr_sqlite3_custom.c @@ -0,0 +1,357 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 1999 - 2007, Digium, Inc. + * + * 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 CDR records. + * + * \author Adapted by 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 AstCDR + * + * + * \ingroup cdr_drivers + */ + +/*** MODULEINFO + <depend>sqlite3</depend> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <time.h> +#include <sqlite3.h> + +#include "asterisk/paths.h" /* use ast_config_AST_LOG_DIR */ +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" +#include "asterisk/config.h" +#include "asterisk/pbx.h" +#include "asterisk/utils.h" +#include "asterisk/cli.h" + +AST_MUTEX_DEFINE_STATIC(lock); + +static const char config_file[] = "cdr_sqlite3_custom.conf"; + +static char *desc = "Customizable SQLite3 CDR Backend"; +static char *name = "cdr_sqlite3_custom"; +static sqlite3 *db = NULL; + +static char table[80]; +static char *columns; + +struct values { + char *expression; + AST_LIST_ENTRY(values) list; +}; + +static AST_LIST_HEAD_STATIC(sql_values, values); + +static int free_config(void); + +static int load_column_config(const char *tmp) +{ + char *col = NULL; + char *cols = 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 (!(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(cols); + return -1; + } + if (!column_string->used) + ast_str_set(&column_string, 0, escaped); + else + ast_str_append(&column_string, 0, ",%s", escaped); + sqlite3_free(escaped); + } + if (!(columns = ast_strdup(column_string->str))) { + ast_log(LOG_ERROR, "Out of memory copying columns string for table '%s.'\n", table); + ast_free(column_string); + ast_free(cols); + return -1; + } + ast_free(column_string); + ast_free(cols); + + return 0; +} + +static int load_values_config(const char *tmp) +{ + char *val = NULL; + char *vals = NULL; + struct values *value = NULL; + + if (ast_strlen_zero(tmp)) { + ast_log(LOG_WARNING, "Values not specified. Module not loaded.\n"); + return -1; + } + if (!(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(vals); + 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(vals); + + 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))) { + if (reload) + ast_log(LOG_WARNING, "Failed to reload configuration file.\n"); + else + ast_log(LOG_WARNING, "Failed to load configuration file. Module not activated.\n"); + return -1; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + if (reload) + free_config(); + + ast_mutex_lock(&lock); + + if (!(mappingvar = ast_variable_browse(cfg, "master"))) { + /* Nothing configured */ + ast_mutex_unlock(&lock); + ast_config_destroy(cfg); + return 0; + } + + /* Mapping must have a table name */ + tmp = ast_variable_retrieve(cfg, "master", "table"); + if (!ast_strlen_zero(tmp)) + ast_copy_string(table, tmp, sizeof(table)); + else { + ast_log(LOG_WARNING, "Table name not specified. Assuming cdr.\n"); + strcpy(table, "cdr"); + } + + /* Columns */ + tmp = ast_variable_retrieve(cfg, "master", "columns"); + if (load_column_config(tmp)) { + ast_mutex_unlock(&lock); + ast_config_destroy(cfg); + free_config(); + return -1; + } + + /* Values */ + tmp = ast_variable_retrieve(cfg, "master", "values"); + if (load_values_config(tmp)) { + ast_mutex_unlock(&lock); + ast_config_destroy(cfg); + free_config(); + return -1; + } + + ast_verb(3, "cdr_sqlite3_custom: Logging CDR records to table '%s' in 'master.db'\n", table); + + ast_mutex_unlock(&lock); + ast_config_destroy(cfg); + + return 0; +} + +static int free_config(void) +{ + struct values *value; + + ast_mutex_lock(&lock); + + 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); + + ast_mutex_unlock(&lock); + + return 0; +} + +static int sqlite3_log(struct ast_cdr *cdr) +{ + int res = 0; + char *error = NULL; + char *sql = NULL; + struct ast_channel dummy = { 0, }; + int count = 0; + + { /* Make it obvious that only sql should be used outside of this block */ + char *escaped; + char subst_buf[2048]; + struct values *value; + struct ast_str *value_string = ast_str_create(1024); + dummy.cdr = cdr; + AST_LIST_TRAVERSE(&sql_values, value, list) { + memset(subst_buf, 0, sizeof(subst_buf)); + pbx_substitute_variables_helper(&dummy, value->expression, subst_buf, sizeof(subst_buf) - 1); + escaped = sqlite3_mprintf("%q", subst_buf); + if (!value_string->used) + ast_str_append(&value_string, 0, "'%s'", escaped); + else + ast_str_append(&value_string, 0, ",'%s'", escaped); + sqlite3_free(escaped); + } + sql = sqlite3_mprintf("INSERT INTO %q (%s) VALUES (%s)", table, columns, value_string->str); + ast_debug(1, "About to log: %s\n", sql); + ast_free(value_string); + } + + ast_mutex_lock(&lock); + + /* XXX This seems awful arbitrary... */ + for (count = 0; count < 5; count++) { + res = sqlite3_exec(db, sql, NULL, NULL, &error); + if (res != SQLITE_BUSY && res != SQLITE_LOCKED) + break; + usleep(200); + } + + if (error) { + ast_log(LOG_ERROR, "%s. SQL: %s.\n", error, sql); + sqlite3_free(error); + } + + if (sql) + sqlite3_free(sql); + + ast_mutex_unlock(&lock); + + return res; +} + +static int unload_module(void) +{ + free_config(); + + ast_cdr_unregister(name); + + return 0; +} + +static int load_module(void) +{ + char *error; + char filename[PATH_MAX]; + int res; + char *sql; + + if (!load_config(0)) { + res = ast_cdr_register(name, desc, sqlite3_log); + if (res) { + ast_log(LOG_ERROR, "Unable to register custom SQLite3 CDR handling\n"); + free_config(); + return AST_MODULE_LOAD_DECLINE; + } + } else + 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; + } + } + + return 0; +} + +static int reload(void) +{ + return load_config(1); +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "SQLite3 Custom CDR Module", + .load = load_module, + .unload = unload_module, + .reload = reload, +); diff --git a/trunk/cdr/cdr_tds.c b/trunk/cdr/cdr_tds.c new file mode 100644 index 000000000..9d322fd27 --- /dev/null +++ b/trunk/cdr/cdr_tds.c @@ -0,0 +1,531 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2004 - 2006, 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 CDR logger + * + * See also + * \arg \ref Config_cdr + * \arg http://www.freetds.org/ + * \ingroup cdr_drivers + */ + +/*! \verbatim + * + * Table Structure for `cdr` + * + * Created on: 05/20/2004 16:16 + * Last changed on: 07/27/2004 20:01 + +CREATE TABLE [dbo].[cdr] ( + [accountcode] [varchar] (20) NULL , + [src] [varchar] (80) NULL , + [dst] [varchar] (80) NULL , + [dcontext] [varchar] (80) NULL , + [clid] [varchar] (80) NULL , + [channel] [varchar] (80) NULL , + [dstchannel] [varchar] (80) NULL , + [lastapp] [varchar] (80) NULL , + [lastdata] [varchar] (80) NULL , + [start] [datetime] NULL , + [answer] [datetime] NULL , + [end] [datetime] NULL , + [duration] [int] NULL , + [billsec] [int] NULL , + [disposition] [varchar] (20) NULL , + [amaflags] [varchar] (16) NULL , + [uniqueid] [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 <tds.h> +#include <tdsconvert.h> +#include <ctype.h> + +#include "asterisk/config.h" +#include "asterisk/channel.h" +#include "asterisk/cdr.h" +#include "asterisk/module.h" + +#ifdef FREETDS_PRE_0_62 +#warning "You have older TDS, you should upgrade!" +#endif + +#define DATE_FORMAT "%Y/%m/%d %T" + +static char *name = "mssql"; +static char *config = "cdr_tds.conf"; + +static char *hostname = NULL, *dbname = NULL, *dbuser = NULL, *password = NULL, *charset = NULL, *language = NULL; +static char *table = NULL; + +static int connected = 0; + +AST_MUTEX_DEFINE_STATIC(tds_lock); + +static TDSSOCKET *tds; +static TDSLOGIN *login; +static TDSCONTEXT *context; + +static char *anti_injection(const char *, int); +static void get_date(char *, struct timeval); + +static int mssql_connect(void); +static int mssql_disconnect(void); + +static int tds_log(struct ast_cdr *cdr) +{ + char sqlcmd[2048], start[80], answer[80], end[80]; + char *accountcode, *src, *dst, *dcontext, *clid, *channel, *dstchannel, *lastapp, *lastdata, *uniqueid; + int res = 0; + int retried = 0; +#ifdef FREETDS_PRE_0_62 + TDS_INT result_type; +#endif + + ast_mutex_lock(&tds_lock); + + memset(sqlcmd, 0, 2048); + + accountcode = anti_injection(cdr->accountcode, 20); + src = anti_injection(cdr->src, 80); + dst = anti_injection(cdr->dst, 80); + dcontext = anti_injection(cdr->dcontext, 80); + clid = anti_injection(cdr->clid, 80); + channel = anti_injection(cdr->channel, 80); + dstchannel = anti_injection(cdr->dstchannel, 80); + lastapp = anti_injection(cdr->lastapp, 80); + lastdata = anti_injection(cdr->lastdata, 80); + uniqueid = anti_injection(cdr->uniqueid, 32); + + get_date(start, cdr->start); + get_date(answer, cdr->answer); + get_date(end, cdr->end); + + sprintf( + sqlcmd, + "INSERT INTO %s " + "(" + "accountcode, " + "src, " + "dst, " + "dcontext, " + "clid, " + "channel, " + "dstchannel, " + "lastapp, " + "lastdata, " + "start, " + "answer, " + "[end], " + "duration, " + "billsec, " + "disposition, " + "amaflags, " + "uniqueid" + ") " + "VALUES " + "(" + "'%s', " /* accountcode */ + "'%s', " /* src */ + "'%s', " /* dst */ + "'%s', " /* dcontext */ + "'%s', " /* clid */ + "'%s', " /* channel */ + "'%s', " /* dstchannel */ + "'%s', " /* lastapp */ + "'%s', " /* lastdata */ + "%s, " /* start */ + "%s, " /* answer */ + "%s, " /* end */ + "%ld, " /* duration */ + "%ld, " /* billsec */ + "'%s', " /* disposition */ + "'%s', " /* amaflags */ + "'%s'" /* uniqueid */ + ")", + table, + accountcode, + src, + dst, + dcontext, + clid, + channel, + dstchannel, + lastapp, + lastdata, + start, + answer, + end, + cdr->duration, + cdr->billsec, + ast_cdr_disp2str(cdr->disposition), + ast_cdr_flags2str(cdr->amaflags), + uniqueid + ); + + do { + if (!connected) { + if (mssql_connect()) + ast_log(LOG_ERROR, "Failed to reconnect to SQL database.\n"); + else + ast_log(LOG_WARNING, "Reconnected to SQL database.\n"); + + retried = 1; /* note that we have now tried */ + } + +#ifdef FREETDS_PRE_0_62 + if (!connected || (tds_submit_query(tds, sqlcmd) != TDS_SUCCEED) || (tds_process_simple_query(tds, &result_type) != TDS_SUCCEED || result_type != TDS_CMD_SUCCEED)) +#else + if (!connected || (tds_submit_query(tds, sqlcmd) != TDS_SUCCEED) || (tds_process_simple_query(tds) != TDS_SUCCEED)) +#endif + { + ast_log(LOG_ERROR, "Failed to insert Call Data Record into SQL database.\n"); + + mssql_disconnect(); /* this is ok even if we are already disconnected */ + } + } while (!connected && !retried); + + ast_free(accountcode); + ast_free(src); + ast_free(dst); + ast_free(dcontext); + ast_free(clid); + ast_free(channel); + ast_free(dstchannel); + ast_free(lastapp); + ast_free(lastdata); + ast_free(uniqueid); + + ast_mutex_unlock(&tds_lock); + + return res; +} + +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_malloc(len + 1)) == NULL) + { + ast_log(LOG_ERROR, "cdr_tds: Out of memory error\n"); + return NULL; + } + memset(buf, 0, len); + + 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, struct timeval tv) +{ + struct ast_tm tm; + char buf[80]; + + /* To make sure we have date variable if not insert null to SQL */ + if (!ast_tvzero(tv)) + { + ast_localtime(&tv, &tm, NULL); + ast_strftime(buf, 80, DATE_FORMAT, &tm); + sprintf(dateField, "'%s'", buf); + } + else + { + strcpy(dateField, "null"); + } +} + +static int mssql_disconnect(void) +{ + if (tds) { + tds_free_socket(tds); + tds = NULL; + } + + if (context) { + tds_free_context(context); + context = NULL; + } + + if (login) { + tds_free_login(login); + login = NULL; + } + + connected = 0; + + return 0; +} + +static int mssql_connect(void) +{ +#if (defined(FREETDS_0_63) || defined(FREETDS_0_64)) + TDSCONNECTION *connection = NULL; +#else + TDSCONNECTINFO *connection = NULL; +#endif + char query[128]; + + /* Connect to M$SQL Server */ + if (!(login = tds_alloc_login())) + { + ast_log(LOG_ERROR, "tds_alloc_login() failed.\n"); + return -1; + } + + tds_set_server(login, hostname); + tds_set_user(login, dbuser); + tds_set_passwd(login, password); + tds_set_app(login, "TSQL"); + tds_set_library(login, "TDS-Library"); +#ifndef FREETDS_PRE_0_62 + tds_set_client_charset(login, charset); +#endif + tds_set_language(login, language); + tds_set_packet(login, 512); + tds_set_version(login, 7, 0); + +#ifdef FREETDS_0_64 + if (!(context = tds_alloc_context(NULL))) +#else + if (!(context = tds_alloc_context())) +#endif + { + ast_log(LOG_ERROR, "tds_alloc_context() failed.\n"); + goto connect_fail; + } + + if (!(tds = tds_alloc_socket(context, 512))) { + ast_log(LOG_ERROR, "tds_alloc_socket() failed.\n"); + goto connect_fail; + } + + tds_set_parent(tds, NULL); + connection = tds_read_config_info(tds, login, context->locale); + if (!connection) + { + ast_log(LOG_ERROR, "tds_read_config() failed.\n"); + goto connect_fail; + } + + if (tds_connect(tds, connection) == TDS_FAIL) + { + ast_log(LOG_ERROR, "Failed to connect to MSSQL server.\n"); + tds = NULL; /* freed by tds_connect() on error */ +#if (defined(FREETDS_0_63) || defined(FREETDS_0_64)) + tds_free_connection(connection); +#else + tds_free_connect(connection); +#endif + connection = NULL; + goto connect_fail; + } +#if (defined(FREETDS_0_63) || defined(FREETDS_0_64)) + tds_free_connection(connection); +#else + tds_free_connect(connection); +#endif + connection = NULL; + + sprintf(query, "USE %s", dbname); +#ifdef FREETDS_PRE_0_62 + if ((tds_submit_query(tds, query) != TDS_SUCCEED) || (tds_process_simple_query(tds, &result_type) != TDS_SUCCEED || result_type != TDS_CMD_SUCCEED)) +#else + if ((tds_submit_query(tds, query) != TDS_SUCCEED) || (tds_process_simple_query(tds) != TDS_SUCCEED)) +#endif + { + ast_log(LOG_ERROR, "Could not change database (%s)\n", dbname); + goto connect_fail; + } + + connected = 1; + return 0; + +connect_fail: + mssql_disconnect(); + return -1; +} + +static int tds_unload_module(void) +{ + mssql_disconnect(); + + ast_cdr_unregister(name); + + if (hostname) ast_free(hostname); + if (dbname) ast_free(dbname); + if (dbuser) ast_free(dbuser); + if (password) ast_free(password); + if (charset) ast_free(charset); + if (language) ast_free(language); + if (table) ast_free(table); + + return 0; +} + +static int tds_load_module(int reload) +{ + int res = 0; + struct ast_config *cfg; + struct ast_variable *var; + const char *ptr = NULL; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; +#ifdef FREETDS_PRE_0_62 + TDS_INT result_type; +#endif + + cfg = ast_config_load(config, config_flags); + if (!cfg) { + ast_log(LOG_NOTICE, "Unable to load config for MSSQL CDR's: %s\n", config); + return 0; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) + return 0; + + var = ast_variable_browse(cfg, "global"); + if (!var) /* nothing configured */ { + ast_config_destroy(cfg); + return 0; + } + + ptr = ast_variable_retrieve(cfg, "global", "hostname"); + if (ptr) { + if (hostname) + ast_free(hostname); + hostname = ast_strdup(ptr); + } else + ast_log(LOG_ERROR, "Database server hostname not specified.\n"); + + ptr = ast_variable_retrieve(cfg, "global", "dbname"); + if (ptr) { + if (dbname) + ast_free(dbname); + dbname = ast_strdup(ptr); + } else + ast_log(LOG_ERROR, "Database dbname not specified.\n"); + + ptr = ast_variable_retrieve(cfg, "global", "user"); + if (ptr) { + if (dbuser) + ast_free(dbuser); + dbuser = ast_strdup(ptr); + } else + ast_log(LOG_ERROR, "Database dbuser not specified.\n"); + + ptr = ast_variable_retrieve(cfg, "global", "password"); + if (ptr) { + if (password) + ast_free(password); + password = ast_strdup(ptr); + } else + ast_log(LOG_ERROR,"Database password not specified.\n"); + + ptr = ast_variable_retrieve(cfg, "global", "charset"); + if (charset) + ast_free(charset); + if (ptr) + charset = ast_strdup(ptr); + else + charset = ast_strdup("iso_1"); + + if (language) + ast_free(language); + ptr = ast_variable_retrieve(cfg, "global", "language"); + if (ptr) + language = ast_strdup(ptr); + else + language = ast_strdup("us_english"); + + ptr = ast_variable_retrieve(cfg, "global", "table"); + if (ptr == NULL) { + ast_debug(1, "cdr_tds: table not specified. Assuming cdr\n"); + ptr = "cdr"; + } + if (table) + ast_free(table); + table = ast_strdup(ptr); + + ast_config_destroy(cfg); + + ast_mutex_lock(&tds_lock); + mssql_disconnect(); + mssql_connect(); + ast_mutex_unlock(&tds_lock); + + return res; +} + +static int reload(void) +{ + return tds_load_module(1); +} + +static int load_module(void) +{ + if (!tds_load_module(0)) + return AST_MODULE_LOAD_DECLINE; + ast_cdr_register(name, ast_module_info->description, tds_log); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + return tds_unload_module(); +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "MSSQL CDR Backend", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); |