123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- /*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2015, CFWare, LLC
- *
- * Corey Farrell <git@cfware.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 Custom function management routines.
- *
- * \author Corey Farrell <git@cfware.com>
- */
- /*** MODULEINFO
- <support_level>core</support_level>
- ***/
- #include "asterisk.h"
- ASTERISK_REGISTER_FILE()
- #include "asterisk/_private.h"
- #include "asterisk/cli.h"
- #include "asterisk/linkedlists.h"
- #include "asterisk/module.h"
- #include "asterisk/pbx.h"
- #include "asterisk/term.h"
- #include "asterisk/threadstorage.h"
- #include "asterisk/xmldoc.h"
- #include "pbx_private.h"
- /*!
- * \brief A thread local indicating whether the current thread can run
- * 'dangerous' dialplan functions.
- */
- AST_THREADSTORAGE(thread_inhibit_escalations_tl);
- /*!
- * \brief Set to true (non-zero) to globally allow all dangerous dialplan
- * functions to run.
- */
- static int live_dangerously;
- /*!
- * \brief Registered functions container.
- *
- * It is sorted by function name.
- */
- static AST_RWLIST_HEAD_STATIC(acf_root, ast_custom_function);
- static char *handle_show_functions(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
- {
- struct ast_custom_function *acf;
- int count_acf = 0;
- int like = 0;
- switch (cmd) {
- case CLI_INIT:
- e->command = "core show functions [like]";
- e->usage =
- "Usage: core show functions [like <text>]\n"
- " List builtin functions, optionally only those matching a given string\n";
- return NULL;
- case CLI_GENERATE:
- return NULL;
- }
- if (a->argc == 5 && (!strcmp(a->argv[3], "like")) ) {
- like = 1;
- } else if (a->argc != 3) {
- return CLI_SHOWUSAGE;
- }
- ast_cli(a->fd, "%s Custom Functions:\n"
- "--------------------------------------------------------------------------------\n",
- like ? "Matching" : "Installed");
- AST_RWLIST_RDLOCK(&acf_root);
- AST_RWLIST_TRAVERSE(&acf_root, acf, acflist) {
- if (!like || strstr(acf->name, a->argv[4])) {
- count_acf++;
- ast_cli(a->fd, "%-20.20s %-35.35s %s\n",
- S_OR(acf->name, ""),
- S_OR(acf->syntax, ""),
- S_OR(acf->synopsis, ""));
- }
- }
- AST_RWLIST_UNLOCK(&acf_root);
- ast_cli(a->fd, "%d %scustom functions installed.\n", count_acf, like ? "matching " : "");
- return CLI_SUCCESS;
- }
- static char *complete_functions(const char *word, int pos, int state)
- {
- struct ast_custom_function *cur;
- char *ret = NULL;
- int which = 0;
- int wordlen;
- int cmp;
- if (pos != 3) {
- return NULL;
- }
- wordlen = strlen(word);
- AST_RWLIST_RDLOCK(&acf_root);
- AST_RWLIST_TRAVERSE(&acf_root, cur, acflist) {
- /*
- * Do a case-insensitive search for convenience in this
- * 'complete' function.
- *
- * We must search the entire container because the functions are
- * sorted and normally found case sensitively.
- */
- cmp = strncasecmp(word, cur->name, wordlen);
- if (!cmp) {
- /* Found match. */
- if (++which <= state) {
- /* Not enough matches. */
- continue;
- }
- ret = ast_strdup(cur->name);
- break;
- }
- }
- AST_RWLIST_UNLOCK(&acf_root);
- return ret;
- }
- static char *handle_show_function(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
- {
- struct ast_custom_function *acf;
- /* Maximum number of characters added by terminal coloring is 22 */
- char infotitle[64 + AST_MAX_APP + 22], syntitle[40], destitle[40], argtitle[40], seealsotitle[40];
- char info[64 + AST_MAX_APP], *synopsis = NULL, *description = NULL, *seealso = NULL;
- char stxtitle[40], *syntax = NULL, *arguments = NULL;
- int syntax_size, description_size, synopsis_size, arguments_size, seealso_size;
- switch (cmd) {
- case CLI_INIT:
- e->command = "core show function";
- e->usage =
- "Usage: core show function <function>\n"
- " Describe a particular dialplan function.\n";
- return NULL;
- case CLI_GENERATE:
- return complete_functions(a->word, a->pos, a->n);
- }
- if (a->argc != 4) {
- return CLI_SHOWUSAGE;
- }
- if (!(acf = ast_custom_function_find(a->argv[3]))) {
- ast_cli(a->fd, "No function by that name registered.\n");
- return CLI_FAILURE;
- }
- syntax_size = strlen(S_OR(acf->syntax, "Not Available")) + AST_TERM_MAX_ESCAPE_CHARS;
- syntax = ast_malloc(syntax_size);
- if (!syntax) {
- ast_cli(a->fd, "Memory allocation failure!\n");
- return CLI_FAILURE;
- }
- snprintf(info, sizeof(info), "\n -= Info about function '%s' =- \n\n", acf->name);
- term_color(infotitle, info, COLOR_MAGENTA, 0, sizeof(infotitle));
- term_color(syntitle, "[Synopsis]\n", COLOR_MAGENTA, 0, 40);
- term_color(destitle, "[Description]\n", COLOR_MAGENTA, 0, 40);
- term_color(stxtitle, "[Syntax]\n", COLOR_MAGENTA, 0, 40);
- term_color(argtitle, "[Arguments]\n", COLOR_MAGENTA, 0, 40);
- term_color(seealsotitle, "[See Also]\n", COLOR_MAGENTA, 0, 40);
- term_color(syntax, S_OR(acf->syntax, "Not available"), COLOR_CYAN, 0, syntax_size);
- #ifdef AST_XML_DOCS
- if (acf->docsrc == AST_XML_DOC) {
- arguments = ast_xmldoc_printable(S_OR(acf->arguments, "Not available"), 1);
- synopsis = ast_xmldoc_printable(S_OR(acf->synopsis, "Not available"), 1);
- description = ast_xmldoc_printable(S_OR(acf->desc, "Not available"), 1);
- seealso = ast_xmldoc_printable(S_OR(acf->seealso, "Not available"), 1);
- } else
- #endif
- {
- synopsis_size = strlen(S_OR(acf->synopsis, "Not Available")) + AST_TERM_MAX_ESCAPE_CHARS;
- synopsis = ast_malloc(synopsis_size);
- description_size = strlen(S_OR(acf->desc, "Not Available")) + AST_TERM_MAX_ESCAPE_CHARS;
- description = ast_malloc(description_size);
- arguments_size = strlen(S_OR(acf->arguments, "Not Available")) + AST_TERM_MAX_ESCAPE_CHARS;
- arguments = ast_malloc(arguments_size);
- seealso_size = strlen(S_OR(acf->seealso, "Not Available")) + AST_TERM_MAX_ESCAPE_CHARS;
- seealso = ast_malloc(seealso_size);
- /* check allocated memory. */
- if (!synopsis || !description || !arguments || !seealso) {
- ast_free(synopsis);
- ast_free(description);
- ast_free(arguments);
- ast_free(seealso);
- ast_free(syntax);
- return CLI_FAILURE;
- }
- term_color(arguments, S_OR(acf->arguments, "Not available"), COLOR_CYAN, 0, arguments_size);
- term_color(synopsis, S_OR(acf->synopsis, "Not available"), COLOR_CYAN, 0, synopsis_size);
- term_color(description, S_OR(acf->desc, "Not available"), COLOR_CYAN, 0, description_size);
- term_color(seealso, S_OR(acf->seealso, "Not available"), COLOR_CYAN, 0, seealso_size);
- }
- ast_cli(a->fd, "%s%s%s\n\n%s%s\n\n%s%s\n\n%s%s\n\n%s%s\n",
- infotitle, syntitle, synopsis, destitle, description,
- stxtitle, syntax, argtitle, arguments, seealsotitle, seealso);
- ast_free(arguments);
- ast_free(synopsis);
- ast_free(description);
- ast_free(seealso);
- ast_free(syntax);
- return CLI_SUCCESS;
- }
- static struct ast_custom_function *ast_custom_function_find_nolock(const char *name)
- {
- struct ast_custom_function *cur;
- int cmp;
- AST_RWLIST_TRAVERSE(&acf_root, cur, acflist) {
- cmp = strcmp(name, cur->name);
- if (cmp > 0) {
- continue;
- }
- if (!cmp) {
- /* Found it. */
- break;
- }
- /* Not in container. */
- cur = NULL;
- break;
- }
- return cur;
- }
- struct ast_custom_function *ast_custom_function_find(const char *name)
- {
- struct ast_custom_function *acf;
- AST_RWLIST_RDLOCK(&acf_root);
- acf = ast_custom_function_find_nolock(name);
- AST_RWLIST_UNLOCK(&acf_root);
- return acf;
- }
- int ast_custom_function_unregister(struct ast_custom_function *acf)
- {
- struct ast_custom_function *cur;
- if (!acf) {
- return -1;
- }
- AST_RWLIST_WRLOCK(&acf_root);
- cur = AST_RWLIST_REMOVE(&acf_root, acf, acflist);
- if (cur) {
- #ifdef AST_XML_DOCS
- if (cur->docsrc == AST_XML_DOC) {
- ast_string_field_free_memory(acf);
- }
- #endif
- ast_verb(2, "Unregistered custom function %s\n", cur->name);
- }
- AST_RWLIST_UNLOCK(&acf_root);
- return cur ? 0 : -1;
- }
- /*!
- * \brief Returns true if given custom function escalates privileges on read.
- *
- * \param acf Custom function to query.
- * \return True (non-zero) if reads escalate privileges.
- * \return False (zero) if reads just read.
- */
- static int read_escalates(const struct ast_custom_function *acf)
- {
- return acf->read_escalates;
- }
- /*!
- * \brief Returns true if given custom function escalates privileges on write.
- *
- * \param acf Custom function to query.
- * \return True (non-zero) if writes escalate privileges.
- * \return False (zero) if writes just write.
- */
- static int write_escalates(const struct ast_custom_function *acf)
- {
- return acf->write_escalates;
- }
- /*! \internal
- * \brief Retrieve the XML documentation of a specified ast_custom_function,
- * and populate ast_custom_function string fields.
- * \param acf ast_custom_function structure with empty 'desc' and 'synopsis'
- * but with a function 'name'.
- * \retval -1 On error.
- * \retval 0 On succes.
- */
- static int acf_retrieve_docs(struct ast_custom_function *acf)
- {
- #ifdef AST_XML_DOCS
- char *tmpxml;
- /* Let's try to find it in the Documentation XML */
- if (!ast_strlen_zero(acf->desc) || !ast_strlen_zero(acf->synopsis)) {
- return 0;
- }
- if (ast_string_field_init(acf, 128)) {
- return -1;
- }
- /* load synopsis */
- tmpxml = ast_xmldoc_build_synopsis("function", acf->name, ast_module_name(acf->mod));
- ast_string_field_set(acf, synopsis, tmpxml);
- ast_free(tmpxml);
- /* load description */
- tmpxml = ast_xmldoc_build_description("function", acf->name, ast_module_name(acf->mod));
- ast_string_field_set(acf, desc, tmpxml);
- ast_free(tmpxml);
- /* load syntax */
- tmpxml = ast_xmldoc_build_syntax("function", acf->name, ast_module_name(acf->mod));
- ast_string_field_set(acf, syntax, tmpxml);
- ast_free(tmpxml);
- /* load arguments */
- tmpxml = ast_xmldoc_build_arguments("function", acf->name, ast_module_name(acf->mod));
- ast_string_field_set(acf, arguments, tmpxml);
- ast_free(tmpxml);
- /* load seealso */
- tmpxml = ast_xmldoc_build_seealso("function", acf->name, ast_module_name(acf->mod));
- ast_string_field_set(acf, seealso, tmpxml);
- ast_free(tmpxml);
- acf->docsrc = AST_XML_DOC;
- #endif
- return 0;
- }
- int __ast_custom_function_register(struct ast_custom_function *acf, struct ast_module *mod)
- {
- struct ast_custom_function *cur;
- if (!acf) {
- return -1;
- }
- acf->mod = mod;
- #ifdef AST_XML_DOCS
- acf->docsrc = AST_STATIC_DOC;
- #endif
- if (acf_retrieve_docs(acf)) {
- return -1;
- }
- AST_RWLIST_WRLOCK(&acf_root);
- cur = ast_custom_function_find_nolock(acf->name);
- if (cur) {
- ast_log(LOG_ERROR, "Function %s already registered.\n", acf->name);
- AST_RWLIST_UNLOCK(&acf_root);
- return -1;
- }
- /* Store in alphabetical order */
- AST_RWLIST_TRAVERSE_SAFE_BEGIN(&acf_root, cur, acflist) {
- if (strcmp(acf->name, cur->name) < 0) {
- AST_RWLIST_INSERT_BEFORE_CURRENT(acf, acflist);
- break;
- }
- }
- AST_RWLIST_TRAVERSE_SAFE_END;
- if (!cur) {
- AST_RWLIST_INSERT_TAIL(&acf_root, acf, acflist);
- }
- AST_RWLIST_UNLOCK(&acf_root);
- ast_verb(2, "Registered custom function '" COLORIZE_FMT "'\n", COLORIZE(COLOR_BRCYAN, 0, acf->name));
- return 0;
- }
- int __ast_custom_function_register_escalating(struct ast_custom_function *acf, enum ast_custom_function_escalation escalation, struct ast_module *mod)
- {
- int res;
- res = __ast_custom_function_register(acf, mod);
- if (res != 0) {
- return -1;
- }
- switch (escalation) {
- case AST_CFE_NONE:
- break;
- case AST_CFE_READ:
- acf->read_escalates = 1;
- break;
- case AST_CFE_WRITE:
- acf->write_escalates = 1;
- break;
- case AST_CFE_BOTH:
- acf->read_escalates = 1;
- acf->write_escalates = 1;
- break;
- }
- return 0;
- }
- /*! \brief return a pointer to the arguments of the function,
- * and terminates the function name with '\\0'
- */
- static char *func_args(char *function)
- {
- char *args = strchr(function, '(');
- if (!args) {
- ast_log(LOG_WARNING, "Function '%s' doesn't contain parentheses. Assuming null argument.\n", function);
- } else {
- char *p;
- *args++ = '\0';
- if ((p = strrchr(args, ')'))) {
- *p = '\0';
- } else {
- ast_log(LOG_WARNING, "Can't find trailing parenthesis for function '%s(%s'?\n", function, args);
- }
- }
- return args;
- }
- void pbx_live_dangerously(int new_live_dangerously)
- {
- if (new_live_dangerously && !live_dangerously) {
- ast_log(LOG_WARNING, "Privilege escalation protection disabled!\n"
- "See https://wiki.asterisk.org/wiki/x/1gKfAQ for more details.\n");
- }
- if (!new_live_dangerously && live_dangerously) {
- ast_log(LOG_NOTICE, "Privilege escalation protection enabled.\n");
- }
- live_dangerously = new_live_dangerously;
- }
- int ast_thread_inhibit_escalations(void)
- {
- int *thread_inhibit_escalations;
- thread_inhibit_escalations = ast_threadstorage_get(
- &thread_inhibit_escalations_tl, sizeof(*thread_inhibit_escalations));
- if (thread_inhibit_escalations == NULL) {
- ast_log(LOG_ERROR, "Error inhibiting privilege escalations for current thread\n");
- return -1;
- }
- *thread_inhibit_escalations = 1;
- return 0;
- }
- int ast_thread_inhibit_escalations_swap(int inhibit)
- {
- int *thread_inhibit_escalations;
- int orig;
- thread_inhibit_escalations = ast_threadstorage_get(
- &thread_inhibit_escalations_tl, sizeof(*thread_inhibit_escalations));
- if (thread_inhibit_escalations == NULL) {
- ast_log(LOG_ERROR, "Error swapping privilege escalations inhibit for current thread\n");
- return -1;
- }
- orig = *thread_inhibit_escalations;
- *thread_inhibit_escalations = !!inhibit;
- return orig;
- }
- /*!
- * \brief Indicates whether the current thread inhibits the execution of
- * dangerous functions.
- *
- * \return True (non-zero) if dangerous function execution is inhibited.
- * \return False (zero) if dangerous function execution is allowed.
- */
- static int thread_inhibits_escalations(void)
- {
- int *thread_inhibit_escalations;
- thread_inhibit_escalations = ast_threadstorage_get(
- &thread_inhibit_escalations_tl, sizeof(*thread_inhibit_escalations));
- if (thread_inhibit_escalations == NULL) {
- ast_log(LOG_ERROR, "Error checking thread's ability to run dangerous functions\n");
- /* On error, assume that we are inhibiting */
- return 1;
- }
- return *thread_inhibit_escalations;
- }
- /*!
- * \brief Determines whether execution of a custom function's read function
- * is allowed.
- *
- * \param acfptr Custom function to check
- * \return True (non-zero) if reading is allowed.
- * \return False (zero) if reading is not allowed.
- */
- static int is_read_allowed(struct ast_custom_function *acfptr)
- {
- if (!acfptr) {
- return 1;
- }
- if (!read_escalates(acfptr)) {
- return 1;
- }
- if (!thread_inhibits_escalations()) {
- return 1;
- }
- if (live_dangerously) {
- /* Global setting overrides the thread's preference */
- ast_debug(2, "Reading %s from a dangerous context\n",
- acfptr->name);
- return 1;
- }
- /* We have no reason to allow this function to execute */
- return 0;
- }
- /*!
- * \brief Determines whether execution of a custom function's write function
- * is allowed.
- *
- * \param acfptr Custom function to check
- * \return True (non-zero) if writing is allowed.
- * \return False (zero) if writing is not allowed.
- */
- static int is_write_allowed(struct ast_custom_function *acfptr)
- {
- if (!acfptr) {
- return 1;
- }
- if (!write_escalates(acfptr)) {
- return 1;
- }
- if (!thread_inhibits_escalations()) {
- return 1;
- }
- if (live_dangerously) {
- /* Global setting overrides the thread's preference */
- ast_debug(2, "Writing %s from a dangerous context\n",
- acfptr->name);
- return 1;
- }
- /* We have no reason to allow this function to execute */
- return 0;
- }
- int ast_func_read(struct ast_channel *chan, const char *function, char *workspace, size_t len)
- {
- char *copy = ast_strdupa(function);
- char *args = func_args(copy);
- struct ast_custom_function *acfptr = ast_custom_function_find(copy);
- int res;
- struct ast_module_user *u = NULL;
- if (acfptr == NULL) {
- ast_log(LOG_ERROR, "Function %s not registered\n", copy);
- } else if (!acfptr->read && !acfptr->read2) {
- ast_log(LOG_ERROR, "Function %s cannot be read\n", copy);
- } else if (!is_read_allowed(acfptr)) {
- ast_log(LOG_ERROR, "Dangerous function %s read blocked\n", copy);
- } else if (acfptr->read) {
- if (acfptr->mod) {
- u = __ast_module_user_add(acfptr->mod, chan);
- }
- res = acfptr->read(chan, copy, args, workspace, len);
- if (acfptr->mod && u) {
- __ast_module_user_remove(acfptr->mod, u);
- }
- return res;
- } else {
- struct ast_str *str = ast_str_create(16);
- if (acfptr->mod) {
- u = __ast_module_user_add(acfptr->mod, chan);
- }
- res = acfptr->read2(chan, copy, args, &str, 0);
- if (acfptr->mod && u) {
- __ast_module_user_remove(acfptr->mod, u);
- }
- ast_copy_string(workspace, ast_str_buffer(str), len > ast_str_size(str) ? ast_str_size(str) : len);
- ast_free(str);
- return res;
- }
- return -1;
- }
- int ast_func_read2(struct ast_channel *chan, const char *function, struct ast_str **str, ssize_t maxlen)
- {
- char *copy = ast_strdupa(function);
- char *args = func_args(copy);
- struct ast_custom_function *acfptr = ast_custom_function_find(copy);
- int res;
- struct ast_module_user *u = NULL;
- if (acfptr == NULL) {
- ast_log(LOG_ERROR, "Function %s not registered\n", copy);
- } else if (!acfptr->read && !acfptr->read2) {
- ast_log(LOG_ERROR, "Function %s cannot be read\n", copy);
- } else if (!is_read_allowed(acfptr)) {
- ast_log(LOG_ERROR, "Dangerous function %s read blocked\n", copy);
- } else {
- if (acfptr->mod) {
- u = __ast_module_user_add(acfptr->mod, chan);
- }
- ast_str_reset(*str);
- if (acfptr->read2) {
- /* ast_str enabled */
- res = acfptr->read2(chan, copy, args, str, maxlen);
- } else {
- /* Legacy function pointer, allocate buffer for result */
- int maxsize = ast_str_size(*str);
- if (maxlen > -1) {
- if (maxlen == 0) {
- if (acfptr->read_max) {
- maxsize = acfptr->read_max;
- } else {
- maxsize = VAR_BUF_SIZE;
- }
- } else {
- maxsize = maxlen;
- }
- ast_str_make_space(str, maxsize);
- }
- res = acfptr->read(chan, copy, args, ast_str_buffer(*str), maxsize);
- }
- if (acfptr->mod && u) {
- __ast_module_user_remove(acfptr->mod, u);
- }
- return res;
- }
- return -1;
- }
- int ast_func_write(struct ast_channel *chan, const char *function, const char *value)
- {
- char *copy = ast_strdupa(function);
- char *args = func_args(copy);
- struct ast_custom_function *acfptr = ast_custom_function_find(copy);
- if (acfptr == NULL) {
- ast_log(LOG_ERROR, "Function %s not registered\n", copy);
- } else if (!acfptr->write) {
- ast_log(LOG_ERROR, "Function %s cannot be written to\n", copy);
- } else if (!is_write_allowed(acfptr)) {
- ast_log(LOG_ERROR, "Dangerous function %s write blocked\n", copy);
- } else {
- int res;
- struct ast_module_user *u = NULL;
- if (acfptr->mod) {
- u = __ast_module_user_add(acfptr->mod, chan);
- }
- res = acfptr->write(chan, copy, args, value);
- if (acfptr->mod && u) {
- __ast_module_user_remove(acfptr->mod, u);
- }
- return res;
- }
- return -1;
- }
- static struct ast_cli_entry acf_cli[] = {
- AST_CLI_DEFINE(handle_show_functions, "Shows registered dialplan functions"),
- AST_CLI_DEFINE(handle_show_function, "Describe a specific dialplan function"),
- };
- static void unload_pbx_functions_cli(void)
- {
- ast_cli_unregister_multiple(acf_cli, ARRAY_LEN(acf_cli));
- }
- int load_pbx_functions_cli(void)
- {
- ast_cli_register_multiple(acf_cli, ARRAY_LEN(acf_cli));
- ast_register_cleanup(unload_pbx_functions_cli);
- return 0;
- }
|