/* * Asterisk -- An open source telephony toolkit. * * Copyright (c) 2005, 2006 Tilghman Lesher * * Tilghman Lesher * * 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 lookups * * \author Tilghman Lesher */ /*** MODULEINFO unixodbc ***/ #include "asterisk.h" ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include #include #include #include #include #include "asterisk/module.h" #include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/options.h" #include "asterisk/channel.h" #include "asterisk/pbx.h" #include "asterisk/module.h" #include "asterisk/config.h" #include "asterisk/res_odbc.h" #include "asterisk/app.h" static char *tdesc = "ODBC lookups"; static char *config = "func_odbc.conf"; enum { OPT_ESCAPECOMMAS = (1 << 0), } odbc_option_flags; struct acf_odbc_query { AST_LIST_ENTRY(acf_odbc_query) list; char dsn[30]; char sql_read[2048]; char sql_write[2048]; unsigned int flags; struct ast_custom_function *acf; }; AST_LIST_HEAD_STATIC(queries, acf_odbc_query); #ifdef NEEDTRACE static void acf_odbc_error(SQLHSTMT stmt, int res) { char state[10] = "", diagnostic[256] = ""; SQLINTEGER nativeerror = 0; SQLSMALLINT diagbytes = 0; SQLGetDiagRec(SQL_HANDLE_STMT, stmt, 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); ast_log(LOG_WARNING, "SQL return value %d: error %s: %s (len %d)\n", res, state, diagnostic, diagbytes); } #endif /* * Master control routine */ static int acf_odbc_write(struct ast_channel *chan, char *cmd, char *s, const char *value) { struct odbc_obj *obj; struct acf_odbc_query *query; char *t, buf[2048]="", varname[15]; int res, i, retry=0; AST_DECLARE_APP_ARGS(values, AST_APP_ARG(field)[100]; ); AST_DECLARE_APP_ARGS(args, AST_APP_ARG(field)[100]; ); SQLHSTMT stmt; SQLINTEGER nativeerror=0, numfields=0, rows=0; SQLSMALLINT diagbytes=0; unsigned char state[10], diagnostic[256]; #ifdef NEEDTRACE SQLINTEGER enable = 1; char *tracefile = "/tmp/odbc.trace"; #endif AST_LIST_LOCK(&queries); AST_LIST_TRAVERSE(&queries, query, list) { if (!strcmp(query->acf->name, cmd)) { break; } } if (!query) { ast_log(LOG_ERROR, "No such function '%s'\n", cmd); AST_LIST_UNLOCK(&queries); return -1; } obj = odbc_request_obj(query->dsn, 0); if (!obj) { ast_log(LOG_ERROR, "No such DSN registered (or out of connections): %s (check res_odbc.conf)\n", query->dsn); AST_LIST_UNLOCK(&queries); return -1; } /* Parse our arguments */ t = value ? ast_strdupa(value) : ""; if (!s || !t) { ast_log(LOG_ERROR, "Out of memory\n"); AST_LIST_UNLOCK(&queries); return -1; } AST_STANDARD_APP_ARGS(args, s); for (i = 0; i < args.argc; i++) { snprintf(varname, sizeof(varname), "ARG%d", i + 1); pbx_builtin_pushvar_helper(chan, varname, args.field[i]); } /* Parse values, just like arguments */ /* Can't use the pipe, because app Set removes them */ AST_NONSTANDARD_APP_ARGS(values, t, ','); for (i = 0; i < values.argc; i++) { snprintf(varname, sizeof(varname), "VAL%d", i + 1); pbx_builtin_pushvar_helper(chan, varname, values.field[i]); } /* Additionally set the value as a whole (but push an empty string if value is NULL) */ pbx_builtin_pushvar_helper(chan, "VALUE", value ? value : ""); pbx_substitute_variables_helper(chan, query->sql_write, buf, sizeof(buf) - 1); /* Restore prior values */ for (i = 0; i < args.argc; i++) { snprintf(varname, sizeof(varname), "ARG%d", i + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } for (i = 0; i < values.argc; i++) { snprintf(varname, sizeof(varname), "VAL%d", i + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } pbx_builtin_setvar_helper(chan, "VALUE", NULL); AST_LIST_UNLOCK(&queries); retry_write: #ifdef NEEDTRACE SQLSetConnectAttr(obj->con, SQL_ATTR_TRACE, &enable, SQL_IS_INTEGER); SQLSetConnectAttr(obj->con, SQL_ATTR_TRACEFILE, tracefile, strlen(tracefile)); #endif 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"); pbx_builtin_setvar_helper(chan, "ODBCROWS", "-1"); return -1; } res = SQLPrepare(stmt, (unsigned char *)buf, SQL_NTS); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", buf); SQLFreeHandle (SQL_HANDLE_STMT, stmt); pbx_builtin_setvar_helper(chan, "ODBCROWS", "-1"); return -1; } res = SQLExecute(stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { if (res == SQL_ERROR) { 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); odbc_release_obj(obj); /* All handles are now invalid (after a disconnect), so we gotta redo all handles */ obj = odbc_request_obj(query->dsn, 1); if (!retry) { retry = 1; goto retry_write; } rows = -1; } else { /* Rows affected */ SQLRowCount(stmt, &rows); } /* Output the affected rows, for all cases. In the event of failure, we * flag this as -1 rows. Note that this is different from 0 affected rows * which would be the case if we succeeded in our query, but the values did * not change. */ snprintf(varname, sizeof(varname), "%d", (int)rows); pbx_builtin_setvar_helper(chan, "ODBCROWS", varname); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", buf); } SQLFreeHandle(SQL_HANDLE_STMT, stmt); return 0; } static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf, size_t len) { struct odbc_obj *obj; struct acf_odbc_query *query; char sql[2048] = "", varname[15]; int res, x, buflen = 0, escapecommas; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(field)[100]; ); SQLHSTMT stmt; SQLSMALLINT colcount=0; SQLINTEGER indicator; #ifdef NEEDTRACE SQLINTEGER enable = 1; char *tracefile = "/tmp/odbc.trace"; #endif AST_LIST_LOCK(&queries); AST_LIST_TRAVERSE(&queries, query, list) { if (!strcmp(query->acf->name, cmd)) { break; } } if (!query) { ast_log(LOG_ERROR, "No such function '%s'\n", cmd); AST_LIST_UNLOCK(&queries); return -1; } obj = odbc_request_obj(query->dsn, 0); if (!obj) { ast_log(LOG_ERROR, "No such DSN registered (or out of connections): %s (check res_odbc.conf)\n", query->dsn); AST_LIST_UNLOCK(&queries); return -1; } #ifdef NEEDTRACE SQLSetConnectAttr(obj->con, SQL_ATTR_TRACE, &enable, SQL_IS_INTEGER); SQLSetConnectAttr(obj->con, SQL_ATTR_TRACEFILE, tracefile, strlen(tracefile)); #endif AST_STANDARD_APP_ARGS(args, s); for (x = 0; x < args.argc; x++) { snprintf(varname, sizeof(varname), "ARG%d", x + 1); pbx_builtin_pushvar_helper(chan, varname, args.field[x]); } pbx_substitute_variables_helper(chan, query->sql_read, sql, sizeof(sql) - 1); /* Restore prior values */ for (x = 0; x < args.argc; x++) { snprintf(varname, sizeof(varname), "ARG%d", x + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } /* Save this flag, so we can release the lock */ escapecommas = ast_test_flag(query, OPT_ESCAPECOMMAS); AST_LIST_UNLOCK(&queries); 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 -1; } 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); SQLFreeHandle (SQL_HANDLE_STMT, stmt); return -1; } res = odbc_smart_execute(obj, stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql); SQLFreeHandle (SQL_HANDLE_STMT, stmt); return -1; } res = SQLNumResultCols(stmt, &colcount); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Column Count error!\n[%s]\n\n", sql); SQLFreeHandle (SQL_HANDLE_STMT, stmt); return -1; } *buf = '\0'; res = SQLFetch(stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { if (res == SQL_NO_DATA) { if (option_verbose > 3) { ast_verbose(VERBOSE_PREFIX_4 "Found no rows [%s]\n", sql); } } else if (option_verbose > 3) { ast_log(LOG_WARNING, "Error %d in FETCH [%s]\n", res, sql); } SQLFreeHandle(SQL_HANDLE_STMT, stmt); return 0; } for (x = 0; x < colcount; x++) { int i; char coldata[256]; buflen = strlen(buf); res = SQLGetData(stmt, x + 1, SQL_CHAR, coldata, sizeof(coldata), &indicator); if (indicator == SQL_NULL_DATA) { coldata[0] = '\0'; res = SQL_SUCCESS; } if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql); SQLFreeHandle(SQL_HANDLE_STMT, stmt); return -1; } /* Copy data, encoding '\' and ',' for the argument parser */ for (i = 0; i < sizeof(coldata); i++) { if (escapecommas && (coldata[i] == '\\' || coldata[i] == ',')) { buf[buflen++] = '\\'; } buf[buflen++] = coldata[i]; if (buflen >= len - 2) break; if (coldata[i] == '\0') break; } buf[buflen - 1] = ','; } /* Trim trailing comma */ buf[buflen - 1] = '\0'; SQLFreeHandle(SQL_HANDLE_STMT, stmt); return 0; } static int acf_escape(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len) { char *out = buf; for (; *data && out - buf < len; data++) { if (*data == '\'') { *out = '\''; out++; } *out++ = *data; } *out = '\0'; return 0; } static struct ast_custom_function escape_function = { .name = "SQL_ESC", .synopsis = "Escapes single ticks for use in SQL statements", .syntax = "SQL_ESC()", .desc = "Used in SQL templates to escape data which may contain single ticks (') which\n" "are otherwise used to delimit data. For example:\n" "SELECT foo FROM bar WHERE baz='${SQL_ESC(${ARG1})}'\n", .read = acf_escape, .write = NULL, }; static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_query **query) { char *tmp; if (!cfg || !catg) { return -1; } *query = ast_calloc(1, sizeof(struct acf_odbc_query)); if (! (*query)) return -1; if ((tmp = ast_variable_retrieve(cfg, catg, "dsn"))) { ast_copy_string((*query)->dsn, tmp, sizeof((*query)->dsn)); } else { return -1; } if ((tmp = ast_variable_retrieve(cfg, catg, "read"))) { ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read)); } if ((tmp = ast_variable_retrieve(cfg, catg, "write"))) { ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write)); } /* Allow escaping of embedded commas in fields to be turned off */ ast_set_flag((*query), OPT_ESCAPECOMMAS); if ((tmp = ast_variable_retrieve(cfg, catg, "escapecommas"))) { if (ast_false(tmp)) ast_clear_flag((*query), OPT_ESCAPECOMMAS); } (*query)->acf = ast_calloc(1, sizeof(struct ast_custom_function)); if (! (*query)->acf) { free(*query); return -1; } if ((tmp = ast_variable_retrieve(cfg, catg, "prefix")) && !ast_strlen_zero(tmp)) { asprintf((char **)&((*query)->acf->name), "%s_%s", tmp, catg); } else { asprintf((char **)&((*query)->acf->name), "ODBC_%s", catg); } if (!((*query)->acf->name)) { free((*query)->acf); free(*query); return -1; } asprintf((char **)&((*query)->acf->syntax), "%s([...[,]])", (*query)->acf->name); if (!((*query)->acf->syntax)) { free((char *)(*query)->acf->name); free((*query)->acf); free(*query); return -1; } (*query)->acf->synopsis = "Runs the referenced query with the specified arguments"; if (!ast_strlen_zero((*query)->sql_read) && !ast_strlen_zero((*query)->sql_write)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. When setting the function, the values are provided\n" "either in whole as ${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n" "\nRead:\n%s\n\nWrite:\n%s\n", (*query)->sql_read, (*query)->sql_write); } else if (!ast_strlen_zero((*query)->sql_read)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. This function may only be read, not set.\n\nSQL:\n%s\n", (*query)->sql_read); } else if (!ast_strlen_zero((*query)->sql_write)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. The values are provided either in whole as\n" "${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n" "This function may only be set.\nSQL:\n%s\n", (*query)->sql_write); } /* Could be out of memory, or could be we have neither sql_read nor sql_write */ if (! ((*query)->acf->desc)) { free((char *)(*query)->acf->syntax); free((char *)(*query)->acf->name); free((*query)->acf); free(*query); return -1; } if (ast_strlen_zero((*query)->sql_read)) { (*query)->acf->read = NULL; } else { (*query)->acf->read = acf_odbc_read; } if (ast_strlen_zero((*query)->sql_write)) { (*query)->acf->write = NULL; } else { (*query)->acf->write = acf_odbc_write; } return 0; } static int free_acf_query(struct acf_odbc_query *query) { if (query) { if (query->acf) { if (query->acf->name) free((char *)query->acf->name); if (query->acf->syntax) free((char *)query->acf->syntax); if (query->acf->desc) free((char *)query->acf->desc); free(query->acf); } free(query); } return 0; } static int odbc_load_module(void) { int res = 0; struct ast_config *cfg; char *catg; AST_LIST_LOCK(&queries); cfg = ast_config_load(config); if (!cfg) { ast_log(LOG_NOTICE, "Unable to load config for func_odbc: %s\n", config); AST_LIST_UNLOCK(&queries); return 0; } for (catg = ast_category_browse(cfg, NULL); catg; catg = ast_category_browse(cfg, catg)) { struct acf_odbc_query *query = NULL; if (init_acf_query(cfg, catg, &query)) { ast_log(LOG_ERROR, "Out of memory\n"); free_acf_query(query); } else { AST_LIST_INSERT_HEAD(&queries, query, list); ast_custom_function_register(query->acf); } } ast_config_destroy(cfg); ast_custom_function_register(&escape_function); AST_LIST_UNLOCK(&queries); return res; } static int odbc_unload_module(void) { struct acf_odbc_query *query; AST_LIST_LOCK(&queries); while (!AST_LIST_EMPTY(&queries)) { query = AST_LIST_REMOVE_HEAD(&queries, list); ast_custom_function_unregister(query->acf); free_acf_query(query); } ast_custom_function_unregister(&escape_function); /* Allow any threads waiting for this lock to pass (avoids a race) */ AST_LIST_UNLOCK(&queries); AST_LIST_LOCK(&queries); AST_LIST_UNLOCK(&queries); return 0; } static int reload(void *mod) { int res = 0; struct ast_config *cfg; struct acf_odbc_query *oldquery; char *catg; AST_LIST_LOCK(&queries); while (!AST_LIST_EMPTY(&queries)) { oldquery = AST_LIST_REMOVE_HEAD(&queries, list); ast_custom_function_unregister(oldquery->acf); free_acf_query(oldquery); } cfg = ast_config_load(config); if (!cfg) { ast_log(LOG_WARNING, "Unable to load config for func_odbc: %s\n", config); goto reload_out; } for (catg = ast_category_browse(cfg, NULL); catg; catg = ast_category_browse(cfg, catg)) { struct acf_odbc_query *query = NULL; if (init_acf_query(cfg, catg, &query)) { ast_log(LOG_ERROR, "Cannot initialize query %s\n", catg); } else { AST_LIST_INSERT_HEAD(&queries, query, list); ast_custom_function_register(query->acf); } } ast_config_destroy(cfg); reload_out: AST_LIST_UNLOCK(&queries); return res; } static int unload_module(void *mod) { return odbc_unload_module(); } static int load_module(void *mod) { return odbc_load_module(); } static const char *description(void) { return tdesc; } /* XXX need to revise usecount - set if query_lock is set */ static const char *key(void) { return ASTERISK_GPL_KEY; } STD_MOD(MOD_1, reload, NULL, NULL);