diff --git a/src/mod/applications/mod_http_cache/Makefile b/src/mod/applications/mod_http_cache/Makefile
new file mode 100644
index 0000000000..264f030dd4
--- /dev/null
+++ b/src/mod/applications/mod_http_cache/Makefile
@@ -0,0 +1,5 @@
+BASE=../../../..
+WANT_CURL=yes
+
+include $(BASE)/build/modmake.rules
+
diff --git a/src/mod/applications/mod_http_cache/http_cache.conf.xml b/src/mod/applications/mod_http_cache/http_cache.conf.xml
new file mode 100644
index 0000000000..4150d6472e
--- /dev/null
+++ b/src/mod/applications/mod_http_cache/http_cache.conf.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/mod/applications/mod_http_cache/mod_http_cache.c b/src/mod/applications/mod_http_cache/mod_http_cache.c
new file mode 100644
index 0000000000..23cb01a41d
--- /dev/null
+++ b/src/mod/applications/mod_http_cache/mod_http_cache.c
@@ -0,0 +1,1060 @@
+/*
+ * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ * Copyright (C) 2005-2011, Anthony Minessale II
+ *
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
+ *
+ * The Initial Developer of the Original Code is
+ * Anthony Minessale II
+ * Portions created by the Initial Developer are Copyright (C)
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Chris Rienzo
+ *
+ * mod_http_cache.c -- HTTP GET with caching
+ * -- designed for downloading audio files from a webserver for playback
+ *
+ */
+#include
+#include
+
+/* Defines module interface to FreeSWITCH */
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_http_cache_shutdown);
+SWITCH_MODULE_LOAD_FUNCTION(mod_http_cache_load);
+SWITCH_MODULE_DEFINITION(mod_http_cache, mod_http_cache_load, mod_http_cache_shutdown, NULL);
+SWITCH_STANDARD_API(http_cache_get);
+SWITCH_STANDARD_API(http_cache_put);
+SWITCH_STANDARD_API(http_cache_tryget);
+SWITCH_STANDARD_API(http_cache_clear);
+
+#define DOWNLOAD_NEEDED "download"
+
+typedef struct url_cache url_cache_t;
+
+/**
+ * status if the cache entry
+ */
+enum cached_url_status {
+ /** downloading */
+ CACHED_URL_RX_IN_PROGRESS,
+ /** marked for removal */
+ CACHED_URL_REMOVE,
+ /** available */
+ CACHED_URL_AVAILABLE
+};
+typedef enum cached_url_status cached_url_status_t;
+
+/**
+ * Cached URL information
+ */
+struct cached_url {
+ /** The URL that was cached */
+ char *url;
+ /** The path and name of the cached URL */
+ char *filename;
+ /** The size of the cached URL, in bytes */
+ size_t size;
+ /** URL use flag */
+ int used;
+ /** Status of this entry */
+ cached_url_status_t status;
+ /** Number of sessions waiting for this URL */
+ int waiters;
+ /** time when downloaded */
+ switch_time_t download_time;
+ /** nanoseconds until stale */
+ switch_time_t max_age;
+};
+typedef struct cached_url cached_url_t;
+
+static cached_url_t *cached_url_create(url_cache_t *cache, const char *url);
+static void cached_url_destroy(cached_url_t *url, switch_memory_pool_t *pool);
+
+/**
+ * Data for write_file_callback()
+ */
+struct http_get_data {
+ /** File descriptor for the cached URL */
+ int fd;
+ /** The cached URL data */
+ cached_url_t *url;
+};
+typedef struct http_get_data http_get_data_t;
+
+static switch_status_t http_get(cached_url_t *url, switch_core_session_t *session);
+static size_t get_file_callback(void *ptr, size_t size, size_t nmemb, void *get);
+static size_t get_header_callback(void *ptr, size_t size, size_t nmemb, void *url);
+static void process_cache_control_header(cached_url_t *url, char *data);
+
+static switch_status_t http_put(switch_core_session_t *session, const char *url, const char *filename);
+
+/**
+ * Queue used for clock cache replacement algorithm. This
+ * queue has been simplified since replacement only happens
+ * once it is filled.
+ */
+struct simple_queue {
+ /** The queue data */
+ void **data;
+ /** queue bounds */
+ size_t max_size;
+ /** queue size */
+ size_t size;
+ /** Current index */
+ int pos;
+};
+typedef struct simple_queue simple_queue_t;
+
+
+/**
+ * The cache
+ */
+struct url_cache {
+ /** The maximum number of URLs to cache */
+ int max_url;
+ /** The maximum size of this cache, in bytes */
+ size_t max_size;
+ /** The default time to allow a cached URL to live, if none is specified */
+ switch_time_t default_max_age;
+ /** The current size of this cache, in bytes */
+ size_t size;
+ /** The location of the cache in the filesystem */
+ char *location;
+ /** Cache mapped by URL */
+ switch_hash_t *map;
+ /** Cached URLs queued for replacement */
+ simple_queue_t queue;
+ /** Synchronizes access to cache */
+ switch_mutex_t *mutex;
+ /** Memory pool */
+ switch_memory_pool_t *pool;
+ /** Number of cache hits */
+ int hits;
+ /** Number of cache misses */
+ int misses;
+ /** Number of cache errors */
+ int errors;
+};
+static url_cache_t gcache;
+
+static char *url_cache_get(url_cache_t *cache, switch_core_session_t *session, const char *url, int download, switch_memory_pool_t *pool);
+static switch_status_t url_cache_add(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url);
+static void url_cache_remove(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url);
+static void url_cache_remove_soft(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url);
+static switch_status_t url_cache_replace(url_cache_t *cache, switch_core_session_t *session);
+static void url_cache_lock(url_cache_t *cache, switch_core_session_t *session);
+static void url_cache_unlock(url_cache_t *cache, switch_core_session_t *session);
+static void url_cache_clear(url_cache_t *cache, switch_core_session_t *session);
+
+/**
+ * Put a file to the URL
+ * @param session the (optional) session uploading the file
+ * @param url The URL
+ * @param filename The file to upload
+ * @return SWITCH_STATUS_SUCCESS if successful
+ */
+static switch_status_t http_put(switch_core_session_t *session, const char *url, const char *filename)
+{
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ CURL *curl_handle = NULL;
+ long httpRes = 0;
+ struct stat file_info = {0};
+ FILE *file_to_put = NULL;
+ int fd;
+
+ /* open file and get the file size */
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "opening %s for upload to %s\n", filename, url);
+ fd = open(filename, O_RDONLY);
+ if (fd == -1) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "open() error: %s\n", strerror(errno));
+ status = SWITCH_STATUS_FALSE;
+ goto done;
+ }
+ if (fstat(fd, &file_info) == -1) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "fstat() error: %s\n", strerror(errno));
+ }
+ close(fd);
+
+ /* libcurl requires FILE* */
+ file_to_put = fopen(filename, "rb");
+ if (!file_to_put) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "fopen() error: %s\n", strerror(errno));
+ status = SWITCH_STATUS_FALSE;
+ goto done;
+ }
+
+ curl_handle = curl_easy_init();
+ if (!curl_handle) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "curl_easy_init() failure\n");
+ status = SWITCH_STATUS_FALSE;
+ goto done;
+ }
+ curl_easy_setopt(curl_handle, CURLOPT_UPLOAD, 1);
+ curl_easy_setopt(curl_handle, CURLOPT_PUT, 1);
+ curl_easy_setopt(curl_handle, CURLOPT_URL, url);
+ curl_easy_setopt(curl_handle, CURLOPT_READDATA, file_to_put);
+ curl_easy_setopt(curl_handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)file_info.st_size);
+ curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(curl_handle, CURLOPT_MAXREDIRS, 10);
+ curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "freeswitch-http-cache/1.0");
+ curl_easy_perform(curl_handle);
+ curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &httpRes);
+ curl_easy_cleanup(curl_handle);
+
+ if (httpRes == 200 || httpRes == 201 || httpRes == 204) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "%s saved to %s\n", filename, url);
+ } else {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Received HTTP error %ld trying to save %s to %s\n", httpRes, filename, url);
+ status = SWITCH_STATUS_GENERR;
+ }
+
+done:
+ if (file_to_put) {
+ fclose(file_to_put);
+ }
+
+ return status;
+}
+
+/**
+ * Called by libcurl to write result of HTTP GET to a file
+ * @param ptr The data to write
+ * @param size The size of the data element to write
+ * @param nmemb The number of elements to write
+ * @param get Info about this current GET request
+ * @return bytes processed
+ */
+static size_t get_file_callback(void *ptr, size_t size, size_t nmemb, void *get)
+{
+ size_t realsize = (size * nmemb);
+ http_get_data_t *get_data = get;
+ ssize_t bytes_written = write(get_data->fd, ptr, realsize);
+ size_t result = 0;
+ if (bytes_written == -1) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write(): %s\n", strerror(errno));
+ } else {
+ if (bytes_written != realsize) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write(): short write!\n");
+ }
+ get_data->url->size += bytes_written;
+ result = bytes_written;
+ }
+
+ return result;
+}
+/**
+ * trim whitespace characters from string
+ * @param str the string to trim
+ * @return the trimmed string
+ */
+static char *trim(char *str)
+{
+ size_t len;
+
+ if (zstr(str)) {
+ return str;
+ }
+ len = strlen(str);
+
+ /* strip whitespace from front */
+ for (int i = 0; i < len; i++) {
+ if (!isspace(str[i])) {
+ str = &str[i];
+ len -= i;
+ break;
+ }
+ }
+ if (zstr(str)) {
+ return str;
+ }
+
+ /* strip whitespace from end */
+ for (int i = len - 1; i >= 0; i--) {
+ if (!isspace(str[i])) {
+ break;
+ }
+ str[i] = '\0';
+ }
+ return str;
+}
+
+#define MAX_AGE "max-age="
+/**
+ * cache-control: max-age=123456
+ * Only support max-age. All other params are ignored.
+ */
+static void process_cache_control_header(cached_url_t *url, char *data)
+{
+ char *max_age_str;
+ int max_age;
+
+ /* trim whitespace and check if empty */
+ data = trim(data);
+ if (zstr(data)) {
+ return;
+ }
+
+ /* find max-age param */
+ max_age_str = strcasestr(data, MAX_AGE);
+ if (zstr(max_age_str)) {
+ return;
+ }
+
+ /* find max-age value */
+ max_age_str = max_age_str + (sizeof(MAX_AGE) - 1);
+ if (zstr(max_age_str)) {
+ return;
+ }
+ for (int i = 0; i < strlen(max_age_str); i++) {
+ if (!isdigit(max_age_str[i])) {
+ max_age_str[i] = '\0';
+ break;
+ }
+ }
+ if (zstr(max_age_str)) {
+ return;
+ }
+ max_age = atoi(max_age_str);
+
+ if (max_age < 0) {
+ return;
+ }
+
+ url->max_age = switch_time_now() + (1000 * 1000 * max_age);
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setting max age to %u seconds from now\n", max_age);
+}
+
+#define CACHE_CONTROL_HEADER "cache-control:"
+#define CACHE_CONTROL_HEADER_LEN (sizeof(CACHE_CONTROL_HEADER) - 1)
+/**
+ * Called by libcurl to process headers from HTTP GET response
+ * @param ptr the header data
+ * @param size The size of the data element to write
+ * @param nmemb The number of elements to write
+ * @param get get data
+ * @return bytes processed
+ */
+static size_t get_header_callback(void *ptr, size_t size, size_t nmemb, void *get)
+{
+ size_t realsize = (size * nmemb);
+ cached_url_t *url = get;
+ char *header = NULL;
+
+ /* validate length... Apache and IIS won't send a header larger than 16 KB */
+ if (realsize == 0 || realsize > 1024 * 16) {
+ return realsize;
+ }
+
+ /* get the header, adding NULL terminator if there isn't one */
+ switch_zmalloc(header, realsize + 1);
+ strncpy(header, (char *)ptr, realsize);
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s", header);
+
+ /* check which header this is and process it */
+ if (!strncasecmp(CACHE_CONTROL_HEADER, header, CACHE_CONTROL_HEADER_LEN)) {
+ process_cache_control_header(url, header + CACHE_CONTROL_HEADER_LEN);
+ }
+
+ switch_safe_free(header);
+ return realsize;
+}
+
+/**
+ * Get exclusive access to the cache
+ * @param cache The cache
+ * @param session The session acquiring the cache
+ */
+static void url_cache_lock(url_cache_t *cache, switch_core_session_t *session)
+{
+ switch_mutex_lock(cache->mutex);
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Locked cache\n");
+}
+
+/**
+ * Relinquish exclusive access to the cache
+ * @param cache The cache
+ * @param session The session relinquishing the cache
+ */
+static void url_cache_unlock(url_cache_t *cache, switch_core_session_t *session)
+{
+ switch_mutex_unlock(cache->mutex);
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Unlocked cache\n");
+}
+
+/**
+ * Empties the cache
+ */
+static void url_cache_clear(url_cache_t *cache, switch_core_session_t *session)
+{
+ url_cache_lock(cache, session);
+
+ // remove each cached URL from the hash and the queue
+ for (int i = 0; i < cache->queue.max_size; i++) {
+ cached_url_t *url = cache->queue.data[i];
+ if (url) {
+ switch_core_hash_delete(cache->map, url->url);
+ cached_url_destroy(url, cache->pool);
+ cache->queue.data[i] = NULL;
+ }
+ }
+ cache->queue.pos = 0;
+ cache->queue.size = 0;
+
+ // reset cache stats
+ cache->size = 0;
+ cache->hits = 0;
+ cache->misses = 0;
+ cache->errors = 0;
+
+ url_cache_unlock(cache, session);
+
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Emptied cache\n");
+}
+
+/**
+ * Get a URL from the cache, add it if it does not exist
+ * @param cache The cache
+ * @param session the (optional) session requesting the URL
+ * @param url The URL
+ * @param download If true, the file will be downloaded if it does not exist in the cache.
+ * @param pool The pool to use for allocating the filename
+ * @return The filename or NULL if there is an error
+ */
+static char *url_cache_get(url_cache_t *cache, switch_core_session_t *session, const char *url, int download, switch_memory_pool_t *pool)
+{
+ char *filename = NULL;
+ cached_url_t *u = NULL;
+ if (zstr(url)) {
+ return NULL;
+ }
+
+ url_cache_lock(cache, session);
+ u = switch_core_hash_find(cache->map, url);
+
+ /* check if expired */
+ if (u && switch_time_now() >= (u->download_time + u->max_age)) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Cached URL has expired.\n");
+ url_cache_remove_soft(cache, session, u); /* will get permanently deleted upon replacement */
+ u = NULL;
+ }
+
+ /* make sure file hasn't been deleted */
+ if (u && switch_file_exists(u->filename, pool) != SWITCH_STATUS_SUCCESS) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Cached URL file is missing.\n");
+ url_cache_remove_soft(cache, session, u); /* will get permanently deleted upon replacement */
+ u = NULL;
+ }
+
+ if (!u && download) {
+ /* URL is not cached, let's add it.*/
+ /* Set up URL entry and add to map to prevent simultaneous downloads */
+ cache->misses++;
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Cache MISS: size = %ld (%ld MB), hit ratio = %d/%d\n", cache->queue.size, cache->size / 1000000, cache->hits, cache->hits + cache->misses);
+ u = cached_url_create(cache, url);
+ if (url_cache_add(cache, session, u) != SWITCH_STATUS_SUCCESS) {
+ /* This error should never happen */
+ url_cache_unlock(cache, session);
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_CRIT, "Failed to add URL to cache!\n");
+ cached_url_destroy(u, cache->pool);
+ return NULL;
+ }
+
+ /* download the file */
+ url_cache_unlock(cache, session);
+ if (http_get(u, session) == SWITCH_STATUS_SUCCESS) {
+ /* Got the file, let the waiters know it is available */
+ url_cache_lock(cache, session);
+ u->status = CACHED_URL_AVAILABLE;
+ filename = switch_core_strdup(pool, u->filename);
+ cache->size += u->size;
+ } else {
+ /* Did not get the file, flag for replacement */
+ url_cache_lock(cache, session);
+ url_cache_remove_soft(cache, session, u);
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Failed to download URL %s\n", url);
+ cache->errors++;
+ }
+ } else if (!u) {
+ filename = DOWNLOAD_NEEDED;
+ } else {
+ /* Wait until file is downloaded */
+ if (u->status == CACHED_URL_RX_IN_PROGRESS) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Waiting for URL %s to be available\n", url);
+ u->waiters++;
+ url_cache_unlock(cache, session);
+ while(u->status == CACHED_URL_RX_IN_PROGRESS) {
+ switch_sleep(10 * 1000); /* 10 ms */
+ }
+ url_cache_lock(cache, session);
+ u->waiters--;
+ }
+
+ /* grab filename if everything is OK */
+ if (u->status == CACHED_URL_AVAILABLE) {
+ filename = switch_core_strdup(pool, u->filename);
+ cache->hits++;
+ u->used = 1;
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Cache HIT: size = %ld (%ld MB), hit ratio = %d/%d\n", cache->queue.size, cache->size / 1000000, cache->hits, cache->hits + cache->misses);
+ }
+ }
+ url_cache_unlock(cache, session);
+ return filename;
+}
+
+/**
+ * Add a URL to the cache. The caller must lock the cache.
+ * @param cache the cache
+ * @param session the (optional) session
+ * @param url the URL to add
+ * @return SWITCH_STATUS_SUCCESS if successful
+ */
+static switch_status_t url_cache_add(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url)
+{
+ simple_queue_t *queue = &cache->queue;
+ if (queue->size >= queue->max_size && url_cache_replace(cache, session) != SWITCH_STATUS_SUCCESS) {
+ return SWITCH_STATUS_FALSE;
+ }
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Adding %s(%s) to cache index %d\n", url->url, url->filename, queue->pos);
+
+ queue->data[queue->pos] = url;
+ queue->pos = (queue->pos + 1) % queue->max_size;
+ queue->size++;
+ switch_core_hash_insert(cache->map, url->url, url);
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/**
+ * Select a URL for replacement and remove it from the cache.
+ * Currently implemented with the clock replacement algorithm. It's not
+ * great, but is better than least recently used and is simple to implement.
+ *
+ * @param cache the cache
+ * @param session the (optional) session
+ * @return SWITCH_STATUS_SUCCESS if successful
+ */
+static switch_status_t url_cache_replace(url_cache_t *cache, switch_core_session_t *session)
+{
+ switch_status_t status = SWITCH_STATUS_FALSE;
+ int i = 0;
+ simple_queue_t *queue = &cache->queue;
+
+ if (queue->size < queue->max_size || queue->size == 0) {
+ return SWITCH_STATUS_FALSE;
+ }
+
+ for (i = 0; i < queue->max_size * 2; i++) {
+ cached_url_t *to_replace = (cached_url_t *)queue->data[queue->pos];
+
+ /* check for queue corruption */
+ if (to_replace == NULL) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_CRIT, "Unexpected empty URL at cache index %d\n", queue->pos);
+ status = SWITCH_STATUS_SUCCESS;
+ break;
+ }
+
+ /* check if available for replacement */
+ if (!to_replace->used && !to_replace->waiters) {
+ /* remove from cache and destroy it */
+ url_cache_remove(cache, session, to_replace);
+ cached_url_destroy(to_replace, cache->pool);
+ status = SWITCH_STATUS_SUCCESS;
+ break;
+ }
+
+ /* not available for replacement. Mark as not used and move to back of queue */
+ if (to_replace->status == CACHED_URL_AVAILABLE) {
+ to_replace->used = 0;
+ }
+ queue->pos = (queue->pos + 1) % queue->max_size;
+ }
+
+ return status;
+}
+
+/**
+ * Remove a URL from the hash map and mark it as eligible for replacement from the queue.
+ */
+static void url_cache_remove_soft(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url)
+{
+ switch_core_hash_delete(cache->map, url->url);
+ url->used = 0;
+ url->status = CACHED_URL_REMOVE;
+}
+
+/**
+ * Remove a URL from the front or back of the cache
+ * @param cache the cache
+ * @param session the (optional) session
+ * @param url the URL to remove
+ */
+static void url_cache_remove(url_cache_t *cache, switch_core_session_t *session, cached_url_t *url)
+{
+ simple_queue_t *queue;
+ cached_url_t *to_remove;
+
+ url_cache_remove_soft(cache, session, url);
+
+ /* make sure cached URL matches the item in the queue and remove it */
+ queue = &cache->queue;
+ to_remove = (cached_url_t *)queue->data[queue->pos];
+ if (url == to_remove) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Removing %s(%s) from cache index %d\n", url->url, url->filename, queue->pos);
+ queue->data[queue->pos] = NULL;
+ queue->size--;
+ } else {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_CRIT, "URL entry, %s, not in cache queue!!!\n", url->url);
+ }
+
+ /* adjust cache statistics */
+ cache->size -= url->size;
+}
+
+/**
+ * Create a cached URL entry
+ * @param cache the cache
+ * @param url the URL to cache
+ * @return the cached URL
+ */
+static cached_url_t *cached_url_create(url_cache_t *cache, const char *url)
+{
+ switch_uuid_t uuid;
+ char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1] = { 0 };
+ char *filename = NULL;
+ char uuid_dir[3] = { 0 };
+ char *dirname = NULL;
+ cached_url_t *u = NULL;
+ const char *file_extension = "";
+
+ if (zstr(url)) {
+ return NULL;
+ }
+
+ switch_zmalloc(u, sizeof(cached_url_t));
+
+ /* filename is constructed from UUID and is stored in cache dir (first 2 characters of UUID) */
+ switch_uuid_get(&uuid);
+ switch_uuid_format(uuid_str, &uuid);
+ strncpy(uuid_dir, uuid_str, 2);
+ dirname = switch_mprintf("%s%s%s", cache->location, SWITCH_PATH_SEPARATOR, uuid_dir);
+ filename = &uuid_str[2];
+
+ /* create sub-directory if it doesn't exist */
+ switch_dir_make_recursive(dirname, SWITCH_DEFAULT_DIR_PERMS, cache->pool);
+
+ /* find extension on the end of URL */
+ for(const char *ext = &url[strlen(url) - 1]; ext != url; ext--) {
+ if (*ext == '/' || *ext == '\\') {
+ break;
+ }
+ if (*ext == '.') {
+ /* found it */
+ file_extension = ext;
+ break;
+ }
+ }
+
+ /* intialize cached URL */
+ u->filename = switch_mprintf("%s%s%s%s", dirname, SWITCH_PATH_SEPARATOR, filename, file_extension);
+ u->url = switch_safe_strdup(url);
+ u->size = 0;
+ u->used = 1;
+ u->status = CACHED_URL_RX_IN_PROGRESS;
+ u->waiters = 0;
+ u->download_time = switch_time_now();
+ u->max_age = cache->default_max_age;
+
+ switch_safe_free(dirname);
+
+ return u;
+}
+
+/**
+ * Destroy a cached URL (delete file and struct)
+ */
+static void cached_url_destroy(cached_url_t *url, switch_memory_pool_t *pool)
+{
+ if (!zstr(url->filename)) {
+ switch_file_remove(url->filename, pool);
+ }
+ switch_safe_free(url->filename);
+ switch_safe_free(url->url);
+ switch_safe_free(url);
+}
+
+/**
+ * Fetch a file via HTTP
+ * @param url The cached URL entry
+ * @param session the (optional) session
+ * @return SWITCH_STATUS_SUCCESS if successful
+ */
+static switch_status_t http_get(cached_url_t *url, switch_core_session_t *session)
+{
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ CURL *curl_handle = NULL;
+ http_get_data_t get_data = {0};
+ long httpRes = 0;
+ int start_time_ms = switch_time_now() / 1000;
+
+ /* set up HTTP GET */
+ get_data.fd = 0;
+ get_data.url = url;
+
+ curl_handle = curl_easy_init();
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "opening %s for URL cache\n", get_data.url->filename);
+ if ((get_data.fd = open(get_data.url->filename, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR)) > -1) {
+ curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(curl_handle, CURLOPT_MAXREDIRS, 10);
+ curl_easy_setopt(curl_handle, CURLOPT_URL, get_data.url->url);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, get_file_callback);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *) &get_data);
+ curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, get_header_callback);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEHEADER, (void *) url);
+ curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "freeswitch-http-cache/1.0");
+ curl_easy_perform(curl_handle);
+ curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &httpRes);
+ curl_easy_cleanup(curl_handle);
+ close(get_data.fd);
+ } else {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "open() error: %s\n", strerror(errno));
+ return SWITCH_STATUS_GENERR;
+ }
+
+ if (httpRes == 200) {
+ int duration_ms = (switch_time_now() / 1000) - start_time_ms;
+ if (duration_ms > 500) {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_WARNING, "URL %s downloaded in %d ms\n", url->url, duration_ms);
+ } else {
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "URL %s downloaded in %d ms\n", url->url, duration_ms);
+ }
+ } else {
+ url->size = 0; // nothing downloaded or download interrupted
+ switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Received HTTP error %ld trying to fetch %s\n", httpRes, url->url);
+ return SWITCH_STATUS_GENERR;
+ }
+
+ return status;
+}
+
+/**
+ * Empties the entire cache
+ * @param cache the cache to empty
+ */
+static void setup_dir(url_cache_t *cache)
+{
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setting up %s\n", cache->location);
+ switch_dir_make_recursive(cache->location, SWITCH_DEFAULT_DIR_PERMS, cache->pool);
+
+ for (int i = 0x00; i <= 0xff; i++) {
+ switch_dir_t *dir = NULL;
+ char *dirname = switch_mprintf("%s%s%02x", cache->location, SWITCH_PATH_SEPARATOR, i);
+ if (switch_dir_open(&dir, dirname, cache->pool) == SWITCH_STATUS_SUCCESS) {
+ char filenamebuf[256] = { 0 };
+ const char *filename = NULL;
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "deleting cache files in %s...\n", dirname);
+ for(filename = switch_dir_next_file(dir, filenamebuf, sizeof(filenamebuf)); filename;
+ filename = switch_dir_next_file(dir, filenamebuf, sizeof(filenamebuf))) {
+ char *path = switch_mprintf("%s%s%s", dirname, SWITCH_PATH_SEPARATOR, filename);
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "deleting: %s\n", path);
+ switch_file_remove(path, cache->pool);
+ switch_safe_free(path);
+ }
+ switch_dir_close(dir);
+ switch_safe_free(dirname);
+ } else {
+ switch_safe_free(dirname);
+ break;
+ }
+ }
+}
+
+#define HTTP_GET_SYNTAX ""
+/**
+ * Get a file from the cache, download if it isn't cached
+ */
+SWITCH_STANDARD_API(http_cache_get)
+{
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ switch_memory_pool_t *lpool = NULL;
+ switch_memory_pool_t *pool = NULL;
+ char *filename;
+
+ if (zstr(cmd) || strncmp("http://", cmd, strlen("http://"))) {
+ stream->write_function(stream, "USAGE: %s\n", HTTP_GET_SYNTAX);
+ return SWITCH_STATUS_SUCCESS;
+ }
+
+ if (session) {
+ pool = switch_core_session_get_pool(session);
+ } else {
+ switch_core_new_memory_pool(&lpool);
+ pool = lpool;
+ }
+
+ filename = url_cache_get(&gcache, session, cmd, 1, pool);
+ if (filename) {
+ stream->write_function(stream, "%s", filename);
+
+ } else {
+ stream->write_function(stream, "-ERR\n");
+ status = SWITCH_STATUS_FALSE;
+ }
+
+ if (lpool) {
+ switch_core_destroy_memory_pool(&lpool);
+ }
+
+ return status;
+}
+
+#define HTTP_TRYGET_SYNTAX ""
+/**
+ * Get a file from the cache, fail if download is needed
+ */
+SWITCH_STANDARD_API(http_cache_tryget)
+{
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ switch_memory_pool_t *lpool = NULL;
+ switch_memory_pool_t *pool = NULL;
+ char *filename;
+
+ if (zstr(cmd) || strncmp("http://", cmd, strlen("http://"))) {
+ stream->write_function(stream, "USAGE: %s\n", HTTP_GET_SYNTAX);
+ return SWITCH_STATUS_SUCCESS;
+ }
+
+ if (session) {
+ pool = switch_core_session_get_pool(session);
+ } else {
+ switch_core_new_memory_pool(&lpool);
+ pool = lpool;
+ }
+
+ filename = url_cache_get(&gcache, session, cmd, 0, pool);
+ if (filename) {
+ if (!strcmp(DOWNLOAD_NEEDED, filename)) {
+ stream->write_function(stream, "-ERR %s\n", DOWNLOAD_NEEDED);
+ } else {
+ stream->write_function(stream, "%s", filename);
+ }
+ } else {
+ stream->write_function(stream, "-ERR\n");
+ status = SWITCH_STATUS_FALSE;
+ }
+
+ if (lpool) {
+ switch_core_destroy_memory_pool(&lpool);
+ }
+
+ return status;
+}
+
+#define HTTP_PUT_SYNTAX " "
+/**
+ * Put a file to the server
+ */
+SWITCH_STANDARD_API(http_cache_put)
+{
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ char *args = NULL;
+ char *argv[10] = { 0 };
+ int argc = 0;
+ //switch_memory_pool_t *pool = NULL;
+ switch_memory_pool_t *lpool = NULL;
+
+ if (zstr(cmd) || strncmp("http://", cmd, strlen("http://"))) {
+ stream->write_function(stream, "USAGE: %s\n", HTTP_PUT_SYNTAX);
+ status = SWITCH_STATUS_SUCCESS;
+ goto done;
+ }
+
+ args = strdup(cmd);
+ argc = switch_separate_string(args, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
+ if (argc != 2) {
+ stream->write_function(stream, "USAGE: %s\n", HTTP_PUT_SYNTAX);
+ status = SWITCH_STATUS_SUCCESS;
+ goto done;
+ }
+
+ /*
+ if (session) {
+ pool = switch_core_session_get_pool(session);
+ } else {
+ switch_core_new_memory_pool(&lpool);
+ pool = lpool;
+ }
+ */
+
+ status = http_put(session, argv[0], argv[1]);
+ if (status == SWITCH_STATUS_SUCCESS) {
+ stream->write_function(stream, "+OK\n");
+ } else {
+ stream->write_function(stream, "-ERR\n");
+ }
+
+ if (lpool) {
+ switch_core_destroy_memory_pool(&lpool);
+ }
+
+done:
+ switch_safe_free(args);
+ return status;
+}
+
+#define HTTP_CACHE_CLEAR_SYNTAX ""
+/**
+ * Clears the cache
+ */
+SWITCH_STANDARD_API(http_cache_clear)
+{
+ if (!zstr(cmd)) {
+ stream->write_function(stream, "USAGE: %s\n", HTTP_CACHE_CLEAR_SYNTAX);
+ } else {
+ url_cache_clear(&gcache, session);
+ stream->write_function(stream, "+OK\n");
+ }
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/**
+ * Configure the module
+ * @param cache to configure
+ * @return SWITCH_STATUS_SUCCESS if successful
+ */
+static switch_status_t do_config(url_cache_t *cache)
+{
+ char *cf = "http_cache.conf";
+ switch_xml_t cfg, xml, param, settings;
+ switch_status_t status = SWITCH_STATUS_SUCCESS;
+ int max_urls;
+ int default_max_age_sec;
+
+ if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", cf);
+ return SWITCH_STATUS_TERM;
+ }
+
+ /* set default config */
+ max_urls = 4000;
+ default_max_age_sec = 86400;
+ cache->location = SWITCH_PREFIX_DIR "/http_cache";
+
+ /* get params */
+ settings = switch_xml_child(cfg, "settings");
+ if (settings) {
+ for (param = switch_xml_child(settings, "param"); param; param = param->next) {
+ char *var = (char *) switch_xml_attr_soft(param, "name");
+ char *val = (char *) switch_xml_attr_soft(param, "value");
+ if (!strcasecmp(var, "max-urls")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting max-urls to %s\n", val);
+ max_urls = atoi(val);
+ } else if (!strcasecmp(var, "location")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting location to %s\n", val);
+ cache->location = switch_core_strdup(cache->pool, val);
+ } else if (!strcasecmp(var, "default-max-age")) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting default-max-age to %s\n", val);
+ default_max_age_sec = atoi(val);
+ } else {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Unsupported param: %s\n", var);
+ }
+ }
+ }
+
+ /* check config */
+ if (max_urls <= 0) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "max-urls must be > 0\n");
+ status = SWITCH_STATUS_TERM;
+ goto done;
+ }
+ if (zstr(cache->location)) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "location must not be empty\n");
+ status = SWITCH_STATUS_TERM;
+ goto done;
+ }
+ if (default_max_age_sec <= 0) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "default-max-age must be > 0\n");
+ status = SWITCH_STATUS_TERM;
+ goto done;
+ }
+
+ cache->max_url = max_urls;
+ cache->default_max_age = (default_max_age_sec * 1000 * 1000); /* convert from seconds to nanoseconds */
+done:
+ switch_xml_free(xml);
+
+ return status;
+}
+
+/**
+ * Called when FreeSWITCH loads the module
+ */
+SWITCH_MODULE_LOAD_FUNCTION(mod_http_cache_load)
+{
+ switch_api_interface_t *api;
+ *module_interface = switch_loadable_module_create_module_interface(pool, modname);
+
+ SWITCH_ADD_API(api, "http_get", "HTTP GET", http_cache_get, HTTP_GET_SYNTAX);
+ SWITCH_ADD_API(api, "http_tryget", "HTTP GET from cache only", http_cache_tryget, HTTP_GET_SYNTAX);
+ SWITCH_ADD_API(api, "http_put", "HTTP PUT", http_cache_put, HTTP_PUT_SYNTAX);
+ SWITCH_ADD_API(api, "http_clear_cache", "Clear the cache", http_cache_clear, HTTP_CACHE_CLEAR_SYNTAX);
+
+ memset(&gcache, 0, sizeof(url_cache_t));
+ gcache.pool = pool;
+
+ if (do_config(&gcache) != SWITCH_STATUS_SUCCESS) {
+ return SWITCH_STATUS_TERM;
+ }
+
+ switch_core_hash_init(&gcache.map, gcache.pool);
+ switch_mutex_init(&gcache.mutex, SWITCH_MUTEX_UNNESTED, gcache.pool);
+
+ /* create the queue */
+ gcache.queue.max_size = gcache.max_url;
+ gcache.queue.data = switch_core_alloc(gcache.pool, sizeof(void *) * gcache.queue.max_size);
+ gcache.queue.pos = 0;
+ gcache.queue.size = 0;
+
+ setup_dir(&gcache);
+
+ /* init CURL */
+ curl_global_init(CURL_GLOBAL_ALL);
+
+ /* indicate that the module should continue to be loaded */
+ return SWITCH_STATUS_SUCCESS;
+}
+
+/**
+ * Called when FreeSWITCH stops the module
+ */
+SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_http_cache_shutdown)
+{
+ url_cache_clear(&gcache, NULL);
+ switch_core_hash_destroy(&gcache.map);
+ switch_mutex_destroy(gcache.mutex);
+ return SWITCH_STATUS_SUCCESS;
+}
+
+
+/* For Emacs:
+ * Local Variables:
+ * mode:c
+ * indent-tabs-mode:t
+ * tab-width:4
+ * c-basic-offset:4
+ * End:
+ * For VIM:
+ * vim:set softtabstop=4 shiftwidth=4 tabstop=4:
+ */