Index: cdr_addon_mysql.c =================================================================== --- cdr_addon_mysql.c (revision 499) +++ cdr_addon_mysql.c (working copy) @@ -57,10 +57,7 @@ static char *name = "mysql"; static char *config = "cdr_mysql.conf"; -static struct ast_str *hostname = NULL, *dbname = NULL, *dbuser = NULL, *password = NULL, *dbsock = NULL, *dbtable = NULL, - *calldate_field = NULL, *clid_field = NULL, *src_field = NULL, *dst_field = NULL, *dcontext_field = NULL, *channel_field = NULL, - *dstchannel_field = NULL, *lastapp_field = NULL, *lastdata_field = NULL, *duration_field = NULL, *billsec_field =NULL, - *disposition_field = NULL, *amaflags_field = NULL, *accountcode_field = NULL, *userfield_field = NULL, *uniqueid_field = NULL; +static struct ast_str *hostname = NULL, *dbname = NULL, *dbuser = NULL, *password = NULL, *dbsock = NULL, *dbtable = NULL; static struct ast_str *ssl_ca = NULL, *ssl_cert = NULL, *ssl_key = NULL; @@ -69,9 +66,7 @@ static time_t connect_time = 0; static int records = 0; static int totalrecords = 0; -static int userfield = 0; static int timeout = 0; -static int loguniqueid = 0; AST_MUTEX_DEFINE_STATIC(mysql_lock); @@ -82,8 +77,17 @@ static AST_LIST_HEAD_STATIC(unload_strings, unload_string); -static MYSQL mysql; +struct column { + char *name; + char *cdrname; + AST_LIST_ENTRY(column) list; +}; +/* Protected with mysql_lock */ +static AST_LIST_HEAD_NOLOCK_STATIC(columns, column); + +static MYSQL mysql = { { NULL }, }; + static char *handle_cli_cdr_mysql_status(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { switch (cmd) { @@ -142,23 +146,21 @@ static int mysql_log(struct ast_cdr *cdr) { - struct ast_tm tm; - char *userfielddata = NULL; - char sqlcmd[2048], timestr[128]; - char *clid=NULL, *dcontext=NULL, *channel=NULL, *dstchannel=NULL, *lastapp=NULL, *lastdata=NULL, *src=NULL, *dst=NULL, *accountcode=NULL, *tmp; + char sqldesc[128]; + char *sql1 = ast_calloc(1, 4096), *sql2 = ast_calloc(1, 2048); + int sql1size = 4096, sql2size = 2048; int retries = 5; - char *uniqueid = NULL; #if MYSQL_VERSION_ID >= 50013 my_bool my_bool_true = 1; #endif + if (!sql1 || !sql2) { + ast_log(LOG_ERROR, "Memory error\n"); + return -1; + } + ast_mutex_lock(&mysql_lock); - memset(sqlcmd, 0, 2048); - - ast_localtime(&cdr->start, &tm, NULL); - ast_strftime(timestr, sizeof(timestr), DATE_FORMAT, &tm); - db_reconnect: if ((!connected) && (hostname || dbsock) && dbuser && password && dbname && dbtable ) { /* Attempt to connect */ @@ -173,11 +175,10 @@ ast_log(LOG_ERROR, "mysql_options returned (%d) %s\n", mysql_errno(&mysql), mysql_error(&mysql)); } #endif - tmp = dbsock ? dbsock->str : NULL; if (ssl_ca || ssl_cert || ssl_key) { - mysql_ssl_set (&mysql, ssl_key->str, ssl_cert->str, ssl_ca->str, NULL, NULL); + mysql_ssl_set(&mysql, ssl_key ? ssl_key->str : NULL, ssl_cert ? ssl_cert->str : NULL, ssl_ca ? ssl_ca->str : NULL, NULL, NULL); } - if (mysql_real_connect(&mysql, hostname->str, dbuser->str, password->str, dbname->str, dbport, tmp, ssl_ca ? CLIENT_SSL : 0)) { + if (mysql_real_connect(&mysql, hostname->str, dbuser->str, password->str, dbname->str, dbport, dbsock ? dbsock->str : NULL, ssl_ca ? CLIENT_SSL : 0)) { connected = 1; connect_time = time(NULL); records = 0; @@ -207,69 +208,128 @@ } } - /* Maximum space needed would be if all characters needed to be escaped, plus a trailing NULL */ - /* WARNING: This code previously used mysql_real_escape_string, but the use of said function - requires an active connection to a database. If we are not connected, then this function - cannot be used. This is a problem since we need to store off the SQL statement into our - spool file for later restoration. - So the question is, what's the best way to handle this? This works for now. - */ - if ((clid = alloca(strlen(cdr->clid) * 2 + 1)) != NULL) - mysql_escape_string(clid, cdr->clid, strlen(cdr->clid)); - if ((dcontext = alloca(strlen(cdr->dcontext) * 2 + 1)) != NULL) - mysql_escape_string(dcontext, cdr->dcontext, strlen(cdr->dcontext)); - if ((channel = alloca(strlen(cdr->channel) * 2 + 1)) != NULL) - mysql_escape_string(channel, cdr->channel, strlen(cdr->channel)); - if ((dstchannel = alloca(strlen(cdr->dstchannel) * 2 + 1)) != NULL) - mysql_escape_string(dstchannel, cdr->dstchannel, strlen(cdr->dstchannel)); - if ((lastapp = alloca(strlen(cdr->lastapp) * 2 + 1)) != NULL) - mysql_escape_string(lastapp, cdr->lastapp, strlen(cdr->lastapp)); - if ((lastdata = alloca(strlen(cdr->lastdata) * 2 + 1)) != NULL) - mysql_escape_string(lastdata, cdr->lastdata, strlen(cdr->lastdata)); - if (loguniqueid && (uniqueid = alloca(strlen(cdr->uniqueid) * 2 + 1)) != NULL) - mysql_escape_string(uniqueid, cdr->uniqueid, strlen(cdr->uniqueid)); - if (userfield && ((userfielddata = alloca(strlen(cdr->userfield) * 2 + 1)) != NULL)) - mysql_escape_string(userfielddata, cdr->userfield, strlen(cdr->userfield)); - if ((src = alloca(strlen(cdr->src) * 2 + 1)) != NULL) - mysql_escape_string(src, cdr->src, strlen(cdr->src)); - if ((dst = alloca(strlen(cdr->dst) * 2 + 1)) != NULL) - mysql_escape_string(dst, cdr->dst, strlen(cdr->dst)); - if ((accountcode = alloca(strlen(cdr->accountcode) * 2 + 1)) != NULL) - mysql_escape_string(accountcode, cdr->accountcode, strlen(cdr->accountcode)); - - /* Check for all alloca failures above at once */ - if ((!clid) || (!dcontext) || (!channel) || (!dstchannel) || (!lastapp) || (!lastdata) || (!src) || (!dst) || (!accountcode) || (loguniqueid && !uniqueid) || (userfield && !userfielddata)) { - ast_log(LOG_ERROR, "Out of memory error (insert fails)\n"); - ast_mutex_unlock(&mysql_lock); - return -1; - } - if (connected) { - ast_debug(1, "Inserting a CDR record.\n"); + MYSQL_ROW row; + MYSQL_RES *result; + int column_count = 0; - if (userfield && userfielddata) { - if (loguniqueid) - snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ('%s','%s','%s','%s','%s', '%s','%s','%s','%s',%ld,%ld,'%s',%ld,'%s','%s','%s')", dbtable->str, calldate_field->str, clid_field->str, src_field->str, dst_field->str, dcontext_field->str, channel_field->str, dstchannel_field->str, lastapp_field->str, lastdata_field->str, duration_field->str, billsec_field->str, disposition_field->str, amaflags_field->str, accountcode_field->str, uniqueid_field->str, userfield_field->str, timestr, clid, src, dst, dcontext, channel, dstchannel, lastapp, lastdata, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), cdr->amaflags, accountcode, uniqueid, userfielddata); - else - snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ('%s','%s','%s','%s','%s', '%s','%s','%s','%s',%ld,%ld,'%s',%ld,'%s','%s')", dbtable->str, calldate_field->str, clid_field->str, src_field->str, dst_field->str, dcontext_field->str, channel_field->str, dstchannel_field->str, lastapp_field->str, lastdata_field->str, duration_field->str, billsec_field->str, disposition_field->str, amaflags_field->str, accountcode_field->str, userfield_field->str, timestr, clid, src, dst, dcontext, channel, dstchannel, lastapp, lastdata, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), cdr->amaflags, accountcode, userfielddata); - } else { - if (loguniqueid) - snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ('%s','%s','%s','%s','%s', '%s','%s','%s','%s',%ld,%ld,'%s',%ld,'%s','%s')", dbtable->str, calldate_field->str, clid_field->str, src_field->str, dst_field->str, dcontext_field->str, channel_field->str, dstchannel_field->str, lastapp_field->str, lastdata_field->str, duration_field->str, billsec_field->str, disposition_field->str, amaflags_field->str, accountcode_field->str, uniqueid_field->str, timestr, clid, src, dst, dcontext,channel, dstchannel, lastapp, lastdata, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), cdr->amaflags, accountcode, uniqueid); - else - snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ('%s','%s','%s','%s','%s', '%s','%s','%s','%s',%ld,%ld,'%s',%ld,'%s')", dbtable->str, calldate_field->str, clid_field->str, src_field->str, dst_field->str, dcontext_field->str, channel_field->str, dstchannel_field->str, lastapp_field->str, lastdata_field->str, duration_field->str, billsec_field->str, disposition_field->str, amaflags_field->str, accountcode_field->str, timestr, clid, src, dst, dcontext, channel, dstchannel, lastapp, lastdata, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), cdr->amaflags, accountcode); + snprintf(sql1, sql1size, "INSERT INTO %s (", dbtable ? dbtable->str : "cdr"); + strcpy(sql2, ") VALUES ('"); + + /* Get table description */ + snprintf(sqldesc, sizeof(sqldesc), "DESC %s", dbtable ? dbtable->str : "cdr"); + if (mysql_query(&mysql, sqldesc)) { + ast_log(LOG_ERROR, "Unable to query table description!!\n"); + mysql_close(&mysql); + connected = 0; + goto log_exit; } - - ast_debug(1, "SQL command as follows: %s\n", sqlcmd); - if (mysql_real_query(&mysql, sqlcmd, strlen(sqlcmd))) { - ast_log(LOG_ERROR, "Failed to insert into database: (%d) %s", mysql_errno(&mysql), mysql_error(&mysql)); + if (!(result = mysql_store_result(&mysql))) { + ast_log(LOG_ERROR, "Unable to query table description!!\n"); mysql_close(&mysql); connected = 0; + goto log_exit; + } + + while ((row = mysql_fetch_row(result))) { + struct column *entry; + char *cdrname; + char workspace[2048], *value = NULL, *ptr; + int sql2len; + + ast_debug(1, "Got a field '%s' of type '%s'\n", row[0], row[1]); + /* Check for an alias */ + AST_LIST_TRAVERSE(&columns, entry, list) { + /* This would probably be better off as a hash */ + if (!strcasecmp(entry->name, row[0])) + break; + } + + cdrname = entry ? entry->cdrname : row[0]; + + /* Construct SQL */ + if (column_count++) { + strcat(sql1, ","); + strcat(sql2, "','"); + } + + if (strlen(sql1) + 2 + strlen(row[0]) > sql1size) { + char *tmp = ast_realloc(sql1, sql1size * 2); + if (!tmp) + goto log_exit; + sql1size *= 2; + sql1 = tmp; + } + strcat(sql1, row[0]); + + /* Need the type and value to determine if we want the raw value or not */ + if ((!strcmp(cdrname, "start") || + !strcmp(cdrname, "answer") || + !strcmp(cdrname, "end") || + !strcmp(cdrname, "disposition") || + !strcmp(cdrname, "amaflags")) && + (strstr(row[1], "int") || + strstr(row[1], "dec") || + strstr(row[1], "float") || + strstr(row[1], "double") || + strstr(row[1], "real") || + strstr(row[1], "numeric") || + strstr(row[1], "fixed"))) + ast_cdr_getvar(cdr, cdrname, &value, workspace, sizeof(workspace), 0, 1); + else + ast_cdr_getvar(cdr, cdrname, &value, workspace, sizeof(workspace), 0, 0); + + if (strlen(sql2) + (value ? strlen(value) * 2 : 0) + 4 > sql2size) { + char *tmp = ast_realloc(sql2, sql2size * 2); + if (!tmp) + goto log_exit; + sql2size *= 2; + sql2 = tmp; + } + if (value) { + /*!\note We're manually escaping here, to ensure that we know exactly + * how much space is used. Since we only accept ASCII strings at this + * point in time, there is no danger in this simplistic escape method, + * but I wouldn't recommend this technique for other databases or if + * we convert to an internal representation of UTF-8 sometime in the + * future. + */ + sql2len = strlen(sql2); + for (ptr = value; *ptr; ptr++) { + if (*ptr == '\\' || *ptr == '\'') + sql2[sql2len++] = '\\'; + sql2[sql2len++] = *ptr; + } + sql2[sql2len] = '\0'; + } + } + mysql_free_result(result); + + ast_debug(1, "Inserting a CDR record.\n"); + if (strlen(sql1) + 3 + strlen(sql2) > sql1size) { + char *tmp = ast_realloc(sql1, strlen(sql1) + 3 + strlen(sql2)); + if (!tmp) + goto log_exit; + sql1 = tmp; + } + strcat(sql1, sql2); + strcat(sql1, "')"); + + ast_debug(1, "SQL command as follows: %s\n", sql1); + + if (mysql_real_query(&mysql, sql1, strlen(sql1))) { + ast_log(LOG_ERROR, "Failed to insert into database: (%d) %s\n", mysql_errno(&mysql), mysql_error(&mysql)); + mysql_close(&mysql); + connected = 0; } else { records++; totalrecords++; } } +log_exit: + ast_free(sql1); + ast_free(sql2); ast_mutex_unlock(&mysql_lock); return 0; } @@ -277,6 +337,7 @@ static int my_unload_module(void) { struct unload_string *us; + struct column *entry; ast_cli_unregister_multiple(cdr_mysql_status_cli, sizeof(cdr_mysql_status_cli) / sizeof(struct ast_cli_entry)); @@ -292,7 +353,10 @@ ast_free(us); } AST_LIST_UNLOCK(&unload_strings); - + + while ((entry = AST_LIST_REMOVE_HEAD(&columns, list))) + ast_free(entry); + dbport = 0; ast_cdr_unregister(name); @@ -337,23 +401,13 @@ return 0; } -static int my_load_config_bool(struct ast_config *cfg, const char *category, const char *variable, int *field, int def) -{ - const char *tmp; - - tmp = ast_variable_retrieve(cfg, category, variable); - - *field = tmp ? ast_true(tmp) : def; - - return 0; -} - static int my_load_module(int reload) { int res; struct ast_config *cfg; struct ast_variable *var; struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + struct column *entry; char *temp; #if MYSQL_VERSION_ID >= 50013 my_bool my_bool_true = 1; @@ -390,31 +444,27 @@ res |= my_load_config_number(cfg, "global", "port", &dbport, 0); res |= my_load_config_number(cfg, "global", "timeout", &timeout, 0); - res |= my_load_config_number(cfg, "global", "userfield", &userfield, 0); - res |= my_load_config_bool(cfg, "global", "loguniqueid", &loguniqueid, 0); + if (res < 0) + return AST_MODULE_LOAD_FAILURE; - res |= my_load_config_string(cfg, "global", "calldate_field", &calldate_field, "calldate"); - res |= my_load_config_string(cfg, "global", "clid_field", &clid_field, "clid"); - res |= my_load_config_string(cfg, "global", "src_field", &src_field, "src"); + /* Check for any aliases */ + while ((entry = AST_LIST_REMOVE_HEAD(&columns, list))) + ast_free(entry); - res |= my_load_config_string(cfg, "global", "dst_field", &dst_field, "dst"); - res |= my_load_config_string(cfg, "global", "dcontext_field", &dcontext_field, "dcontext"); - res |= my_load_config_string(cfg, "global", "channel_field", &channel_field, "channel"); - res |= my_load_config_string(cfg, "global", "dstchannel_field", &dstchannel_field, "dstchannel"); - res |= my_load_config_string(cfg, "global", "lastapp_field", &lastapp_field, "lastapp"); - res |= my_load_config_string(cfg, "global", "lastdata_field", &lastdata_field, "lastdata"); - res |= my_load_config_string(cfg, "global", "duration_field", &duration_field, "duration"); - res |= my_load_config_string(cfg, "global", "billsec_field", &billsec_field, "billsec"); - res |= my_load_config_string(cfg, "global", "disposition_field", &disposition_field, "disposition"); - res |= my_load_config_string(cfg, "global", "amaflags_field", &amaflags_field, "amaflags"); - res |= my_load_config_string(cfg, "global", "accountcode_field", &accountcode_field, "accountcode"); - res |= my_load_config_string(cfg, "global", "userfield_field", &userfield_field, "userfield"); - res += my_load_config_string(cfg, "global", "uniqueid_field", &uniqueid_field, "uniqueid"); + for (var = ast_variable_browse(cfg, "aliases"); var; var = var->next) { + struct column *entry = ast_calloc(1, sizeof(*entry) + strlen(var->name) + 1 + strlen(var->value) + 1); + if (!entry) + continue; + entry->cdrname = (char *)entry + sizeof(*entry); + entry->name = (char *)entry + sizeof(*entry) + strlen(var->name) + 1; + strcpy(entry->cdrname, var->name); + strcpy(entry->name, var->value); - if (res < 0) - return AST_MODULE_LOAD_FAILURE; - + AST_LIST_INSERT_TAIL(&columns, entry, list); + ast_log(LOG_NOTICE, "Found an alias from CDR variable %s to DB column %s\n", entry->cdrname, entry->name); + } + ast_config_destroy(cfg); ast_debug(1, "Got hostname of %s\n", hostname->str); @@ -426,22 +476,6 @@ ast_debug(1, "Got dbname of %s\n", dbname->str); ast_debug(1, "Got password of %s\n", password->str); - ast_debug(1, "Got calldate field of %s\n", calldate_field->str); - ast_debug(1, "Got clid field of %s\n", clid_field->str); - ast_debug(1, "Got src field of %s\n", src_field->str); - ast_debug(1, "Got dst field of %s\n", dst_field->str); - ast_debug(1, "Got dcontext field of %s\n", dcontext_field->str); - ast_debug(1, "Got channel field of %s\n", channel_field->str); - ast_debug(1, "Got dstchannel field of %s\n", dstchannel_field->str); - ast_debug(1, "Got lastapp field of %s\n", lastapp_field->str); - ast_debug(1, "Got lastdata field of %s\n", lastdata_field->str); - ast_debug(1, "Got duration field of %s\n", duration_field->str); - ast_debug(1, "Got billsec field of %s\n", billsec_field->str); - ast_debug(1, "Got amaflags field of %s\n", amaflags_field->str); - ast_debug(1, "Got accountcode field of %s\n", accountcode_field->str); - ast_debug(1, "Got userfield field of %s\n", userfield_field->str); - ast_debug(1, "Got uniqueid field of %s\n", uniqueid_field->str); - mysql_init(&mysql); if (timeout && mysql_options(&mysql, MYSQL_OPT_CONNECT_TIMEOUT, (char *)&timeout) != 0) { Index: configs/cdr_mysql.conf.sample =================================================================== --- configs/cdr_mysql.conf.sample (revision 499) +++ configs/cdr_mysql.conf.sample (working copy) @@ -18,30 +18,28 @@ ;user=asteriskcdruser ;port=3306 ;sock=/tmp/mysql.sock -;userfield=1 ; -;loguniqueid=no ; Set to "yes" to enable logging the channel uniqueid -; -; You may also configure the field names used in the CDR table. -; -;calldate_field=calldate -;clid_field=clid -;src_field=src -;dst_field=dst -;dcontext_field=dcontext -;channel_field=channel -;dstchannel_field=dstchannel -;lastapp_field=lastapp -;lastdata_field=lastdata -;duration_field=duration -;billsec_field=billsec -;disposition_field=disposition -;amaflags_field=amaflags -;accountcode_field=accountcode -;userfield_field=userfield -;uniqueid_field=uniqueid -; ; ssl connections (optional) ;ssl_ca= ;ssl_cert= ;ssl_key= +; +; You may also configure the field names used in the CDR table. +; +[aliases] +;start=calldate +;callerid=clid +;src=src +;dst=dst +;dcontext=dcontext +;channel=channel +;dstchannel=dstchannel +;lastapp=lastapp +;lastdata=lastdata +;duration=duration +;billsec=billsec +;disposition=disposition +;amaflags=amaflags +;accountcode=accountcode +;userfield=userfield +;uniqueid=uniqueid