summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES39
-rw-r--r--apps/app_skel.c2
-rw-r--r--channels/chan_pjsip.c1
-rw-r--r--channels/chan_sip.c2
-rw-r--r--funcs/func_aes.c2
-rw-r--r--funcs/func_curl.c212
-rw-r--r--include/asterisk/cli.h12
-rw-r--r--main/app.c2
-rw-r--r--main/asterisk.c7
-rw-r--r--main/cli.c53
-rw-r--r--main/config_options.c2
-rw-r--r--main/file.c5
-rw-r--r--main/logger.c3
-rw-r--r--main/manager.c8
-rw-r--r--main/media_cache.c44
-rw-r--r--main/utils.c1
-rw-r--r--res/res_curl.c1
-rw-r--r--res/res_http_media_cache.c447
-rw-r--r--res/res_musiconhold.c17
-rw-r--r--res/res_pjsip/pjsip_options.c6
-rw-r--r--tests/test_http_media_cache.c700
21 files changed, 1467 insertions, 99 deletions
diff --git a/CHANGES b/CHANGES
index 057542f18..da516efdc 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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,
+ );