diff --git a/src/mod/applications/mod_http_cache/Makefile b/src/mod/applications/mod_http_cache/Makefile index c9dfa5399e..9b61a41042 100644 --- a/src/mod/applications/mod_http_cache/Makefile +++ b/src/mod/applications/mod_http_cache/Makefile @@ -1,4 +1,9 @@ BASE=../../../.. +LOCAL_OBJS= \ + aws.o +LOCAL_SOURCES= \ + aws.c + include $(BASE)/build/modmake.rules diff --git a/src/mod/applications/mod_http_cache/aws.c b/src/mod/applications/mod_http_cache/aws.c new file mode 100644 index 0000000000..fafe9cd4d7 --- /dev/null +++ b/src/mod/applications/mod_http_cache/aws.c @@ -0,0 +1,238 @@ +/* + * aws.c for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2013, Grasshopper + * + * 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 aws.c for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is Grasshopper + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Rienzo + * + * aws.c -- Some Amazon Web Services helper functions + * + */ +#include "aws.h" +#include + +#if defined(HAVE_OPENSSL) +#include +#include +#endif + +/* 160 bits / 8 bits per byte */ +#define SHA1_LENGTH 20 + +/** + * @param url to check + * @return true if this is an S3 url + */ +int aws_s3_is_s3_url(const char *url) +{ + /* AWS bucket naming rules are complex... this match only supports virtual hosting of buckets */ + return !zstr(url) && switch_regex_match(url, "^https?://[a-z0-9][-a-z0-9.]{1,61}[a-z0-9]\\.s3\\.amazonaws\\.com/.*$") == SWITCH_STATUS_SUCCESS; +} + +/** + * Create the string to sign for a AWS signature calculation + * @param verb (PUT/GET) + * @param bucket bucket object is stored in + * @param object to access (filename.ext) + * @param content_type optional content type + * @param content_md5 optional content MD5 checksum + * @param date header + * @return the string_to_sign (must be freed) + */ +char *aws_s3_string_to_sign(const char *verb, const char *bucket, const char *object, const char *content_type, const char *content_md5, const char *date) +{ + /* + * String to sign has the following format: + * \n\n\n\n/bucket/object + */ + return switch_mprintf("%s\n%s\n%s\n%s\n/%s/%s", + verb, content_md5 ? content_md5 : "", content_type ? content_type : "", + date, bucket, object); +} + +/** + * Create the AWS S3 signature + * @param signature buffer to store the signature + * @param signature_length length of signature buffer + * @param string_to_sign + * @param aws_secret_access_key secret access key + * @return the signature buffer or NULL if missing input + */ +char *aws_s3_signature(char *signature, int signature_length, const char *string_to_sign, const char *aws_secret_access_key) +{ +#if defined(HAVE_OPENSSL) + unsigned int signature_raw_length = SHA1_LENGTH; + char signature_raw[SHA1_LENGTH]; + signature_raw[0] = '\0'; + if (!signature || signature_length <= 0) { + return NULL; + } + if (zstr(aws_secret_access_key)) { + return NULL; + } + if (!string_to_sign) { + string_to_sign = ""; + } + HMAC(EVP_sha1(), + aws_secret_access_key, + strlen(aws_secret_access_key), + (const unsigned char *)string_to_sign, + strlen(string_to_sign), + (unsigned char *)signature_raw, + &signature_raw_length); + + /* convert result to base64 */ + memset(signature, 0, signature_length); + switch_b64_encode((unsigned char *)signature_raw, signature_raw_length, (unsigned char *)signature, signature_length); +#endif + return signature; +} + +/** + * Parse bucket and object from URL + * @param url to parse. This value is modified. + * @param bucket to store result in + * @param bucket_length of result buffer + */ +void aws_s3_parse_url(char *url, char **bucket, char **object) +{ + char *bucket_start; + char *bucket_end; + char *object_start; + + *bucket = NULL; + *object = NULL; + + if (!aws_s3_is_s3_url(url)) { + return; + } + + /* expect: http(s)://bucket.s3.amazonaws.com/object */ + bucket_start = strstr(url, "://"); + if (!bucket_start) { + /* invalid URL */ + return; + } + bucket_start += 3; + + bucket_end = strchr(bucket_start, '.'); + if (!bucket_end) { + /* invalid URL */ + return; + } + *bucket_end = '\0'; + + object_start = strchr(bucket_end + 1, '/'); + if (!object_start) { + /* invalid URL */ + return; + } + object_start++; + + if (strchr(object_start, '/')) { + /* invalid URL */ + return; + } + + if (zstr(bucket_start) || zstr(object_start)) { + /* invalid URL */ + return; + } + + *bucket = bucket_start; + *object = object_start; +} + +/** + * Create a pre-signed URL for AWS S3 + * @param verb (PUT/GET) + * @param url address (virtual-host-style) + * @param content_type optional content type + * @param content_md5 optional content MD5 checksum + * @param aws_access_key_id secret access key identifier + * @param aws_secret_access_key secret access key + * @param expires seconds since the epoch + * @return presigned_url + */ +char *aws_s3_presigned_url_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires) +{ + char signature[S3_SIGNATURE_LENGTH_MAX]; + char signature_url_encoded[S3_SIGNATURE_LENGTH_MAX]; + char *string_to_sign; + char *url_dup = strdup(url); + char *bucket; + char *object; + + /* create URL encoded signature */ + aws_s3_parse_url(url_dup, &bucket, &object); + string_to_sign = aws_s3_string_to_sign(verb, bucket, object, content_type, content_md5, expires); + signature[0] = '\0'; + aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, string_to_sign, aws_secret_access_key); + switch_url_encode(signature, signature_url_encoded, S3_SIGNATURE_LENGTH_MAX); + free(string_to_sign); + free(url_dup); + + /* create the presigned URL */ + return switch_mprintf("%s?Signature=%s&Expires=%s&AWSAccessKeyId=%s", url, signature_url_encoded, expires, aws_access_key_id); +} + +/** + * Create an authentication signature for AWS S3 + * @param authentication buffer to store result + * @param authentication_length maximum result length + * @param verb (PUT/GET) + * @param url address (virtual-host-style) + * @param content_type optional content type + * @param content_md5 optional content MD5 checksum + * @param aws_access_key_id secret access key identifier + * @param aws_secret_access_key secret access key + * @param date header + * @return signature for Authorization header + */ +char *aws_s3_authentication_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *date) +{ + char signature[S3_SIGNATURE_LENGTH_MAX]; + char *string_to_sign; + char *url_dup = strdup(url); + char *bucket; + char *object; + + /* create base64 encoded signature */ + aws_s3_parse_url(url_dup, &bucket, &object); + string_to_sign = aws_s3_string_to_sign(verb, bucket, object, content_type, content_md5, date); + signature[0] = '\0'; + aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, string_to_sign, aws_secret_access_key); + free(string_to_sign); + free(url_dup); + + return switch_mprintf("AWS %s:%s", aws_access_key_id, signature); +} + +/* 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 + */ diff --git a/src/mod/applications/mod_http_cache/aws.h b/src/mod/applications/mod_http_cache/aws.h new file mode 100644 index 0000000000..57c7ed652d --- /dev/null +++ b/src/mod/applications/mod_http_cache/aws.h @@ -0,0 +1,55 @@ +/* + * aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2013, Grasshopper + * + * 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 aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is Grasshopper + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Rienzo + * + * aws.h - Some Amazon Web Services helper functions + * + */ +#ifndef AWS_H +#define AWS_H + +#include + +/* (SHA1_LENGTH * 1.37 base64 bytes per byte * 3 url-encoded bytes per byte) */ +#define S3_SIGNATURE_LENGTH_MAX 83 + +int aws_s3_is_s3_url(const char *url); +void aws_s3_parse_url(char *url, char **bucket, char **object); +char *aws_s3_string_to_sign(const char *verb, const char *bucket, const char *object, const char *content_type, const char *content_md5, const char *date); +char *aws_s3_signature(char *signature, int signature_length, const char *string_to_sign, const char *aws_secret_access_key); +char *aws_s3_presigned_url_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires); +char *aws_s3_authentication_create(const char *verb, const char *url, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *date); + +#endif + +/* 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 + */ diff --git a/src/mod/applications/mod_http_cache/conf/autoload_configs/http_cache.conf.xml b/src/mod/applications/mod_http_cache/conf/autoload_configs/http_cache.conf.xml index 1fe886227d..1b25005839 100644 --- a/src/mod/applications/mod_http_cache/conf/autoload_configs/http_cache.conf.xml +++ b/src/mod/applications/mod_http_cache/conf/autoload_configs/http_cache.conf.xml @@ -9,4 +9,19 @@ + + + + + + + + + + + + + + + diff --git a/src/mod/applications/mod_http_cache/mod_http_cache.c b/src/mod/applications/mod_http_cache/mod_http_cache.c index 897a1b9035..5eb191b9fa 100644 --- a/src/mod/applications/mod_http_cache/mod_http_cache.c +++ b/src/mod/applications/mod_http_cache/mod_http_cache.c @@ -1,6 +1,6 @@ /* * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - * Copyright (C) 2005-2012, Anthony Minessale II + * Copyright (C) 2005-2013, Anthony Minessale II * * Version: MPL 1.1 * @@ -34,6 +34,7 @@ */ #include #include +#include "aws.h" /* Defines module interface to FreeSWITCH */ SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_http_cache_shutdown); @@ -49,6 +50,17 @@ SWITCH_STANDARD_API(http_cache_prefetch); typedef struct url_cache url_cache_t; +/** + * An http profile. Defines optional credentials + * for access to Amazon S3. + */ +struct http_profile { + const char *name; + const char *aws_s3_access_key_id; + const char *aws_s3_secret_access_key; +}; +typedef struct http_profile http_profile_t; + /** * status if the cache entry */ @@ -99,12 +111,12 @@ struct http_get_data { }; 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 switch_status_t http_get(url_cache_t *cache, http_profile_t *profile, 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); +static switch_status_t http_put(url_cache_t *cache, http_profile_t *profile, switch_core_session_t *session, const char *url, const char *filename); /** * Queue used for clock cache replacement algorithm. This @@ -123,7 +135,6 @@ struct simple_queue { }; typedef struct simple_queue simple_queue_t; - /** * The cache */ @@ -138,6 +149,8 @@ struct url_cache { size_t size; /** The location of the cache in the filesystem */ char *location; + /** HTTP profiles */ + switch_hash_t *profiles; /** Cache mapped by URL */ switch_hash_t *map; /** Cached URLs queued for replacement */ @@ -173,7 +186,7 @@ struct url_cache { }; 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 char *url_cache_get(url_cache_t *cache, http_profile_t *profile, 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); @@ -181,16 +194,21 @@ static switch_status_t url_cache_replace(url_cache_t *cache, switch_core_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); +static http_profile_t *url_cache_http_profile_find(url_cache_t *cache, const char *name); +static void url_cache_http_profile_add(url_cache_t *cache, const char *name, const char *aws_s3_access_key_id, const char *aws_s3_secret_access_key); + +static switch_curl_slist_t *append_aws_s3_headers(switch_curl_slist_t *headers, http_profile_t *profile, const char *verb, const char *content_type, const char *url); /** * Put a file to the URL * @param cache the cache + * @param profile the HTTP profile * @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) +static switch_status_t http_put(url_cache_t *cache, http_profile_t *profile, switch_core_session_t *session, const char *url, const char *filename) { switch_status_t status = SWITCH_STATUS_SUCCESS; @@ -215,6 +233,7 @@ static switch_status_t http_put(url_cache_t *cache, switch_core_session_t *sessi buf = switch_mprintf("Content-Type: %s", mime_type); headers = switch_curl_slist_append(headers, buf); + headers = append_aws_s3_headers(headers, profile, "PUT", mime_type, url); /* 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); @@ -316,6 +335,7 @@ static size_t get_file_callback(void *ptr, size_t size, size_t nmemb, void *get) return result; } + /** * trim whitespace characters from string * @param str the string to trim @@ -492,13 +512,14 @@ static void url_cache_clear(url_cache_t *cache, switch_core_session_t *session) /** * Get a URL from the cache, add it if it does not exist * @param cache The cache + * @param profile optional profile * @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) +static char *url_cache_get(url_cache_t *cache, http_profile_t *profile, switch_core_session_t *session, const char *url, int download, switch_memory_pool_t *pool) { char *filename = NULL; cached_url_t *u = NULL; @@ -537,7 +558,7 @@ static char *url_cache_get(url_cache_t *cache, switch_core_session_t *session, c /* download the file */ url_cache_unlock(cache, session); - if (http_get(cache, u, session) == SWITCH_STATUS_SUCCESS) { + if (http_get(cache, profile, 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; @@ -685,6 +706,33 @@ static void url_cache_remove(url_cache_t *cache, switch_core_session_t *session, cache->size -= url->size; } +/** + * Find a profile + */ +static http_profile_t *url_cache_http_profile_find(url_cache_t *cache, const char *name) +{ + if (cache && !zstr(name)) { + return (http_profile_t *)switch_core_hash_find(cache->profiles, name); + } + return NULL; +} + +/** + * Add a profile to the cache + */ +static void url_cache_http_profile_add(url_cache_t *cache, const char *name, const char *aws_s3_access_key_id, const char *aws_s3_secret_access_key) +{ + http_profile_t *profile = switch_core_alloc(cache->pool, sizeof(*profile)); + profile->name = switch_core_strdup(cache->pool, name); + if (aws_s3_access_key_id) { + profile->aws_s3_access_key_id = switch_core_strdup(cache->pool, aws_s3_access_key_id); + } + if (aws_s3_secret_access_key) { + profile->aws_s3_secret_access_key = switch_core_strdup(cache->pool, aws_s3_secret_access_key); + } + switch_core_hash_insert(cache->profiles, profile->name, profile); +} + /** * Create a cached URL entry * @param cache the cache @@ -759,16 +807,49 @@ static void cached_url_destroy(cached_url_t *url, switch_memory_pool_t *pool) switch_safe_free(url); } +/** + * Append Amazon S3 headers to request if necessary + * @param headers to add to. If NULL, new headers are created. + * @param profile with S3 credentials + * @param content_type of object (PUT only) + * @param verb (GET/PUT) + * @param url + * @return updated headers + */ +static switch_curl_slist_t *append_aws_s3_headers(switch_curl_slist_t *headers, http_profile_t *profile, const char *verb, const char *content_type, const char *url) +{ + /* check if Amazon headers are needed */ + if (profile && profile->aws_s3_access_key_id && aws_s3_is_s3_url(url)) { + char date[256]; + char header[1024]; + char *authenticate; + + /* Date: */ + switch_rfc822_date(date, switch_time_now()); + snprintf(header, 1024, "Date: %s", date); + headers = switch_curl_slist_append(headers, header); + + /* Authorization: */ + authenticate = aws_s3_authentication_create(verb, url, content_type, "", profile->aws_s3_access_key_id, profile->aws_s3_secret_access_key, date); + snprintf(header, 1024, "Authorization: %s", authenticate); + free(authenticate); + headers = switch_curl_slist_append(headers, header); + } + return headers; +} + /** * Fetch a file via HTTP * @param cache the cache + * @param profile the HTTP profile * @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) +static switch_status_t http_get(url_cache_t *cache, http_profile_t *profile, cached_url_t *url, switch_core_session_t *session) { switch_status_t status = SWITCH_STATUS_SUCCESS; + switch_curl_slist_t *headers = NULL; /* optional linked-list of HTTP headers */ switch_CURL *curl_handle = NULL; http_get_data_t get_data = {0}; long httpRes = 0; @@ -778,12 +859,18 @@ static switch_status_t http_get(url_cache_t *cache, cached_url_t *url, switch_co get_data.fd = 0; get_data.url = url; + /* add optional AWS S3 headers if necessary */ + headers = append_aws_s3_headers(headers, profile, "GET", "", 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_NOSIGNAL, 1); + if (headers) { + switch_curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers); + } 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); @@ -808,7 +895,8 @@ static switch_status_t http_get(url_cache_t *cache, cached_url_t *url, switch_co 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; + status = SWITCH_STATUS_GENERR; + goto done; } if (httpRes == 200) { @@ -821,7 +909,14 @@ static switch_status_t http_get(url_cache_t *cache, cached_url_t *url, switch_co } 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; + status = SWITCH_STATUS_GENERR; + goto done; + } + +done: + + if (headers) { + switch_curl_slist_free_all(headers); } return status; @@ -858,18 +953,13 @@ static void setup_dir(url_cache_t *cache) } } -static int isUrl(const char *filename) -{ - return !zstr(filename) && (!strncmp("http://", filename, strlen("http://")) || !strncmp("https://", filename, strlen("https://"))); -} - -#define HTTP_PREFETCH_SYNTAX "" +#define HTTP_PREFETCH_SYNTAX "{param=val}" SWITCH_STANDARD_API(http_cache_prefetch) { switch_status_t status = SWITCH_STATUS_SUCCESS; char *url; - if (!isUrl(cmd)) { + if (zstr(cmd)) { stream->write_function(stream, "USAGE: %s\n", HTTP_PREFETCH_SYNTAX); return SWITCH_STATUS_SUCCESS; } @@ -887,7 +977,7 @@ SWITCH_STANDARD_API(http_cache_prefetch) return status; } -#define HTTP_GET_SYNTAX "" +#define HTTP_GET_SYNTAX "{param=val}" /** * Get a file from the cache, download if it isn't cached */ @@ -896,9 +986,12 @@ 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; + http_profile_t *profile = NULL; char *filename; + switch_event_t *params = NULL; + char *url; - if (!isUrl(cmd)) { + if (zstr(cmd)) { stream->write_function(stream, "USAGE: %s\n", HTTP_GET_SYNTAX); return SWITCH_STATUS_SUCCESS; } @@ -910,7 +1003,16 @@ SWITCH_STANDARD_API(http_cache_get) pool = lpool; } - filename = url_cache_get(&gcache, session, cmd, 1, pool); + /* parse params and get profile */ + url = switch_core_strdup(pool, cmd); + if (*url == '{') { + switch_event_create_brackets(url, '{', '}', ',', ¶ms, &url, SWITCH_FALSE); + } + if (params) { + profile = url_cache_http_profile_find(&gcache, switch_event_get_header(params, "profile")); + } + + filename = url_cache_get(&gcache, profile, session, url, 1, pool); if (filename) { stream->write_function(stream, "%s", filename); @@ -923,10 +1025,14 @@ SWITCH_STANDARD_API(http_cache_get) switch_core_destroy_memory_pool(&lpool); } + if (params) { + switch_event_destroy(¶ms); + } + return status; } -#define HTTP_TRYGET_SYNTAX "" +#define HTTP_TRYGET_SYNTAX "{param=val}" /** * Get a file from the cache, fail if download is needed */ @@ -935,9 +1041,12 @@ 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; + http_profile_t *profile = NULL; char *filename; + switch_event_t *params = NULL; + char *url; - if (!isUrl(cmd)) { + if (zstr(cmd)) { stream->write_function(stream, "USAGE: %s\n", HTTP_GET_SYNTAX); return SWITCH_STATUS_SUCCESS; } @@ -949,7 +1058,16 @@ SWITCH_STANDARD_API(http_cache_tryget) pool = lpool; } - filename = url_cache_get(&gcache, session, cmd, 0, pool); + /* parse params and get profile */ + url = switch_core_strdup(pool, cmd); + if (*url == '{') { + switch_event_create_brackets(url, '{', '}', ',', ¶ms, &url, SWITCH_FALSE); + } + if (params) { + profile = url_cache_http_profile_find(&gcache, switch_event_get_header(params, "profile")); + } + + filename = url_cache_get(&gcache, profile, session, url, 0, pool); if (filename) { if (!strcmp(DOWNLOAD_NEEDED, filename)) { stream->write_function(stream, "-ERR %s\n", DOWNLOAD_NEEDED); @@ -965,21 +1083,37 @@ SWITCH_STANDARD_API(http_cache_tryget) switch_core_destroy_memory_pool(&lpool); } + if (params) { + switch_event_destroy(¶ms); + } + return status; } -#define HTTP_PUT_SYNTAX " " +#define HTTP_PUT_SYNTAX "{param=val} " /** * Put a file to the server */ SWITCH_STANDARD_API(http_cache_put) { switch_status_t status = SWITCH_STATUS_SUCCESS; + http_profile_t *profile = NULL; + switch_memory_pool_t *lpool = NULL; + switch_memory_pool_t *pool = NULL; char *args = NULL; char *argv[10] = { 0 }; int argc = 0; + switch_event_t *params = NULL; + char *url; - if (zstr(cmd) || (strncmp("http://", cmd, strlen("http://")) && strncmp("https://", cmd, strlen("https://")))) { + if (session) { + pool = switch_core_session_get_pool(session); + } else { + switch_core_new_memory_pool(&lpool); + pool = lpool; + } + + if (zstr(cmd)) { stream->write_function(stream, "USAGE: %s\n", HTTP_PUT_SYNTAX); status = SWITCH_STATUS_SUCCESS; goto done; @@ -993,7 +1127,16 @@ SWITCH_STANDARD_API(http_cache_put) goto done; } - status = http_put(&gcache, session, argv[0], argv[1]); + /* parse params and get profile */ + url = switch_core_strdup(pool, argv[0]); + if (*url == '{') { + switch_event_create_brackets(url, '{', '}', ',', ¶ms, &url, SWITCH_FALSE); + } + if (params) { + profile = url_cache_http_profile_find(&gcache, switch_event_get_header(params, "profile")); + } + + status = http_put(&gcache, profile, session, url, argv[1]); if (status == SWITCH_STATUS_SUCCESS) { stream->write_function(stream, "+OK\n"); } else { @@ -1002,6 +1145,15 @@ SWITCH_STANDARD_API(http_cache_put) done: switch_safe_free(args); + + if (lpool) { + switch_core_destroy_memory_pool(&lpool); + } + + if (params) { + switch_event_destroy(¶ms); + } + return status; } @@ -1066,7 +1218,7 @@ static void *SWITCH_THREAD_FUNC prefetch_thread(switch_thread_t *thread, void *o static switch_status_t do_config(url_cache_t *cache) { char *cf = "http_cache.conf"; - switch_xml_t cfg, xml, param, settings; + switch_xml_t cfg, xml, param, settings, profiles; switch_status_t status = SWITCH_STATUS_SUCCESS; int max_urls; switch_time_t default_max_age_sec; @@ -1128,6 +1280,39 @@ static switch_status_t do_config(url_cache_t *cache) } } + /* get profiles */ + profiles = switch_xml_child(cfg, "profiles"); + if (profiles) { + switch_xml_t profile; + for (profile = switch_xml_child(profiles, "profile"); profile; profile = profile->next) { + const char *name = switch_xml_attr_soft(profile, "name"); + if (!zstr(name)) { + switch_xml_t s3 = switch_xml_child(profile, "aws-s3"); + const char *access_key_id = NULL; + const char *secret_access_key = NULL; + if (s3) { + switch_xml_t id = switch_xml_child(s3, "access-key-id"); + switch_xml_t secret = switch_xml_child(s3, "secret-access-key"); + if (id && secret) { + access_key_id = switch_xml_txt(id); + secret_access_key = switch_xml_txt(secret); + if (!access_key_id || !secret_access_key) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Missing aws s3 credentials for profile \"%s\"\n", name); + access_key_id = NULL; + secret_access_key = NULL; + } + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Missing key id or secret\n"); + } + } + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Adding profile \"%s\" to cache\n", name); + url_cache_http_profile_add(cache, name, access_key_id, secret_access_key); + } else { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "HTTP profile missing name\n"); + } + } + } + /* check config */ if (max_urls <= 0) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "max-urls must be > 0\n"); @@ -1179,6 +1364,7 @@ struct http_context { static switch_status_t http_cache_file_open(switch_file_handle_t *handle, const char *path) { switch_status_t status = SWITCH_STATUS_SUCCESS; + http_profile_t *profile = NULL; struct http_context *context = switch_core_alloc(handle->memory_pool, sizeof(*context)); const char *local_path; @@ -1187,7 +1373,10 @@ static switch_status_t http_cache_file_open(switch_file_handle_t *handle, const return SWITCH_STATUS_FALSE; } - local_path = url_cache_get(&gcache, NULL, path, 1, handle->memory_pool); + if (handle->params) { + profile = url_cache_http_profile_find(&gcache, switch_event_get_header(handle->params, "profile")); + } + local_path = url_cache_get(&gcache, profile, NULL, path, 1, handle->memory_pool); if (!local_path) { return SWITCH_STATUS_FALSE; } @@ -1289,6 +1478,10 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_http_cache_load) memset(&gcache, 0, sizeof(url_cache_t)); gcache.pool = pool; + switch_core_hash_init(&gcache.map, gcache.pool); + switch_core_hash_init(&gcache.profiles, gcache.pool); + switch_mutex_init(&gcache.mutex, SWITCH_MUTEX_UNNESTED, gcache.pool); + switch_thread_rwlock_create(&gcache.shutdown_lock, gcache.pool); if (do_config(&gcache) != SWITCH_STATUS_SUCCESS) { return SWITCH_STATUS_TERM; @@ -1317,11 +1510,7 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_http_cache_load) file_interface->file_read = http_file_read; } - 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 */ + /* create the queue from configuration */ 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; diff --git a/src/mod/applications/mod_http_cache/test_aws/Makefile b/src/mod/applications/mod_http_cache/test_aws/Makefile new file mode 100644 index 0000000000..40a160631a --- /dev/null +++ b/src/mod/applications/mod_http_cache/test_aws/Makefile @@ -0,0 +1,12 @@ +BASE=../../../../.. + +LOCAL_CFLAGS += -I../ -I./ +LOCAL_OBJS= main.o ../aws.o +LOCAL_SOURCES= main.c +include $(BASE)/build/modmake.rules + +local_all: + libtool --mode=link gcc main.o ../aws.o -o test test_aws.la + +local_clean: + -rm test diff --git a/src/mod/applications/mod_http_cache/test_aws/main.c b/src/mod/applications/mod_http_cache/test_aws/main.c new file mode 100644 index 0000000000..0bc7383b93 --- /dev/null +++ b/src/mod/applications/mod_http_cache/test_aws/main.c @@ -0,0 +1,131 @@ + + +#include +#include "test.h" +#include "aws.h" + +/** + * Test string to sign generation + */ +static void test_string_to_sign(void) +{ + ASSERT_STRING_EQUALS("GET\n\n\nFri, 17 May 2013 19:35:26 GMT\n/rienzo-vault/troporocks.mp3", aws_s3_string_to_sign("GET", "rienzo-vault", "troporocks.mp3", "", "", "Fri, 17 May 2013 19:35:26 GMT")); + ASSERT_STRING_EQUALS("GET\nc8fdb181845a4ca6b8fec737b3581d76\naudio/mpeg\nThu, 17 Nov 2005 18:49:58 GMT\n/foo/man.chu", aws_s3_string_to_sign("GET", "foo", "man.chu", "audio/mpeg", "c8fdb181845a4ca6b8fec737b3581d76", "Thu, 17 Nov 2005 18:49:58 GMT")); + ASSERT_STRING_EQUALS("\n\n\n\n//", aws_s3_string_to_sign("", "", "", "", "", "")); + ASSERT_STRING_EQUALS("\n\n\n\n//", aws_s3_string_to_sign(NULL, NULL, NULL, NULL, NULL, NULL)); +} + +/** + * Test signature generation + */ +static void test_signature(void) +{ + char signature[S3_SIGNATURE_LENGTH_MAX]; + signature[0] = '\0'; + ASSERT_STRING_EQUALS("weGrLrc9HDlkYPTepVl0A9VYNlw=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\nFri, 17 May 2013 19:35:26 GMT\n/rienzo-vault/troporocks.mp3", "hOIZt1oeTX1JzINOMBoKf0BxONRZNQT1J8gIznLx")); + ASSERT_STRING_EQUALS("jZNOcbfWmD/A/f3hSvVzXZjM2HU=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + ASSERT_STRING_EQUALS("5m+HAmc5JsrgyDelh9+a2dNrzN8=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\n\nx-amz-date:Thu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + ASSERT_STRING_EQUALS("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + ASSERT_STRING_EQUALS("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "GET\n\n\n\nx-amz-date:Thu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\n/quotes/nelson", "")); + ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", "")); + ASSERT_NULL(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, NULL)); + ASSERT_NULL(aws_s3_signature(NULL, S3_SIGNATURE_LENGTH_MAX, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + ASSERT_NULL(aws_s3_signature(signature, 0, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); + /* TODO freeswitch bug... */ + //ASSERT_STRING_EQUALS("jZNO", aws_s3_signature(signature, 5, "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); +} + +/** + * Test amazon URL detection + */ +static void test_check_url(void) +{ + ASSERT_TRUE(aws_s3_is_s3_url("http://bucket.s3.amazonaws.com/object.ext")); + ASSERT_TRUE(aws_s3_is_s3_url("http://bucket.s3.amazonaws.com/object")); + ASSERT_TRUE(aws_s3_is_s3_url("http://red.bucket.s3.amazonaws.com/object.ext")); + ASSERT_TRUE(aws_s3_is_s3_url("https://bucket.s3.amazonaws.com/object.ext")); + ASSERT_TRUE(aws_s3_is_s3_url("https://bucket.s3.amazonaws.com/object")); + ASSERT_FALSE(aws_s3_is_s3_url("bucket.s3.amazonaws.com/object.ext")); + ASSERT_FALSE(aws_s3_is_s3_url("https://s3.amazonaws.com/bucket/object")); + ASSERT_FALSE(aws_s3_is_s3_url("http://s3.amazonaws.com/bucket/object")); + ASSERT_FALSE(aws_s3_is_s3_url("http://google.com/")); + ASSERT_FALSE(aws_s3_is_s3_url("http://phono.com/audio/troporocks.mp3")); + ASSERT_FALSE(aws_s3_is_s3_url("")); + ASSERT_FALSE(aws_s3_is_s3_url(NULL)); +} + +/** + * Test bucket/object extraction from URL + */ +static void test_parse_url(void) +{ + char *bucket; + char *object; + aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com/nelson"), &bucket, &object); + ASSERT_STRING_EQUALS("quotes", bucket); + ASSERT_STRING_EQUALS("nelson", object); + + aws_s3_parse_url(strdup("https://quotes.s3.amazonaws.com/nelson.mp3"), &bucket, &object); + ASSERT_STRING_EQUALS("quotes", bucket); + ASSERT_STRING_EQUALS("nelson.mp3", object); + + aws_s3_parse_url(strdup("http://s3.amazonaws.com/quotes/nelson"), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(strdup("http://quotes/quotes/nelson"), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com/"), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(strdup("http://quotes.s3.amazonaws.com"), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(strdup("http://quotes"), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(strdup(""), &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); + + aws_s3_parse_url(NULL, &bucket, &object); + ASSERT_NULL(bucket); + ASSERT_NULL(object); +} + +/** + * Test Authorization header creation + */ +static void test_authorization_header(void) +{ + ASSERT_STRING_EQUALS("AWS AKIAIOSFODNN7EXAMPLE:YJkomOaqUJlvEluDq4fpusID38Y=", aws_s3_authentication_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890")); +} + +/** + * Test pre-signed URL creation + */ +static void test_presigned_url(void) +{ + ASSERT_STRING_EQUALS("https://vault.s3.amazonaws.com/awesome.mp3?Signature=YJkomOaqUJlvEluDq4fpusID38Y%3D&Expires=1234567890&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE", aws_s3_presigned_url_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890")); +} + +/** + * main program + */ +int main(int argc, char **argv) +{ + TEST_INIT + TEST(test_string_to_sign); + TEST(test_signature); + TEST(test_check_url); + TEST(test_parse_url); + TEST(test_authorization_header); + TEST(test_presigned_url); + return 0; +} diff --git a/src/mod/applications/mod_http_cache/test_aws/test.h b/src/mod/applications/mod_http_cache/test_aws/test.h new file mode 100644 index 0000000000..91c9b7960a --- /dev/null +++ b/src/mod/applications/mod_http_cache/test_aws/test.h @@ -0,0 +1,113 @@ +/* + * test.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2013, Grasshopper + * + * 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 test.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is Grasshopper + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Rienzo + * + * test.h -- simple unit testing macros + * + */ +#ifndef TEST_H +#define TEST_H + +#define assert_equals(test, expected_str, expected, actual, file, line) \ +{ \ + int actual_val = actual; \ + if (expected != actual_val) { \ + printf("TEST\t%s\tFAIL\t%s\t%i\t!=\t%i\t%s:%i\n", test, expected_str, expected, actual_val, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define assert_string_equals(test, expected, actual, file, line) \ +{ \ + const char *actual_str = actual; \ + if (!actual_str || strcmp(expected, actual_str)) { \ + printf("TEST\t%s\tFAIL\t\t%s\t!=\t%s\t%s:%i\n", test, expected, actual_str, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define assert_not_null(test, actual, file, line) \ +{ \ + const void *actual_val = actual; \ + if (!actual_val) { \ + printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define assert_null(test, actual, file, line) \ +{ \ + const void *actual_val = actual; \ + if (actual_val) { \ + printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define assert_true(test, actual, file, line) \ +{ \ + int actual_val = actual; \ + if (!actual_val) { \ + printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define assert_false(test, actual, file, line) \ +{ \ + int actual_val = actual; \ + if (actual_val) { \ + printf("TEST\t%s\tFAIL\t\t\t\t\t%s:%i\n", test, file, line); \ + exit(1); \ + } else { \ + printf("TEST\t%s\tPASS\n", test); \ + } \ +} + +#define ASSERT_EQUALS(expected, actual) assert_equals(#actual, #expected, expected, actual, __FILE__, __LINE__) +#define ASSERT_STRING_EQUALS(expected, actual) assert_string_equals(#actual, expected, actual, __FILE__, __LINE__) +#define ASSERT_NOT_NULL(actual) assert_not_null(#actual " not null", actual, __FILE__, __LINE__) +#define ASSERT_NULL(actual) assert_null(#actual " is null", actual, __FILE__, __LINE__) +#define ASSERT_TRUE(actual) assert_true(#actual " is true", actual, __FILE__, __LINE__) +#define ASSERT_FALSE(actual) assert_false(#actual " is false", actual, __FILE__, __LINE__) + +#define SKIP_ASSERT_EQUALS(expected, actual) if (0) { ASSERT_EQUALS(expected, actual); } + +#define TEST(name) printf("TEST BEGIN\t" #name "\n"); name(); printf("TEST END\t"#name "\tPASS\n"); + +#define SKIP_TEST(name) if (0) { TEST(name) }; + +#define TEST_INIT const char *err; switch_core_init(0, SWITCH_TRUE, &err); + +#endif diff --git a/src/mod/applications/mod_http_cache/test_aws/test_aws.c b/src/mod/applications/mod_http_cache/test_aws/test_aws.c new file mode 100644 index 0000000000..a0953207de --- /dev/null +++ b/src/mod/applications/mod_http_cache/test_aws/test_aws.c @@ -0,0 +1,6 @@ +int dummy(int i) +{ + return 0; +} + +