freeswitch/src/mod/applications/mod_http_cache/mod_http_cache.c

1237 lines
38 KiB
C

/*
* FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
* Copyright (C) 2005-2012, Anthony Minessale II <anthm@freeswitch.org>
*
* 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 <anthm@freeswitch.org>
* Portions created by the Initial Developer are Copyright (C)
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Christopher M. Rienzo <chris@rienzo.com>
* Darren Schreiber <d@d-man.org>
*
* Maintainer: Christopher M. Rienzo <chris@rienzo.com>
*
* mod_http_cache.c -- HTTP GET with caching
* -- designed for downloading audio files from a webserver for playback
*
*/
#include <switch.h>
#include <switch_curl.h>
/* 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);
SWITCH_STANDARD_API(http_cache_prefetch);
#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(url_cache_t *cache, 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(url_cache_t *cache, 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;
/** The prefetch queue */
switch_queue_t *prefetch_queue;
/** Max size of prefetch queue */
int prefetch_queue_size;
/** Size of prefetch thread pool */
int prefetch_thread_count;
/** Shutdown flag */
int shutdown;
/** Synchronizes shutdown of cache */
switch_thread_rwlock_t *shutdown_lock;
/** SSL cert filename */
char *ssl_cacert;
/** Verify certificate */
int ssl_verifypeer;
/** Verify that hostname matches certificate */
int ssl_verifyhost;
};
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 cache the cache
* @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(url_cache_t *cache, switch_core_session_t *session, const char *url, const char *filename)
{
switch_status_t status = SWITCH_STATUS_SUCCESS;
switch_curl_slist_t *headers = NULL; /* optional linked-list of HTTP headers */
char *ext; /* file extension, used for MIME type identification */
const char *mime_type = "application/octet-stream";
char *buf;
CURL *curl_handle = NULL;
long httpRes = 0;
struct stat file_info = {0};
FILE *file_to_put = NULL;
int fd;
/* guess what type of mime content this is going to be */
if ((ext = strrchr(filename, '.'))) {
ext++;
if (!(mime_type = switch_core_mime_ext2type(ext))) {
mime_type = "application/octet-stream";
}
}
buf = switch_mprintf("Content-Type: %s", mime_type);
headers = switch_curl_slist_append(headers, buf);
/* 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 = switch_curl_easy_init();
if (!curl_handle) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "switch_curl_easy_init() failure\n");
status = SWITCH_STATUS_FALSE;
goto done;
}
switch_curl_easy_setopt(curl_handle, CURLOPT_UPLOAD, 1);
switch_curl_easy_setopt(curl_handle, CURLOPT_PUT, 1);
switch_curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
switch_curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers);
switch_curl_easy_setopt(curl_handle, CURLOPT_URL, url);
switch_curl_easy_setopt(curl_handle, CURLOPT_READDATA, file_to_put);
switch_curl_easy_setopt(curl_handle, CURLOPT_INFILESIZE_LARGE, (curl_off_t)file_info.st_size);
switch_curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
switch_curl_easy_setopt(curl_handle, CURLOPT_MAXREDIRS, 10);
switch_curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "freeswitch-http-cache/1.0");
if (!cache->ssl_verifypeer) {
switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L);
} else {
/* this is the file with all the trusted certificate authorities */
if (!zstr(cache->ssl_cacert)) {
switch_curl_easy_setopt(curl_handle, CURLOPT_CAINFO, cache->ssl_cacert);
}
/* verify that the host name matches the cert */
if (!cache->ssl_verifyhost) {
switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L);
}
}
switch_curl_easy_perform(curl_handle);
switch_curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &httpRes);
switch_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);
}
if (headers) {
switch_curl_slist_free_all(headers);
}
switch_safe_free(buf);
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;
int i;
if (zstr(str)) {
return str;
}
len = strlen(str);
/* strip whitespace from front */
for (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 (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;
switch_time_t max_age;
int i;
/* 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 (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 = max_age * 1000 * 1000;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setting max age to %u seconds from now\n", (int)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)
{
int i;
url_cache_lock(cache, session);
// remove each cached URL from the hash and the queue
for (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);
if (u && u->status == CACHED_URL_AVAILABLE) {
if (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;
} else if (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 = %zu (%zu 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(cache, 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 = %zu (%zu 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 = "";
const char *ext = NULL;
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 (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_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Deleting %s from cache\n", 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 cache the cache
* @param url The cached URL entry
* @param session the (optional) session
* @return SWITCH_STATUS_SUCCESS if successful
*/
static switch_status_t http_get(url_cache_t *cache, cached_url_t *url, switch_core_session_t *session)
{
switch_status_t status = SWITCH_STATUS_SUCCESS;
switch_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 = switch_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) {
switch_curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
switch_curl_easy_setopt(curl_handle, CURLOPT_MAXREDIRS, 10);
switch_curl_easy_setopt(curl_handle, CURLOPT_URL, get_data.url->url);
switch_curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, get_file_callback);
switch_curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *) &get_data);
switch_curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, get_header_callback);
switch_curl_easy_setopt(curl_handle, CURLOPT_WRITEHEADER, (void *) url);
switch_curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "freeswitch-http-cache/1.0");
if (!cache->ssl_verifypeer) {
switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L);
} else {
/* this is the file with all the trusted certificate authorities */
if (!zstr(cache->ssl_cacert)) {
switch_curl_easy_setopt(curl_handle, CURLOPT_CAINFO, cache->ssl_cacert);
}
/* verify that the host name matches the cert */
if (!cache->ssl_verifyhost) {
switch_curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L);
}
}
switch_curl_easy_perform(curl_handle);
switch_curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &httpRes);
switch_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)
{
int i;
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 (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);
}
}
static int isUrl(const char *filename)
{
return !zstr(filename) && (!strncmp("http://", filename, strlen("http://")) || !strncmp("https://", filename, strlen("https://")));
}
#define HTTP_PREFETCH_SYNTAX "<url>"
SWITCH_STANDARD_API(http_cache_prefetch)
{
switch_status_t status = SWITCH_STATUS_SUCCESS;
char *url;
if (!isUrl(cmd)) {
stream->write_function(stream, "USAGE: %s\n", HTTP_PREFETCH_SYNTAX);
return SWITCH_STATUS_SUCCESS;
}
/* send to thread pool */
url = strdup(cmd);
if (switch_queue_trypush(gcache.prefetch_queue, url) != SWITCH_STATUS_SUCCESS) {
switch_safe_free(url);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_WARNING, "Failed to queue prefetch request\n");
stream->write_function(stream, "-ERR\n");
} else {
stream->write_function(stream, "+OK\n");
}
return status;
}
#define HTTP_GET_SYNTAX "<url>"
/**
* 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 (!isUrl(cmd)) {
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 "<url>"
/**
* 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 "<url> <file>"
/**
* 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;
if (zstr(cmd) || (strncmp("http://", cmd, strlen("http://")) && strncmp("https://", cmd, strlen("https://")))) {
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;
}
status = http_put(&gcache, session, argv[0], argv[1]);
if (status == SWITCH_STATUS_SUCCESS) {
stream->write_function(stream, "+OK\n");
} else {
stream->write_function(stream, "-ERR\n");
}
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;
}
/**
* Thread to prefetch URLs
* @param thread the thread
* @param obj started flag
* @return NULL
*/
static void *SWITCH_THREAD_FUNC prefetch_thread(switch_thread_t *thread, void *obj)
{
int *started = obj;
void *url = NULL;
switch_thread_rwlock_rdlock(gcache.shutdown_lock);
*started = 1;
// process prefetch requests
while (!gcache.shutdown) {
if (switch_queue_pop(gcache.prefetch_queue, &url) == SWITCH_STATUS_SUCCESS) {
switch_stream_handle_t stream = { 0 };
SWITCH_STANDARD_STREAM(stream);
switch_api_execute("http_get", url, NULL, &stream);
switch_safe_free(stream.data);
switch_safe_free(url);
}
url = NULL;
}
// shutting down- clear the queue
while (switch_queue_trypop(gcache.prefetch_queue, &url) == SWITCH_STATUS_SUCCESS) {
switch_safe_free(url);
url = NULL;
}
switch_thread_rwlock_unlock(gcache.shutdown_lock);
return NULL;
}
/**
* 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;
switch_time_t 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";
cache->prefetch_queue_size = 100;
cache->prefetch_thread_count = 8;
cache->ssl_cacert = SWITCH_PREFIX_DIR "/conf/cacert.pem";
cache->ssl_verifyhost = 1;
cache->ssl_verifypeer = 1;
/* 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 if (!strcasecmp(var, "prefetch-queue-size")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting prefetch-queue-size to %s\n", val);
cache->prefetch_queue_size = atoi(val);
} else if (!strcasecmp(var, "prefetch-thread-count")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting prefetch-thread-count to %s\n", val);
cache->prefetch_thread_count = atoi(val);
} else if (!strcasecmp(var, "ssl-cacert")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting ssl-cacert to %s\n", val);
cache->ssl_cacert = switch_core_strdup(cache->pool, val);
} else if (!strcasecmp(var, "ssl-verifyhost")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting ssl-verifyhost to %s\n", val);
cache->ssl_verifyhost = !switch_false(val); /* only disable if explicitly set to false */
} else if (!strcasecmp(var, "ssl-verifypeer")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Setting ssl-verifypeer to %s\n", val);
cache->ssl_verifypeer = !switch_false(val); /* only disable if explicitly set to false */
} 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;
}
if (cache->prefetch_queue_size <= 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "prefetch-queue-size must be > 0\n");
status = SWITCH_STATUS_TERM;
goto done;
}
if (cache->prefetch_thread_count <= 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "prefetch-thread-count 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;
int i;
*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);
SWITCH_ADD_API(api, "http_prefetch", "Prefetch document in a background thread. Use http_get to get the prefetched document", http_cache_prefetch, HTTP_PREFETCH_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);
switch_thread_rwlock_create(&gcache.shutdown_lock, 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);
/* Start the prefetch threads */
switch_queue_create(&gcache.prefetch_queue, gcache.prefetch_queue_size, gcache.pool);
for (i = 0; i < gcache.prefetch_thread_count; i++) {
int started = 0;
switch_thread_t *thread;
switch_threadattr_t *thd_attr = NULL;
switch_threadattr_create(&thd_attr, gcache.pool);
switch_threadattr_detach_set(thd_attr, 1);
switch_threadattr_stacksize_set(thd_attr, SWITCH_THREAD_STACKSIZE);
switch_thread_create(&thread, thd_attr, prefetch_thread, &started, gcache.pool);
while (!started) {
switch_sleep(1000);
}
}
/* 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)
{
gcache.shutdown = 1;
switch_queue_interrupt_all(gcache.prefetch_queue);
switch_thread_rwlock_wrlock(gcache.shutdown_lock);
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:
*/