diff options
-rw-r--r-- | CHANGES | 39 | ||||
-rw-r--r-- | apps/app_skel.c | 2 | ||||
-rw-r--r-- | channels/chan_pjsip.c | 1 | ||||
-rw-r--r-- | channels/chan_sip.c | 2 | ||||
-rw-r--r-- | funcs/func_aes.c | 2 | ||||
-rw-r--r-- | funcs/func_curl.c | 212 | ||||
-rw-r--r-- | include/asterisk/cli.h | 12 | ||||
-rw-r--r-- | main/app.c | 2 | ||||
-rw-r--r-- | main/asterisk.c | 7 | ||||
-rw-r--r-- | main/cli.c | 53 | ||||
-rw-r--r-- | main/config_options.c | 2 | ||||
-rw-r--r-- | main/file.c | 5 | ||||
-rw-r--r-- | main/logger.c | 3 | ||||
-rw-r--r-- | main/manager.c | 8 | ||||
-rw-r--r-- | main/media_cache.c | 44 | ||||
-rw-r--r-- | main/utils.c | 1 | ||||
-rw-r--r-- | res/res_curl.c | 1 | ||||
-rw-r--r-- | res/res_http_media_cache.c | 447 | ||||
-rw-r--r-- | res/res_musiconhold.c | 17 | ||||
-rw-r--r-- | res/res_pjsip/pjsip_options.c | 6 | ||||
-rw-r--r-- | tests/test_http_media_cache.c | 700 |
21 files changed, 1467 insertions, 99 deletions
@@ -31,6 +31,21 @@ ConfBridge - record_command: a command to execute when recording is finished Note that these options may also be with the CONFBRIDGE function. +ControlPlayback +------------------ + * Remote files can now be retrieved and played back. See the Playback + dialplan application for more details. + +Playback +------------------ + * Remote files can now be retrieved and played back via the Playback and other + media playback dialplan applications. This is done by directly providing + the URL to play to the dialplan application: + same => n,Playback(http://1.1.1.1/howler-monkeys-fl.wav) + Note that unlike 'normal' media files, the entire URI to the file must be + provided, including the file extension. Currently, on HTTP and HTTPS URI + schemes are supported. + SMS ------------------ * Added the 'n' option, which prevents the SMS from being written to the log @@ -132,6 +147,17 @@ Core of '[json]' can be set, e.g., full => [json]debug,verbose,notice,warning,error + * The core now supports a 'media cache', which stores temporary media files + retrieved from external sources. CLI commands have been added to manipulate + and display the cached files, including: + - 'media cache show <all>' - show all cached media files, or details about + one particular cached media file + - 'media cache refresh <item>' - force a refresh of a particular media file + in the cache + - 'media cache delete <item>' - remove an item from the cache + - 'media cache create <uri>' - retrieve a URI and store it in the cache + + Functions ------------------ @@ -140,6 +166,13 @@ CHANNEL * Added CHANNEL(onhold) item that returns 1 (onhold) and 0 (not-onhold) for the hold status of a channel. +CURL +------------------ + * The CURL function now supports a write option, which will save the retrieved + file to a location on disk. As an example: + same => n,Set(CURL(https://1.1.1.1/foo.wav)=/tmp/foo.wav) + will save 'foo.wav' to /tmp. + DTMF Features ------------------ * The transferdialattempts default value has been changed from 1 to 3. The @@ -150,6 +183,12 @@ DTMF Features Resources ------------------ +res_http_media_cache +------------------ + * A backend for the core media cache, this module retrieves media files from + a remote HTTP(S) server and stores them in the core media cache for later + playback. + res_musiconhold ------------------ * Added sort=randstart to the sort options. It sorts the files by name and diff --git a/apps/app_skel.c b/apps/app_skel.c index 54ecbe1ec..0f17d9bf3 100644 --- a/apps/app_skel.c +++ b/apps/app_skel.c @@ -739,7 +739,7 @@ static int load_module(void) /* Level options */ aco_option_register(&cfg_info, "max_number", ACO_EXACT, level_options, NULL, OPT_UINT_T, 0, FLDSET(struct skel_level, max_num)); - aco_option_register(&cfg_info, "max_guesses", ACO_EXACT, level_options, NULL, OPT_UINT_T, 1, FLDSET(struct skel_level, max_guesses)); + aco_option_register(&cfg_info, "max_guesses", ACO_EXACT, level_options, NULL, OPT_UINT_T, 0, FLDSET(struct skel_level, max_guesses)); if (aco_process_config(&cfg_info, 0) == ACO_PROCESS_ERROR) { goto error; diff --git a/channels/chan_pjsip.c b/channels/chan_pjsip.c index 729f453f9..cd55400c3 100644 --- a/channels/chan_pjsip.c +++ b/channels/chan_pjsip.c @@ -348,6 +348,7 @@ static int send_direct_media_request(void *data) if (direct_media_mitigate_glare(cdata->session)) { ast_debug(4, "Disregarding setting RTP on %s: mitigating re-INVITE glare\n", ast_channel_name(cdata->chan)); + ao2_ref(cdata, -1); return 0; } diff --git a/channels/chan_sip.c b/channels/chan_sip.c index 97cd6abb0..09ab1a196 100644 --- a/channels/chan_sip.c +++ b/channels/chan_sip.c @@ -18859,7 +18859,7 @@ static void check_via(struct sip_pvt *p, const struct sip_request *req) c = strchr(via, ' '); if (c) { *c = '\0'; - c = ast_skip_blanks(c+1); + c = ast_strip(c+1); if (strcasecmp(via, "SIP/2.0/UDP") && strcasecmp(via, "SIP/2.0/TCP") && strcasecmp(via, "SIP/2.0/TLS")) { ast_log(LOG_WARNING, "Don't know how to respond via '%s'\n", via); return; diff --git a/funcs/func_aes.c b/funcs/func_aes.c index 9347b6f8e..d80636f6d 100644 --- a/funcs/func_aes.c +++ b/funcs/func_aes.c @@ -146,7 +146,7 @@ static int aes_helper(struct ast_channel *chan, const char *cmd, char *data, } if (encrypt) { /* if encrypting encode result to base64 */ - ast_base64encode(buf, (unsigned char *) tmp, strlen(tmp), len); + ast_base64encode(buf, (unsigned char *) tmp, tmpP - tmp, len); } else { memcpy(buf, tmp, len); } diff --git a/funcs/func_curl.c b/funcs/func_curl.c index fd03fc375..6a8c36767 100644 --- a/funcs/func_curl.c +++ b/funcs/func_curl.c @@ -58,15 +58,39 @@ ASTERISK_REGISTER_FILE() Retrieve content from a remote web or ftp server </synopsis> <syntax> - <parameter name="url" required="true" /> + <parameter name="url" required="true"> + <para>The full URL for the resource to retrieve.</para> + </parameter> <parameter name="post-data"> + <para><emphasis>Read Only</emphasis></para> <para>If specified, an <literal>HTTP POST</literal> will be performed with the content of <replaceable>post-data</replaceable>, instead of an <literal>HTTP GET</literal> (default).</para> </parameter> </syntax> - <description /> + <description> + <para>When this function is read, a <literal>HTTP GET</literal> + (by default) will be used to retrieve the contents of the provided + <replaceable>url</replaceable>. The contents are returned as the + result of the function.</para> + <example title="Displaying contents of a page" language="text"> + exten => s,1,Verbose(0, ${CURL(http://localhost:8088/static/astman.css)}) + </example> + <para>When this function is written to, a <literal>HTTP GET</literal> + will be used to retrieve the contents of the provided + <replaceable>url</replaceable>. The value written to the function + specifies the destination file of the cURL'd resource.</para> + <example title="Retrieving a file" language="text"> + exten => s,1,Set(CURL(http://localhost:8088/static/astman.css)=/var/spool/asterisk/tmp/astman.css)) + </example> + <note> + <para>If <literal>live_dangerously</literal> in <literal>asterisk.conf</literal> + is set to <literal>no</literal>, this function can only be written to from the + dialplan, and not directly from external protocols. Read operations are + unaffected.</para> + </note> + </description> <see-also> <ref type="function">CURLOPT</ref> </see-also> @@ -526,16 +550,27 @@ static int acf_curlopt_read2(struct ast_channel *chan, const char *cmd, char *da return acf_curlopt_helper(chan, cmd, data, NULL, buf, len); } +/*! \brief Callback data passed to \ref WriteMemoryCallback */ +struct curl_write_callback_data { + /*! \brief If a string is being built, the string buffer */ + struct ast_str *str; + /*! \brief The max size of \ref str */ + ssize_t len; + /*! \brief If a file is being retrieved, the file to write to */ + FILE *out_file; +}; + static size_t WriteMemoryCallback(void *ptr, size_t size, size_t nmemb, void *data) { - register int realsize = size * nmemb; - struct ast_str **pstr = (struct ast_str **)data; - - ast_debug(3, "Called with data=%p, str=%p, realsize=%d, len=%zu, used=%zu\n", data, *pstr, realsize, ast_str_size(*pstr), ast_str_strlen(*pstr)); - - ast_str_append_substr(pstr, 0, ptr, realsize); - - ast_debug(3, "Now, len=%zu, used=%zu\n", ast_str_size(*pstr), ast_str_strlen(*pstr)); + register int realsize = 0; + struct curl_write_callback_data *cb_data = data; + + if (cb_data->str) { + realsize = size * nmemb; + ast_str_append_substr(&cb_data->str, 0, ptr, realsize); + } else if (cb_data->out_file) { + realsize = fwrite(ptr, size, nmemb, cb_data->out_file); + } return realsize; } @@ -594,15 +629,16 @@ static int url_is_vulnerable(const char *url) return 0; } -static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info, char *buf, struct ast_str **input_str, ssize_t len) +struct curl_args { + const char *url; + const char *postdata; + struct curl_write_callback_data cb_data; +}; + +static int acf_curl_helper(struct ast_channel *chan, struct curl_args *args) { struct ast_str *escapebuf = ast_str_thread_get(&thread_escapebuf, 16); - struct ast_str *str = ast_str_create(16); int ret = -1; - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(url); - AST_APP_ARG(postdata); - ); CURL **curl; struct curl_settings *cur; struct ast_datastore *store = NULL; @@ -610,29 +646,17 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info AST_LIST_HEAD(global_curl_info, curl_settings) *list = NULL; char curl_errbuf[CURL_ERROR_SIZE + 1]; /* add one to be safe */ - if (buf) { - *buf = '\0'; - } - - if (!str) { - return -1; - } - if (!escapebuf) { - ast_free(str); return -1; } - if (ast_strlen_zero(info)) { - ast_log(LOG_WARNING, "CURL requires an argument (URL)\n"); - ast_free(str); + if (!(curl = ast_threadstorage_get(&curl_instance, sizeof(*curl)))) { + ast_log(LOG_ERROR, "Cannot allocate curl structure\n"); return -1; } - AST_STANDARD_APP_ARGS(args, info); - - if (url_is_vulnerable(args.url)) { - ast_log(LOG_ERROR, "URL '%s' is vulnerable to HTTP injection attacks. Aborting CURL() call.\n", args.url); + if (url_is_vulnerable(args->url)) { + ast_log(LOG_ERROR, "URL '%s' is vulnerable to HTTP injection attacks. Aborting CURL() call.\n", args->url); return -1; } @@ -640,12 +664,6 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info ast_autoservice_start(chan); } - if (!(curl = ast_threadstorage_get(&curl_instance, sizeof(*curl)))) { - ast_log(LOG_ERROR, "Cannot allocate curl structure\n"); - ast_free(str); - return -1; - } - AST_LIST_LOCK(&global_curl_info); AST_LIST_TRAVERSE(&global_curl_info, cur, list) { if (cur->key == CURLOPT_SPECIAL_HASHCOMPAT) { @@ -668,12 +686,12 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info } } - curl_easy_setopt(*curl, CURLOPT_URL, args.url); - curl_easy_setopt(*curl, CURLOPT_FILE, (void *) &str); + curl_easy_setopt(*curl, CURLOPT_URL, args->url); + curl_easy_setopt(*curl, CURLOPT_FILE, (void *) &args->cb_data); - if (args.postdata) { + if (args->postdata) { curl_easy_setopt(*curl, CURLOPT_POST, 1); - curl_easy_setopt(*curl, CURLOPT_POSTFIELDS, args.postdata); + curl_easy_setopt(*curl, CURLOPT_POSTFIELDS, args->postdata); } /* Temporarily assign a buffer for curl to write errors to. */ @@ -681,7 +699,7 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info curl_easy_setopt(*curl, CURLOPT_ERRORBUFFER, curl_errbuf); if (curl_easy_perform(*curl) != 0) { - ast_log(LOG_WARNING, "%s ('%s')\n", curl_errbuf, args.url); + ast_log(LOG_WARNING, "%s ('%s')\n", curl_errbuf, args->url); } /* Reset buffer to NULL so curl doesn't try to write to it when the @@ -694,19 +712,19 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info AST_LIST_UNLOCK(list); } - if (args.postdata) { + if (args->postdata) { curl_easy_setopt(*curl, CURLOPT_POST, 0); } - if (ast_str_strlen(str)) { - ast_str_trim_blanks(str); + if (args->cb_data.str && ast_str_strlen(args->cb_data.str)) { + ast_str_trim_blanks(args->cb_data.str); - ast_debug(3, "str='%s'\n", ast_str_buffer(str)); + ast_debug(3, "CURL returned str='%s'\n", ast_str_buffer(args->cb_data.str)); if (hashcompat) { - char *remainder = ast_str_buffer(str); + char *remainder = ast_str_buffer(args->cb_data.str); char *piece; - struct ast_str *fields = ast_str_create(ast_str_strlen(str) / 2); - struct ast_str *values = ast_str_create(ast_str_strlen(str) / 2); + struct ast_str *fields = ast_str_create(ast_str_strlen(args->cb_data.str) / 2); + struct ast_str *values = ast_str_create(ast_str_strlen(args->cb_data.str) / 2); int rowcount = 0; while (fields && values && (piece = strsep(&remainder, "&"))) { char *name = strsep(&piece, "="); @@ -720,49 +738,93 @@ static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info rowcount++; } pbx_builtin_setvar_helper(chan, "~ODBCFIELDS~", ast_str_buffer(fields)); - if (buf) { - ast_copy_string(buf, ast_str_buffer(values), len); - } else { - ast_str_set(input_str, len, "%s", ast_str_buffer(values)); - } + ast_str_set(&args->cb_data.str, 0, "%s", ast_str_buffer(values)); ast_free(fields); ast_free(values); - } else { - if (buf) { - ast_copy_string(buf, ast_str_buffer(str), len); - } else { - ast_str_set(input_str, len, "%s", ast_str_buffer(str)); - } } ret = 0; } - ast_free(str); - if (chan) + if (chan) { ast_autoservice_stop(chan); + } return ret; } -static int acf_curl_exec(struct ast_channel *chan, const char *cmd, char *info, char *buf, size_t len) +static int acf_curl_exec(struct ast_channel *chan, const char *cmd, char *info, struct ast_str **buf, ssize_t len) { - return acf_curl_helper(chan, cmd, info, buf, NULL, len); + struct curl_args curl_params = { 0, }; + int res; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(url); + AST_APP_ARG(postdata); + ); + + AST_STANDARD_APP_ARGS(args, info); + + if (ast_strlen_zero(info)) { + ast_log(LOG_WARNING, "CURL requires an argument (URL)\n"); + return -1; + } + + curl_params.url = args.url; + curl_params.postdata = args.postdata; + curl_params.cb_data.str = ast_str_create(16); + if (!curl_params.cb_data.str) { + return -1; + } + + res = acf_curl_helper(chan, &curl_params); + ast_str_set(buf, len, "%s", ast_str_buffer(curl_params.cb_data.str)); + ast_free(curl_params.cb_data.str); + + return res; } -static int acf_curl2_exec(struct ast_channel *chan, const char *cmd, char *info, struct ast_str **buf, ssize_t len) +static int acf_curl_write(struct ast_channel *chan, const char *cmd, char *name, const char *value) { - return acf_curl_helper(chan, cmd, info, NULL, buf, len); + struct curl_args curl_params = { 0, }; + int res; + char *args_value = ast_strdupa(value); + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(file_path); + ); + + AST_STANDARD_APP_ARGS(args, args_value); + + if (ast_strlen_zero(name)) { + ast_log(LOG_WARNING, "CURL requires an argument (URL)\n"); + return -1; + } + + if (ast_strlen_zero(args.file_path)) { + ast_log(LOG_WARNING, "CURL requires a file to write\n"); + return -1; + } + + curl_params.url = name; + curl_params.cb_data.out_file = fopen(args.file_path, "w"); + if (!curl_params.cb_data.out_file) { + ast_log(LOG_WARNING, "Failed to open file %s: %s (%d)\n", + args.file_path, + strerror(errno), + errno); + return -1; + } + + res = acf_curl_helper(chan, &curl_params); + + fclose(curl_params.cb_data.out_file); + + return res; } static struct ast_custom_function acf_curl = { .name = "CURL", - .synopsis = "Retrieves the contents of a URL", - .syntax = "CURL(url[,post-data])", - .desc = - " url - URL to retrieve\n" - " post-data - Optional data to send as a POST (GET is default action)\n", - .read = acf_curl_exec, - .read2 = acf_curl2_exec, + .read2 = acf_curl_exec, + .write = acf_curl_write, }; static struct ast_custom_function acf_curlopt = { @@ -865,7 +927,7 @@ static int load_module(void) } } - res = ast_custom_function_register(&acf_curl); + res = ast_custom_function_register_escalating(&acf_curl, AST_CFE_WRITE); res |= ast_custom_function_register(&acf_curlopt); AST_TEST_REGISTER(vulnerable_url); diff --git a/include/asterisk/cli.h b/include/asterisk/cli.h index 0bda6665c..c79a4e93c 100644 --- a/include/asterisk/cli.h +++ b/include/asterisk/cli.h @@ -326,6 +326,18 @@ char *ast_complete_channels(const char *line, const char *word, int pos, int sta */ void ast_cli_print_timestr_fromseconds(int fd, int seconds, const char *prefix); +/* + * \brief Allow a CLI command to be executed while Asterisk is shutting down. + * + * CLI commands by defeault are disabled when Asterisk is shutting down. This is + * to ensure the safety of the shutdown since CLI commands may attempt to access + * resources that have been freed as a result of the shutdown. + * + * If a CLI command should be allowed at shutdown, then the best way to enable this + * is to call ast_cli_allow_at_shutdown during the CLI_INIT state of the CLI handler. + */ +int ast_cli_allow_at_shutdown(struct ast_cli_entry *e); + #if defined(__cplusplus) || defined(c_plusplus) } #endif diff --git a/main/app.c b/main/app.c index 826e41128..e1d70498c 100644 --- a/main/app.c +++ b/main/app.c @@ -1112,6 +1112,8 @@ static int control_streamfile(struct ast_channel *chan, if (!strcasecmp(end, ":end")) { *end = '\0'; end++; + } else { + end = NULL; } } diff --git a/main/asterisk.c b/main/asterisk.c index da804e196..7636ec7b4 100644 --- a/main/asterisk.c +++ b/main/asterisk.c @@ -2343,6 +2343,7 @@ static char *handle_stop_now(struct ast_cli_entry *e, int cmd, struct ast_cli_ar e->usage = "Usage: core stop now\n" " Shuts down a running Asterisk immediately, hanging up all active calls .\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2363,6 +2364,7 @@ static char *handle_stop_gracefully(struct ast_cli_entry *e, int cmd, struct ast "Usage: core stop gracefully\n" " Causes Asterisk to not accept new calls, and exit when all\n" " active calls have terminated normally.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2382,6 +2384,7 @@ static char *handle_stop_when_convenient(struct ast_cli_entry *e, int cmd, struc e->usage = "Usage: core stop when convenient\n" " Causes Asterisk to perform a shutdown when all active calls have ended.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2403,6 +2406,7 @@ static char *handle_restart_now(struct ast_cli_entry *e, int cmd, struct ast_cli "Usage: core restart now\n" " Causes Asterisk to hangup all calls and exec() itself performing a cold\n" " restart.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2423,6 +2427,7 @@ static char *handle_restart_gracefully(struct ast_cli_entry *e, int cmd, struct "Usage: core restart gracefully\n" " Causes Asterisk to stop accepting new calls and exec() itself performing a cold\n" " restart when all active calls have ended.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2442,6 +2447,7 @@ static char *handle_restart_when_convenient(struct ast_cli_entry *e, int cmd, st e->usage = "Usage: core restart when convenient\n" " Causes Asterisk to perform a cold restart when all active calls have ended.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; @@ -2463,6 +2469,7 @@ static char *handle_abort_shutdown(struct ast_cli_entry *e, int cmd, struct ast_ "Usage: core abort shutdown\n" " Causes Asterisk to abort an executing shutdown or restart, and resume normal\n" " call operations.\n"; + ast_cli_allow_at_shutdown(e); return NULL; case CLI_GENERATE: return NULL; diff --git a/main/cli.c b/main/cli.c index 0ac5d612a..f2bedc91a 100644 --- a/main/cli.c +++ b/main/cli.c @@ -63,6 +63,7 @@ ASTERISK_REGISTER_FILE() #include "asterisk/bridge.h" #include "asterisk/stasis_channels.h" #include "asterisk/stasis_bridges.h" +#include "asterisk/vector.h" /*! * \brief List of restrictions per user. @@ -109,6 +110,9 @@ static struct module_level_list debug_modules = AST_RWLIST_HEAD_INIT_VALUE; AST_THREADSTORAGE(ast_cli_buf); +AST_RWLOCK_DEFINE_STATIC(shutdown_commands_lock); +static AST_VECTOR(, struct ast_cli_entry *) shutdown_commands; + /*! \brief Initial buffer size for resulting strings in ast_cli() */ #define AST_CLI_INITLEN 256 @@ -2031,6 +2035,7 @@ static void cli_shutdown(void) /*! \brief initialize the _full_cmd string in * each of the builtins. */ void ast_builtins_init(void) { + AST_VECTOR_INIT(&shutdown_commands, 0); ast_cli_register_multiple(cli_cli, ARRAY_LEN(cli_cli)); ast_register_cleanup(cli_shutdown); } @@ -2209,6 +2214,13 @@ static int cli_is_registered(struct ast_cli_entry *e) return 0; } +static void remove_shutdown_command(struct ast_cli_entry *e) +{ + ast_rwlock_wrlock(&shutdown_commands_lock); + AST_VECTOR_REMOVE_ELEM_UNORDERED(&shutdown_commands, e, AST_VECTOR_ELEM_CLEANUP_NOOP); + ast_rwlock_unlock(&shutdown_commands_lock); +} + int ast_cli_unregister(struct ast_cli_entry *e) { if (e->inuse) { @@ -2217,6 +2229,7 @@ int ast_cli_unregister(struct ast_cli_entry *e) AST_RWLIST_WRLOCK(&helpers); AST_RWLIST_REMOVE(&helpers, e, list); AST_RWLIST_UNLOCK(&helpers); + remove_shutdown_command(e); ast_free(e->_full_cmd); e->_full_cmd = NULL; if (e->handler) { @@ -2675,10 +2688,27 @@ char *ast_cli_generator(const char *text, const char *word, int state) return __ast_cli_generator(text, word, state, 1); } +static int allowed_on_shutdown(struct ast_cli_entry *e) +{ + int found = 0; + int i; + + ast_rwlock_rdlock(&shutdown_commands_lock); + for (i = 0; i < AST_VECTOR_SIZE(&shutdown_commands); ++i) { + if (e == AST_VECTOR_GET(&shutdown_commands, i)) { + found = 1; + break; + } + } + ast_rwlock_unlock(&shutdown_commands_lock); + + return found; +} + int ast_cli_command_full(int uid, int gid, int fd, const char *s) { const char *args[AST_MAX_ARGS + 1]; - struct ast_cli_entry *e; + struct ast_cli_entry *e = NULL; int x; char *duplicate = parse_args(s, &x, args + 1, AST_MAX_ARGS, NULL); char tmp[AST_MAX_ARGS + 1]; @@ -2702,6 +2732,11 @@ int ast_cli_command_full(int uid, int gid, int fd, const char *s) goto done; } + if (ast_shutting_down() && !allowed_on_shutdown(e)) { + ast_cli(fd, "Command '%s' cannot be run during shutdown\n", s); + goto done; + } + ast_join(tmp, sizeof(tmp), args + 1); /* Check if the user has rights to run this command. */ if (!cli_has_permissions(uid, gid, tmp)) { @@ -2724,8 +2759,11 @@ int ast_cli_command_full(int uid, int gid, int fd, const char *s) } else if (retval == CLI_FAILURE) { ast_cli(fd, "Command '%s' failed.\n", s); } - ast_atomic_fetchadd_int(&e->inuse, -1); + done: + if (e) { + ast_atomic_fetchadd_int(&e->inuse, -1); + } ast_free(duplicate); return retval == CLI_SUCCESS ? RESULT_SUCCESS : RESULT_FAILURE; } @@ -2751,3 +2789,14 @@ void ast_cli_print_timestr_fromseconds(int fd, int seconds, const char *prefix) { print_uptimestr(fd, ast_tv(seconds, 0), prefix, 0); } + +int ast_cli_allow_at_shutdown(struct ast_cli_entry *e) +{ + int res; + + ast_rwlock_wrlock(&shutdown_commands_lock); + res = AST_VECTOR_APPEND(&shutdown_commands, e); + ast_rwlock_unlock(&shutdown_commands_lock); + + return res; +} diff --git a/main/config_options.c b/main/config_options.c index f8c7b0c67..e59e5cf7a 100644 --- a/main/config_options.c +++ b/main/config_options.c @@ -1346,7 +1346,7 @@ static int int_handler_fn(const struct aco_option *opt, struct ast_variable *var */ static int uint_handler_fn(const struct aco_option *opt, struct ast_variable *var, void *obj) { unsigned int *field = (unsigned int *)(obj + opt->args[0]); - unsigned int flags = PARSE_INT32 | opt->flags; + unsigned int flags = PARSE_UINT32 | opt->flags; int res = 0; if (opt->flags & PARSE_IN_RANGE) { res = opt->flags & PARSE_DEFAULT ? diff --git a/main/file.c b/main/file.c index f0f826a4f..654937a58 100644 --- a/main/file.c +++ b/main/file.c @@ -54,6 +54,7 @@ ASTERISK_REGISTER_FILE() #include "asterisk/stasis.h" #include "asterisk/json.h" #include "asterisk/stasis_system.h" +#include "asterisk/media_cache.h" /*! \brief * The following variable controls the layout of localized sound files. @@ -644,6 +645,10 @@ static int fileexists_test(const char *filename, const char *fmt, const char *la return 0; } + if (!ast_media_cache_retrieve(filename, NULL, buf, buflen)) { + return filehelper(buf, result_cap, NULL, ACTION_EXISTS); + } + if (ast_language_is_prefix && !is_absolute_path(filename)) { /* new layout */ if (lang) { snprintf(buf, buflen, "%s/%s", lang, filename); diff --git a/main/logger.c b/main/logger.c index 13f6de890..42a1c7000 100644 --- a/main/logger.c +++ b/main/logger.c @@ -1514,7 +1514,8 @@ static void logger_print_normal(struct logmsg *logmsg) continue; } - syslog_level = LOG_MAKEPRI(chan->facility, syslog_level); + /* Don't use LOG_MAKEPRI because it's broken in glibc<2.17 */ + syslog_level = chan->facility | syslog_level; /* LOG_MAKEPRI(chan->facility, syslog_level); */ if (!chan->formatter.format_log(chan, logmsg, buf, BUFSIZ)) { syslog(syslog_level, "%s", buf); } diff --git a/main/manager.c b/main/manager.c index 2adcb3e5a..e74b253ff 100644 --- a/main/manager.c +++ b/main/manager.c @@ -6138,6 +6138,14 @@ static int process_message(struct mansession *s, const struct message *m) return 0; } + if (ast_shutting_down()) { + ast_log(LOG_ERROR, "Unable to process manager action '%s'. Asterisk is shutting down.\n", action); + mansession_lock(s); + astman_send_error(s, m, "Asterisk is shutting down"); + mansession_unlock(s); + return 0; + } + if (!s->session->authenticated && strcasecmp(action, "Login") && strcasecmp(action, "Logoff") diff --git a/main/media_cache.c b/main/media_cache.c index 1f81e3ae1..958a05bb2 100644 --- a/main/media_cache.c +++ b/main/media_cache.c @@ -189,23 +189,31 @@ static void media_cache_item_del_from_astdb(struct ast_bucket_file *bucket_file) static void bucket_file_update_path(struct ast_bucket_file *bucket_file, const char *preferred_file_name) { - if (ast_strlen_zero(preferred_file_name)) { - return; - } + char *ext; - if (!strcmp(bucket_file->path, preferred_file_name)) { - return; - } + if (!ast_strlen_zero(preferred_file_name) && strcmp(bucket_file->path, preferred_file_name)) { + /* Use the preferred file name if available */ + + rename(bucket_file->path, preferred_file_name); + ast_copy_string(bucket_file->path, preferred_file_name, + sizeof(bucket_file->path)); + } else if (!strchr(bucket_file->path, '.') && (ext = strrchr(ast_sorcery_object_get_id(bucket_file), '.'))) { + /* If we don't have a file extension and were provided one in the URI, use it */ + char new_path[PATH_MAX]; + + ast_bucket_file_metadata_set(bucket_file, "ext", ext); - rename(bucket_file->path, preferred_file_name); - ast_copy_string(bucket_file->path, preferred_file_name, - sizeof(bucket_file->path)); + snprintf(new_path, sizeof(new_path), "%s%s", bucket_file->path, ext); + rename(bucket_file->path, new_path); + ast_copy_string(bucket_file->path, new_path, sizeof(bucket_file->path)); + } } int ast_media_cache_retrieve(const char *uri, const char *preferred_file_name, char *file_path, size_t len) { struct ast_bucket_file *bucket_file; + char *ext; SCOPED_AO2LOCK(media_lock, media_cache); if (ast_strlen_zero(uri)) { @@ -220,11 +228,18 @@ int ast_media_cache_retrieve(const char *uri, const char *preferred_file_name, if (bucket_file) { if (!ast_bucket_file_is_stale(bucket_file)) { ast_copy_string(file_path, bucket_file->path, len); + if ((ext = strrchr(file_path, '.'))) { + *ext = '\0'; + } ao2_ref(bucket_file, -1); + + ast_debug(5, "Returning media at local file: %s\n", file_path); return 0; } - /* Stale! Drop the ref, as we're going to retrieve it next. */ + /* Stale! Remove the item completely, as we're going to replace it next */ + ao2_unlink_flags(media_cache, bucket_file, OBJ_NOLOCK); + ast_bucket_file_delete(bucket_file); ao2_ref(bucket_file, -1); } @@ -233,7 +248,7 @@ int ast_media_cache_retrieve(const char *uri, const char *preferred_file_name, */ bucket_file = ast_bucket_file_retrieve(uri); if (!bucket_file) { - ast_log(LOG_WARNING, "Failed to obtain media at '%s'\n", uri); + ast_debug(2, "Failed to obtain media at '%s'\n", uri); return -1; } @@ -243,9 +258,14 @@ int ast_media_cache_retrieve(const char *uri, const char *preferred_file_name, bucket_file_update_path(bucket_file, preferred_file_name); media_cache_item_sync_to_astdb(bucket_file); ast_copy_string(file_path, bucket_file->path, len); + if ((ext = strrchr(file_path, '.'))) { + *ext = '\0'; + } ao2_link_flags(media_cache, bucket_file, OBJ_NOLOCK); ao2_ref(bucket_file, -1); + ast_debug(5, "Returning media at local file: %s\n", file_path); + return 0; } @@ -692,7 +712,7 @@ int ast_media_cache_init(void) { ast_register_atexit(media_cache_shutdown); - media_cache = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_RWLOCK, AO2_BUCKETS, + media_cache = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_MUTEX, AO2_BUCKETS, media_cache_hash, media_cache_cmp); if (!media_cache) { return -1; diff --git a/main/utils.c b/main/utils.c index 8a9f91062..6a778b90c 100644 --- a/main/utils.c +++ b/main/utils.c @@ -1153,6 +1153,7 @@ static char *handle_show_locks(struct ast_cli_entry *e, int cmd, struct ast_cli_ "Usage: core show locks\n" " This command is for lock debugging. It prints out which locks\n" "are owned by each active thread.\n"; + ast_cli_allow_on_shutdown(e); return NULL; case CLI_GENERATE: diff --git a/res/res_curl.c b/res/res_curl.c index eeacbd298..0a781f190 100644 --- a/res/res_curl.c +++ b/res/res_curl.c @@ -51,6 +51,7 @@ ASTERISK_REGISTER_FILE() static const char *dependents[] = { "func_curl.so", "res_config_curl.so", + "res_http_media_cache.so", }; static int unload_module(void) diff --git a/res/res_http_media_cache.c b/res/res_http_media_cache.c new file mode 100644 index 000000000..2207b96dd --- /dev/null +++ b/res/res_http_media_cache.c @@ -0,0 +1,447 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Matt Jordan + * + * Matt Jordan <mjordan@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 + * + * \author \verbatim Matt Jordan <mjordan@digium.com> \endverbatim + * + * HTTP backend for the core media cache + */ + +/*** MODULEINFO + <depend>curl</depend> + <depend>res_curl</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +ASTERISK_REGISTER_FILE() + +#include <curl/curl.h> + +#include "asterisk/module.h" +#include "asterisk/bucket.h" +#include "asterisk/sorcery.h" +#include "asterisk/threadstorage.h" + +#define GLOBAL_USERAGENT "asterisk-libcurl-agent/1.0" + +#define MAX_HEADER_LENGTH 1023 + +/*! \brief Data passed to cURL callbacks */ +struct curl_bucket_file_data { + /*! The \c ast_bucket_file object that caused the operation */ + struct ast_bucket_file *bucket_file; + /*! File to write data to */ + FILE *out_file; +}; + +/*! + * \internal \brief The cURL header callback function + */ +static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data) +{ + struct curl_bucket_file_data *cb_data = data; + size_t realsize; + char *value; + char *header; + + realsize = size * nitems; + + if (realsize > MAX_HEADER_LENGTH) { + ast_log(LOG_WARNING, "cURL header length of '%zu' is too large: max %d\n", + realsize, MAX_HEADER_LENGTH); + return 0; + } + + /* buffer may not be NULL terminated */ + header = ast_alloca(realsize + 1); + memcpy(header, buffer, realsize); + header[realsize] = '\0'; + value = strchr(header, ':'); + if (!value) { + /* Not a header we care about; bail */ + return realsize; + } + *value++ = '\0'; + + if (strcasecmp(header, "ETag") + && strcasecmp(header, "Cache-Control") + && strcasecmp(header, "Last-Modified") + && strcasecmp(header, "Expires")) { + return realsize; + } + + value = ast_trim_blanks(ast_skip_blanks(value)); + header = ast_str_to_lower(header); + + ast_bucket_file_metadata_set(cb_data->bucket_file, header, value); + + return realsize; +} + +/*! + * \internal \brief The cURL body callback function + */ +static size_t curl_body_callback(void *ptr, size_t size, size_t nitems, void *data) +{ + struct curl_bucket_file_data *cb_data = data; + size_t realsize; + + realsize = fwrite(ptr, size, nitems, cb_data->out_file); + + return realsize; +} + +/*! + * \internal \brief Set the expiration metadata on the bucket file based on HTTP caching rules + */ +static void bucket_file_set_expiration(struct ast_bucket_file *bucket_file) +{ + struct ast_bucket_metadata *metadata; + char time_buf[32]; + struct timeval actual_expires = ast_tvnow(); + + metadata = ast_bucket_file_metadata_get(bucket_file, "cache-control"); + if (metadata) { + char *str_max_age; + + str_max_age = strstr(metadata->value, "s-maxage"); + if (!str_max_age) { + str_max_age = strstr(metadata->value, "max-age"); + } + + if (str_max_age) { + unsigned int max_age; + char *equal = strchr(str_max_age, '='); + if (equal && (sscanf(equal + 1, "%30u", &max_age) == 1)) { + actual_expires.tv_sec += max_age; + } + } + ao2_ref(metadata, -1); + } else { + metadata = ast_bucket_file_metadata_get(bucket_file, "expires"); + if (metadata) { + struct tm expires_time; + + strptime(metadata->value, "%a, %d %b %Y %T %z", &expires_time); + expires_time.tm_isdst = -1; + actual_expires.tv_sec = mktime(&expires_time); + + ao2_ref(metadata, -1); + } + } + + /* Use 'now' if we didn't get an expiration time */ + snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec); + + ast_bucket_file_metadata_set(bucket_file, "__actual_expires", time_buf); +} + +/*! \internal + * \brief Return whether or not we should always revalidate against the server + */ +static int bucket_file_always_revalidate(struct ast_bucket_file *bucket_file) +{ + RAII_VAR(struct ast_bucket_metadata *, metadata, + ast_bucket_file_metadata_get(bucket_file, "cache-control"), + ao2_cleanup); + + if (!metadata) { + return 0; + } + + if (strstr(metadata->value, "no-cache") + || strstr(metadata->value, "must-revalidate")) { + return 1; + } + + return 0; +} + +/*! \internal + * \brief Return whether or not the item has expired + */ +static int bucket_file_expired(struct ast_bucket_file *bucket_file) +{ + RAII_VAR(struct ast_bucket_metadata *, metadata, + ast_bucket_file_metadata_get(bucket_file, "__actual_expires"), + ao2_cleanup); + struct timeval current_time = ast_tvnow(); + struct timeval expires = { .tv_sec = 0, .tv_usec = 0 }; + + if (!metadata) { + return 1; + } + + if (sscanf(metadata->value, "%lu", &expires.tv_sec) != 1) { + return 1; + } + + return ast_tvcmp(current_time, expires) == -1 ? 0 : 1; +} + +/*! + * \internal \brief Obtain a CURL handle with common setup options + */ +static CURL *get_curl_instance(struct curl_bucket_file_data *cb_data) +{ + CURL *curl; + + curl = curl_easy_init(); + if (!curl) { + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); + curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_URL, ast_sorcery_object_get_id(cb_data->bucket_file)); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, cb_data); + + return curl; +} + +/*! + * \brief Execute the CURL + */ +static long execute_curl_instance(CURL *curl) +{ + char curl_errbuf[CURL_ERROR_SIZE + 1]; + long http_code; + + curl_errbuf[CURL_ERROR_SIZE] = '\0'; + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); + + if (curl_easy_perform(curl)) { + ast_log(LOG_WARNING, "%s\n", curl_errbuf); + return -1; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + + return http_code; +} + +/*! + * \internal \brief CURL the URI specified by the bucket_file and store it in the provided path + */ +static int bucket_file_run_curl(struct ast_bucket_file *bucket_file) +{ + struct curl_bucket_file_data cb_data = { + .bucket_file = bucket_file, + }; + long http_code; + CURL *curl; + + cb_data.out_file = fopen(bucket_file->path, "wb"); + if (!cb_data.out_file) { + ast_log(LOG_WARNING, "Failed to open file '%s' for writing: %s (%d)\n", + bucket_file->path, strerror(errno), errno); + return -1; + } + + curl = get_curl_instance(&cb_data); + if (!curl) { + fclose(cb_data.out_file); + return -1; + } + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_body_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&cb_data); + + http_code = execute_curl_instance(curl); + + fclose(cb_data.out_file); + + if (http_code / 100 == 2) { + bucket_file_set_expiration(bucket_file); + return 0; + } else { + ast_log(LOG_WARNING, "Failed to retrieve URL '%s': server returned %ld\n", + ast_sorcery_object_get_id(bucket_file), http_code); + } + + return -1; +} + +static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object) +{ + struct ast_bucket_file *bucket_file = object; + struct ast_bucket_metadata *metadata; + struct curl_slist *header_list = NULL; + long http_code; + CURL *curl; + struct curl_bucket_file_data cb_data = { + .bucket_file = bucket_file + }; + char etag_buf[256]; + + if (!bucket_file_expired(bucket_file) && !bucket_file_always_revalidate(bucket_file)) { + return 0; + } + + /* See if we have an ETag for this item. If not, it's stale. */ + metadata = ast_bucket_file_metadata_get(bucket_file, "etag"); + if (!metadata) { + return 1; + } + + curl = get_curl_instance(&cb_data); + + /* Set the ETag header on our outgoing request */ + snprintf(etag_buf, sizeof(etag_buf), "If-None-Match: %s", metadata->value); + header_list = curl_slist_append(header_list, etag_buf); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + ao2_ref(metadata, -1); + + http_code = execute_curl_instance(curl); + + curl_slist_free_all(header_list); + + if (http_code == 304) { + bucket_file_set_expiration(bucket_file); + return 0; + } + + return 1; +} + +static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data, + void *object) +{ + struct ast_bucket_file *bucket_file = object; + + return bucket_file_run_curl(bucket_file); +} + +static void *bucket_http_wizard_retrieve_id(const struct ast_sorcery *sorcery, + void *data, const char *type, const char *id) +{ + struct ast_bucket_file *bucket_file; + + if (strcmp(type, "file")) { + ast_log(LOG_WARNING, "Failed to create storage: invalid bucket type '%s'\n", type); + return NULL; + } + + if (ast_strlen_zero(id)) { + ast_log(LOG_WARNING, "Failed to create storage: no URI\n"); + return NULL; + } + + bucket_file = ast_bucket_file_alloc(id); + if (!bucket_file) { + ast_log(LOG_WARNING, "Failed to create storage for '%s'\n", id); + return NULL; + } + + if (ast_bucket_file_temporary_create(bucket_file)) { + ast_log(LOG_WARNING, "Failed to create temporary storage for '%s'\n", id); + ast_sorcery_delete(sorcery, bucket_file); + ao2_ref(bucket_file, -1); + return NULL; + } + + if (bucket_file_run_curl(bucket_file)) { + ast_sorcery_delete(sorcery, bucket_file); + ao2_ref(bucket_file, -1); + return NULL; + } + + return bucket_file; +} + +static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data, + void *object) +{ + struct ast_bucket_file *bucket_file = object; + + unlink(bucket_file->path); + + return 0; +} + +static struct ast_sorcery_wizard http_bucket_wizard = { + .name = "http", + .create = bucket_http_wizard_create, + .retrieve_id = bucket_http_wizard_retrieve_id, + .delete = bucket_http_wizard_delete, + .is_stale = bucket_http_wizard_is_stale, +}; + +static struct ast_sorcery_wizard http_bucket_file_wizard = { + .name = "http", + .create = bucket_http_wizard_create, + .retrieve_id = bucket_http_wizard_retrieve_id, + .delete = bucket_http_wizard_delete, + .is_stale = bucket_http_wizard_is_stale, +}; + +static struct ast_sorcery_wizard https_bucket_wizard = { + .name = "https", + .create = bucket_http_wizard_create, + .retrieve_id = bucket_http_wizard_retrieve_id, + .delete = bucket_http_wizard_delete, + .is_stale = bucket_http_wizard_is_stale, +}; + +static struct ast_sorcery_wizard https_bucket_file_wizard = { + .name = "https", + .create = bucket_http_wizard_create, + .retrieve_id = bucket_http_wizard_retrieve_id, + .delete = bucket_http_wizard_delete, + .is_stale = bucket_http_wizard_is_stale, +}; + +static int unload_module(void) +{ + return 0; +} + +static int load_module(void) +{ + if (ast_bucket_scheme_register("http", &http_bucket_wizard, &http_bucket_file_wizard, + NULL, NULL)) { + ast_log(LOG_ERROR, "Failed to register Bucket HTTP wizard scheme implementation\n"); + return AST_MODULE_LOAD_FAILURE; + } + + if (ast_bucket_scheme_register("https", &https_bucket_wizard, &https_bucket_file_wizard, + NULL, NULL)) { + ast_log(LOG_ERROR, "Failed to register Bucket HTTPS wizard scheme implementation\n"); + return AST_MODULE_LOAD_FAILURE; + } + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "HTTP Media Cache Backend", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_DEFAULT, + ); diff --git a/res/res_musiconhold.c b/res/res_musiconhold.c index 4e5056358..f124d58f2 100644 --- a/res/res_musiconhold.c +++ b/res/res_musiconhold.c @@ -1379,6 +1379,18 @@ static struct mohclass *_moh_class_malloc(const char *file, int line, const char return class; } +static struct ast_variable *load_realtime_musiconhold(const char *name) +{ + struct ast_variable *var = ast_load_realtime("musiconhold", "name", name, SENTINEL); + if (!var) { + ast_log(LOG_WARNING, + "Music on Hold class '%s' not found in memory/database. " + "Verify your configuration.\n", + name); + } + return var; +} + static int local_ast_moh_start(struct ast_channel *chan, const char *mclass, const char *interpclass) { struct mohclass *mohclass = NULL; @@ -1387,6 +1399,7 @@ static int local_ast_moh_start(struct ast_channel *chan, const char *mclass, con int res = 0; int i; int realtime_possible = ast_check_realtime("musiconhold"); + int warn_if_not_in_memory = !realtime_possible; const char *classes[] = {NULL, NULL, interpclass, "default"}; if (ast_test_flag(global_flags, MOH_PREFERCHANNELCLASS)) { @@ -1414,9 +1427,9 @@ static int local_ast_moh_start(struct ast_channel *chan, const char *mclass, con for (i = 0; i < ARRAY_LEN(classes); ++i) { if (!ast_strlen_zero(classes[i])) { - mohclass = get_mohbyname(classes[i], 1, 0); + mohclass = get_mohbyname(classes[i], warn_if_not_in_memory, 0); if (!mohclass && realtime_possible) { - var = ast_load_realtime("musiconhold", "name", classes[i], SENTINEL); + var = load_realtime_musiconhold(classes[i]); } if (mohclass || var) { break; diff --git a/res/res_pjsip/pjsip_options.c b/res/res_pjsip/pjsip_options.c index aed962030..4cce55836 100644 --- a/res/res_pjsip/pjsip_options.c +++ b/res/res_pjsip/pjsip_options.c @@ -1027,14 +1027,14 @@ int ast_sip_initialize_sorcery_qualify(void) snprintf(status_value_unknown, sizeof(status_value_unknown), "%u", UNKNOWN); ast_sorcery_object_field_register_nodoc(sorcery, CONTACT_STATUS, "last_status", - status_value_unknown, OPT_UINT_T, 1, FLDSET(struct ast_sip_contact_status, last_status)); + status_value_unknown, OPT_UINT_T, 0, FLDSET(struct ast_sip_contact_status, last_status)); snprintf(status_value_created, sizeof(status_value_created), "%u", CREATED); ast_sorcery_object_field_register_nodoc(sorcery, CONTACT_STATUS, "status", - status_value_created, OPT_UINT_T, 1, FLDSET(struct ast_sip_contact_status, status)); + status_value_created, OPT_UINT_T, 0, FLDSET(struct ast_sip_contact_status, status)); ast_sorcery_object_field_register_custom_nodoc(sorcery, CONTACT_STATUS, "rtt_start", "0.0", rtt_start_handler, rtt_start_to_str, NULL, 0, 0); ast_sorcery_object_field_register_nodoc(sorcery, CONTACT_STATUS, "rtt", - "0", OPT_UINT_T, 1, FLDSET(struct ast_sip_contact_status, rtt)); + "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_contact_status, rtt)); return 0; } diff --git a/tests/test_http_media_cache.c b/tests/test_http_media_cache.c new file mode 100644 index 000000000..c08604f1e --- /dev/null +++ b/tests/test_http_media_cache.c @@ -0,0 +1,700 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Matt Jordan + * + * Matt Jordan <mjordan@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 Tests for the HTTP media cache backend + * + * \author \verbatim Matt Jordan <mjordan@digium.com> \endverbatim + * + * \ingroup tests + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>curl</depend> + <depend>res_http_media_cache</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +ASTERISK_REGISTER_FILE() + +#include <fcntl.h> + +#include "asterisk/module.h" +#include "asterisk/http.h" +#include "asterisk/bucket.h" +#include "asterisk/test.h" + +#define CATEGORY "/res/http_media_cache/" + +#define TEST_URI "test_media_cache" + +struct test_options { + int status_code; + int send_file; + struct { + int s_maxage; + int maxage; + int no_cache; + int must_revalidate; + } cache_control; + struct timeval expires; + const char *status_text; + const char *etag; +}; + +static struct test_options options; + +static char server_uri[512]; + +#define VALIDATE_EXPIRES(test, bucket_file, expected, delta) do { \ + RAII_VAR(struct ast_bucket_metadata *, metadata, ast_bucket_file_metadata_get((bucket_file), "__actual_expires"), ao2_cleanup); \ + int actual_expires; \ + ast_test_validate(test, metadata != NULL); \ + ast_test_validate(test, sscanf(metadata->value, "%d", &actual_expires) == 1); \ + ast_test_validate(test, (((expected) + (delta) > actual_expires) && ((expected) - (delta) < actual_expires))); \ +} while (0) + +#define VALIDATE_STR_METADATA(test, bucket_file, key, expected) do { \ + RAII_VAR(struct ast_bucket_metadata *, metadata, ast_bucket_file_metadata_get((bucket_file), (key)), ao2_cleanup); \ + ast_test_validate(test, metadata != NULL); \ + ast_test_validate(test, !strcmp(metadata->value, (expected))); \ +} while (0) + +#define SET_OR_APPEND_CACHE_CONTROL(str) do { \ + if (!ast_str_strlen((str))) { \ + ast_str_set(&(str), 0, "%s", "cache-control: "); \ + } else { \ + ast_str_append(&(str), 0, "%s", ", "); \ + } \ +} while (0) + +static int http_callback(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_params, struct ast_variable *headers) +{ + char file_name[64] = "/tmp/test-media-cache-XXXXXX"; + struct ast_str *http_header = ast_str_create(128); + struct ast_str *cache_control = ast_str_create(128); + int fd = -1; + int unmodified = 0; + int send_file = options.send_file && method == AST_HTTP_GET; + + if (!http_header) { + goto error; + } + + if (send_file) { + char buf[1024]; + + fd = mkstemp(file_name); + if (fd == -1) { + ast_log(LOG_ERROR, "Unable to open temp file for testing: %s (%d)", strerror(errno), errno); + goto error; + } + + memset(buf, 1, sizeof(buf)); + if (write(fd, buf, sizeof(buf)) != sizeof(buf)) { + ast_log(LOG_ERROR, "Failed to write expected number of bytes to pipe\n"); + close(fd); + goto error; + } + close(fd); + + fd = open(file_name, 0); + if (fd == -1) { + ast_log(LOG_ERROR, "Unable to open temp file for testing: %s (%d)", strerror(errno), errno); + goto error; + } + } + + if (options.cache_control.maxage) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "max-age=%d", options.cache_control.maxage); + } + + if (options.cache_control.s_maxage) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "s-maxage=%d", options.cache_control.s_maxage); + } + + if (options.cache_control.no_cache) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "%s", "no-cache"); + } + + if (options.cache_control.must_revalidate) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "%s", "must-revalidate"); + } + + if (ast_str_strlen(cache_control)) { + ast_str_append(&http_header, 0, "%s\r\n", ast_str_buffer(cache_control)); + } + + if (options.expires.tv_sec) { + struct ast_tm now_time; + char tmbuf[64]; + + ast_localtime(&options.expires, &now_time, NULL); + ast_strftime(tmbuf, sizeof(tmbuf), "%a, %d %b %Y %T %z", &now_time); + ast_str_append(&http_header, 0, "Expires: %s\r\n", tmbuf); + } + + if (!ast_strlen_zero(options.etag)) { + struct ast_variable *v; + + ast_str_append(&http_header, 0, "ETag: %s\r\n", options.etag); + for (v = headers; v; v = v->next) { + if (!strcasecmp(v->name, "If-None-Match") && !strcasecmp(v->value, options.etag)) { + unmodified = 1; + break; + } + } + } + + if (!unmodified) { + ast_http_send(ser, method, options.status_code, options.status_text, http_header, NULL, send_file ? fd : 0, 1); + } else { + ast_http_send(ser, method, 304, "Not Modified", http_header, NULL, 0, 1); + } + + if (send_file) { + close(fd); + unlink(file_name); + } + + ast_free(cache_control); + + return 0; + +error: + ast_free(http_header); + ast_free(cache_control); + ast_http_request_close_on_completion(ser); + ast_http_error(ser, 418, "I'm a Teapot", "Please don't ask me to brew coffee."); + + return 0; +} + +static struct ast_http_uri test_uri = { + .description = "HTTP Media Cache Test URI", + .uri = TEST_URI, + .callback = http_callback, + .has_subtree = 1, + .data = NULL, + .key = __FILE__, +}; + +static int pre_test_cb(struct ast_test_info *info, struct ast_test *test) +{ + memset(&options, 0, sizeof(options)); + + return 0; +} + +static void bucket_file_cleanup(void *obj) +{ + struct ast_bucket_file *bucket_file = obj; + + if (bucket_file) { + ast_bucket_file_delete(bucket_file); + ao2_ref(bucket_file, -1); + } +} + +AST_TEST_DEFINE(retrieve_cache_control_directives) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of a resource with Cache-Control directives that affect staleness"; + info->description = + "This test covers retrieval of a resource with the Cache-Control header,\n" + "which specifies no-cache and/or must-revalidate."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + ast_test_status_update(test, "Testing no-cache...\n"); + options.cache_control.no_cache = 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing no-cache with ETag...\n"); + options.cache_control.no_cache = 1; + options.etag = "123456789"; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + options.etag = NULL; + + ast_test_status_update(test, "Testing no-cache with max-age...\n"); + options.cache_control.no_cache = 1; + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + options.cache_control.maxage = 0; + options.cache_control.no_cache = 0; + + ast_test_status_update(test, "Testing must-revalidate...\n"); + options.cache_control.must_revalidate = 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing must-revalidate with ETag...\n"); + options.cache_control.must_revalidate = 1; + options.etag = "123456789"; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + options.etag = NULL; + + ast_test_status_update(test, "Testing must-revalidate with max-age...\n"); + options.cache_control.must_revalidate = 1; + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_cache_control_age) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of a resource with age specifiers in Cache-Control"; + info->description = + "This test covers retrieval of a resource with the Cache-Control header,\n" + "which specifies max-age and/or s-maxage. The test verifies proper precedence\n" + "ordering of the header attributes, along with its relation if the Expires\n" + "header is present."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + ast_test_status_update(test, "Testing max-age...\n"); + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age and s-maxage...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 600; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 600, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 0; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age, s-maxage, and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 600; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 600, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_etag_expired) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of an expired resource with an ETag"; + info->description = + "This test covers a staleness check of a resource with an ETag\n" + "that has also expired. It guarantees that even if a resource\n" + "is expired, we will still not consider it stale if the resource\n" + "has not changed per the ETag value."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.etag = "123456789"; + options.expires.tv_sec = now.tv_sec - 1; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_STR_METADATA(test, bucket_file, "etag", options.etag); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec - 1, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_expires) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval with explicit expiration"; + info->description = + "This test covers retrieving a resource that has an Expires.\n" + "After retrieval of the resource, staleness is checked. With\n" + "a non-expired resource, we expect the resource to not be stale.\n" + "When the expiration has occurred, we expect the staleness check\n" + "to fail."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.expires.tv_sec = now.tv_sec + 3000; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 3000, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + /* Clean up previous result */ + bucket_file_cleanup(bucket_file); + + options.expires.tv_sec = now.tv_sec - 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec - 1, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_etag) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval with an ETag"; + info->description = + "This test covers retrieving a resource that has an ETag.\n" + "After retrieval of the resource, staleness is checked. With\n" + "matching ETags, we expect the resource to not be stale. When\n" + "the ETag does not match, we expect the resource to be stale."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.etag = "123456789"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_STR_METADATA(test, bucket_file, "etag", options.etag); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + options.etag = "99999999"; + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_nominal) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test nominal retrieval"; + info->description = + "Test nominal retrieval of a resource."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(create_nominal) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test nominal creation"; + info->description = + "Test nominal creation of a resource."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_alloc(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_temporary_create(bucket_file) == 0); + ast_test_validate(test, ast_bucket_file_create(bucket_file) == 0); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + return AST_TEST_PASS; +} + + +static int process_config(int reload) +{ + struct ast_config *config; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + const char *bindaddr; + const char *bindport; + const char *prefix; + const char *enabled; + + config = ast_config_load("http.conf", config_flags); + if (!config || config == CONFIG_STATUS_FILEINVALID) { + return -1; + } else if (config == CONFIG_STATUS_FILEUNCHANGED) { + return 0; + } + + enabled = ast_config_option(config, "general", "enabled"); + if (!enabled || ast_false(enabled)) { + ast_config_destroy(config); + return -1; + } + + /* Construct our Server URI */ + bindaddr = ast_config_option(config, "general", "bindaddr"); + if (!bindaddr) { + ast_config_destroy(config); + return -1; + } + + bindport = ast_config_option(config, "general", "bindport"); + if (!bindport) { + bindport = "8088"; + } + + prefix = ast_config_option(config, "general", "prefix"); + + snprintf(server_uri, sizeof(server_uri), "http://%s:%s%s/%s", bindaddr, bindport, S_OR(prefix, ""), TEST_URI); + + ast_config_destroy(config); + + return 0; +} + +static int reload_module(void) +{ + return process_config(1); +} + +static int load_module(void) +{ + if (process_config(0)) { + return AST_MODULE_LOAD_DECLINE; + } + + if (ast_http_uri_link(&test_uri)) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(create_nominal); + + AST_TEST_REGISTER(retrieve_nominal); + AST_TEST_REGISTER(retrieve_etag); + AST_TEST_REGISTER(retrieve_expires); + AST_TEST_REGISTER(retrieve_etag_expired); + AST_TEST_REGISTER(retrieve_cache_control_age); + AST_TEST_REGISTER(retrieve_cache_control_directives); + + ast_test_register_init(CATEGORY, pre_test_cb); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_http_uri_unlink(&test_uri); + + AST_TEST_UNREGISTER(create_nominal); + + AST_TEST_UNREGISTER(retrieve_nominal); + AST_TEST_UNREGISTER(retrieve_etag); + AST_TEST_UNREGISTER(retrieve_expires); + AST_TEST_UNREGISTER(retrieve_etag_expired); + AST_TEST_UNREGISTER(retrieve_cache_control_age); + AST_TEST_UNREGISTER(retrieve_cache_control_directives); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "HTTP Media Cache Backend Tests", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .reload = reload_module, + .unload = unload_module, + .load_pri = AST_MODPRI_DEFAULT, + ); |