diff --git a/src/mod/applications/mod_http_cache/aws.c b/src/mod/applications/mod_http_cache/aws.c index fbbbf00d98..9c49363e6f 100644 --- a/src/mod/applications/mod_http_cache/aws.c +++ b/src/mod/applications/mod_http_cache/aws.c @@ -22,6 +22,7 @@ * * Contributor(s): * Chris Rienzo + * Quoc-Bao Nguyen * * aws.c -- Some Amazon Web Services helper functions * @@ -34,211 +35,378 @@ #include #endif -/* 160 bits / 8 bits per byte */ -#define SHA1_LENGTH 20 -/** - * 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) - */ -static 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 - */ -static 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 */ - switch_b64_encode((unsigned char *)signature_raw, signature_raw_length, (unsigned char *)signature, signature_length); +#include +#include #endif - return signature; + +#if defined(HAVE_OPENSSL) +/** + * Calculate HMAC-SHA256 hash of a message + * @param buffer buffer to store the HMAC-SHA256 version of message as byte array + * @param buffer_length length of buffer + * @param key buffer that store the key to run HMAC-SHA256 + * @param key_length length of the key + * @param message message that will be hashed + * @return byte array, equals to buffer + */ +static char *hmac256(char* buffer, unsigned int buffer_length, const char* key, unsigned int key_length, const char* message) +{ + if (zstr(key) || zstr(message) || buffer_length < SHA256_DIGEST_LENGTH) { + return NULL; + } + + HMAC(EVP_sha256(), + key, + (int)key_length, + (unsigned char *)message, + strlen(message), + (unsigned char*)buffer, + &buffer_length); + + return (char*)buffer; +} + + +/** + * Calculate HMAC-SHA256 hash of a message + * @param buffer buffer to store the HMAC-SHA256 version of the message as hex string + * @param key buffer that store the key to run HMAC-SHA256 + * @param key_length length of the key + * @param message message that will be hashed + * @return hex string that store the HMAC-SHA256 version of the message + */ +static char *hmac256_hex(char* buffer, const char* key, unsigned int key_length, const char* message) +{ + char hmac256_raw[SHA256_DIGEST_LENGTH] = { 0 }; + + if (hmac256(hmac256_raw, SHA256_DIGEST_LENGTH, key, key_length, message) == NULL) { + return NULL; + } + + for (unsigned int i = 0; i < SHA256_DIGEST_LENGTH; i++) + { + snprintf(buffer + i*2, 3, "%02x", (unsigned char)hmac256_raw[i]); + } + buffer[SHA256_DIGEST_LENGTH * 2] = '\0'; + + return buffer; +} + + +/** + * Calculate SHA256 hash of a message + * @param buffer buffer to store the SHA256 version of the message as hex string + * @param string string to be hashed + * @return hex string that store the SHA256 version of the message + */ +static char *sha256_hex(char* buffer, const char* string) +{ + unsigned char sha256_raw[SHA256_DIGEST_LENGTH] = { 0 }; + + SHA256((unsigned char*)string, strlen(string), sha256_raw); + + for (unsigned int i = 0; i < SHA256_DIGEST_LENGTH; i++) + { + snprintf(buffer + i*2, 3, "%02x", sha256_raw[i]); + } + buffer[SHA256_DIGEST_LENGTH * 2] = '\0'; + + return buffer; +} + + +/** + * Get current time_stamp. Example: 20190724T110316Z + * @param format format of the time in strftime format + * @param buffer buffer to store the result + * @param buffer_length length of buffer + * @return current time stamp + */ +static char *get_time(char* format, char* buffer, unsigned int buffer_length) +{ + switch_time_exp_t time; + switch_size_t size; + + switch_time_exp_gmt(&time, switch_time_now()); + + switch_strftime(buffer, &size, buffer_length, format, &time); + + return buffer; +} + + +/** + * Get signature key + * @param key_signing buffer to store signature key + * @param aws_s3_profile AWS profile + * @return key_signing + */ +static char* aws_s3_signature_key(char* key_signing, switch_aws_s3_profile* aws_s3_profile) { + + char key_date[SHA256_DIGEST_LENGTH]; + char key_region[SHA256_DIGEST_LENGTH]; + char key_service[SHA256_DIGEST_LENGTH]; + char* aws4_secret_access_key = switch_mprintf("AWS4%s", aws_s3_profile->access_key_secret); + + hmac256(key_date, SHA256_DIGEST_LENGTH, aws4_secret_access_key, strlen(aws4_secret_access_key), aws_s3_profile->date_stamp); + hmac256(key_region, SHA256_DIGEST_LENGTH, key_date, SHA256_DIGEST_LENGTH, aws_s3_profile->region); + hmac256(key_service, SHA256_DIGEST_LENGTH, key_region, SHA256_DIGEST_LENGTH, "s3"); + hmac256(key_signing, SHA256_DIGEST_LENGTH, key_service, SHA256_DIGEST_LENGTH, "aws4_request"); + + switch_safe_free(aws4_secret_access_key); + + return key_signing; } /** - * Create a pre-signed URL for AWS S3 - * @param verb (PUT/GET) - * @param url address (virtual-host-style) - * @param base_domain (optional - amazon aws assumed if not specified) - * @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 + * Get query string that will be put together with the signature + * @param aws_s3_profile AWS profile + * @return the query string (must be freed) */ -SWITCH_MOD_DECLARE(char *) aws_s3_presigned_url_create(const char *verb, const char *url, const char *base_domain, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires) +static char* aws_s3_standardized_query_string(switch_aws_s3_profile* aws_s3_profile) { - 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; + char* credential; + char expires[10]; + char* standardized_query_string; - /* create URL encoded signature */ - parse_url(url_dup, base_domain, "s3", &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); + credential = switch_mprintf("%s%%2F%s%%2F%s%%2Fs3%%2Faws4_request", aws_s3_profile->access_key_id, aws_s3_profile->date_stamp, aws_s3_profile->region); + switch_snprintf(expires, 9, "%ld", aws_s3_profile->expires); - /* create the presigned URL */ - return switch_mprintf("%s?Signature=%s&Expires=%s&AWSAccessKeyId=%s", url, signature_url_encoded, expires, aws_access_key_id); + standardized_query_string = switch_mprintf( + "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=%s&X-Amz-Date=%s&X-Amz-Expires=%s&X-Amz-SignedHeaders=host", + credential, aws_s3_profile->time_stamp, expires + ); + + switch_safe_free(credential); + + return standardized_query_string; } /** - * 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 base_domain (optional - amazon aws assumed if not specified) - * @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 + * Get request string that is used to build string to sign + * @param aws_s3_profile AWS profile + * @return the request string (must be freed) */ -static char *aws_s3_authentication_create(const char *verb, const char *url, const char *base_domain, 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; +static char* aws_s3_standardized_request(switch_aws_s3_profile* aws_s3_profile) { - /* create base64 encoded signature */ - parse_url(url_dup, base_domain, "s3", &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); + char* standardized_query_string = aws_s3_standardized_query_string(aws_s3_profile); - return switch_mprintf("AWS %s:%s", aws_access_key_id, signature); + char* standardized_request = switch_mprintf( + "%s\n/%s\n%s\nhost:%s.%s\n\nhost\nUNSIGNED-PAYLOAD", + aws_s3_profile->verb, aws_s3_profile->object, standardized_query_string, aws_s3_profile->bucket, aws_s3_profile->base_domain + ); + + switch_safe_free(standardized_query_string); + + return standardized_request; } + +/** + * Create the string to sign for a AWS signature version 4 + * @param standardized_request request string that is used to build string to sign + * @param aws_s3_profile AWS profile + * @return the string to sign (must be freed) + */ +static char *aws_s3_string_to_sign(char* standardized_request, switch_aws_s3_profile* aws_s3_profile) { + + char standardized_request_hex[SHA256_DIGEST_LENGTH * 2 + 1] = {'\0'}; + char* string_to_sign; + + sha256_hex(standardized_request_hex, standardized_request); + + string_to_sign = switch_mprintf( + "AWS4-HMAC-SHA256\n%s\n%s/%s/s3/aws4_request\n%s", + aws_s3_profile->time_stamp, aws_s3_profile->date_stamp, aws_s3_profile->region, standardized_request_hex + ); + + return string_to_sign; +} + +/** + * Create a full query string that contains signature version 4 for AWS request + * @param aws_s3_profile AWS profile + * @return full query string that include the signature + */ +static char *aws_s3_authentication_create(switch_aws_s3_profile* aws_s3_profile) { + char signature[SHA256_DIGEST_LENGTH * 2 + 1]; + char *string_to_sign; + + char* standardized_query_string; + char* standardized_request; + char signature_key[SHA256_DIGEST_LENGTH]; + char* query_param; + + // Get standardized_query_string + standardized_query_string = aws_s3_standardized_query_string(aws_s3_profile); + + // Get standardized_request + standardized_request = aws_s3_standardized_request(aws_s3_profile); + + // Get string_to_sign + string_to_sign = aws_s3_string_to_sign(standardized_request, aws_s3_profile); + + // Get signature_key + aws_s3_signature_key(signature_key, aws_s3_profile); + + // Get signature + hmac256_hex(signature, signature_key, SHA256_DIGEST_LENGTH, string_to_sign); + + // Build final query string + query_param = switch_mprintf("%s&X-Amz-Signature=%s", standardized_query_string, signature); + + switch_safe_free(string_to_sign); + switch_safe_free(standardized_query_string); + switch_safe_free(standardized_request); + + return query_param; +} +#endif + +/** + * Append Amazon S3 query params to request if necessary + * @param headers to add to. AWS signature v4 requires no header to be appended + * @param profile with S3 credentials + * @param content_type of object (PUT only) + * @param verb http methods (GET/PUT) + * @param url full url + * @param block_num block number, only used by Azure + * @param query_string pointer to query param string that will be calculated + * @return updated headers + */ +SWITCH_MOD_DECLARE(switch_curl_slist_t) *aws_s3_append_headers( + http_profile_t *profile, + switch_curl_slist_t *headers, + const char *verb, + unsigned int content_length, + const char *content_type, + const char *url, + const unsigned int block_num, + char **query_string +) { +#if defined(HAVE_OPENSSL) + switch_aws_s3_profile aws_s3_profile; + char* url_dup; + + // Get bucket and object name from url + switch_strdup(url_dup, url); + parse_url(url_dup, profile->base_domain, "s3", &aws_s3_profile.bucket, &aws_s3_profile.object); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "bucket: %s\n", aws_s3_profile.bucket); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "object: %s\n", aws_s3_profile.object); + + // Get date and time + get_time("%Y%m%d", aws_s3_profile.date_stamp, DATE_STAMP_LENGTH); + get_time("%Y%m%dT%H%M%SZ", aws_s3_profile.time_stamp, TIME_STAMP_LENGTH); + + // Get access key id and secret + aws_s3_profile.access_key_id = profile->aws_s3_access_key_id; + aws_s3_profile.access_key_secret = profile->secret_access_key; + + // Get base domain + aws_s3_profile.base_domain = profile->base_domain; + aws_s3_profile.region = profile->region; + aws_s3_profile.verb = verb; + aws_s3_profile.expires = profile->expires; + + *query_string = aws_s3_authentication_create(&aws_s3_profile); + + switch_safe_free(url_dup); +#endif + return headers; +} + +/** + * Get key id, secret and region from env variables or config file + * @param xml object that store config file + * @param profile pointer that config will be written to + * @return status + */ SWITCH_MOD_DECLARE(switch_status_t) aws_s3_config_profile(switch_xml_t xml, http_profile_t *profile) { - switch_status_t status = SWITCH_STATUS_SUCCESS; +#if defined(HAVE_OPENSSL) switch_xml_t base_domain_xml = switch_xml_child(xml, "base-domain"); + switch_xml_t region_xml = switch_xml_child(xml, "region"); + switch_xml_t expires_xml = switch_xml_child(xml, "expires"); + // Function pointer to be called to append query params to original url profile->append_headers_ptr = aws_s3_append_headers; /* check if environment variables set the keys */ profile->aws_s3_access_key_id = getenv("AWS_ACCESS_KEY_ID"); profile->secret_access_key = getenv("AWS_SECRET_ACCESS_KEY"); if (!zstr(profile->aws_s3_access_key_id) && !zstr(profile->secret_access_key)) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, - "Using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables for s3 access on profile \"%s\"\n", profile->name); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables for AWS S3 access for profile \"%s\"\n", profile->name); profile->aws_s3_access_key_id = strdup(profile->aws_s3_access_key_id); profile->secret_access_key = strdup(profile->secret_access_key); } else { /* use configuration for keys */ switch_xml_t id = switch_xml_child(xml, "access-key-id"); switch_xml_t secret = switch_xml_child(xml, "secret-access-key"); + if (!id || !secret) + { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Missing access-key-id or secret-access-key in http_cache.conf.xml for profile \"%s\"\n", profile->name); + return SWITCH_STATUS_FALSE; + } - if (id && secret) { - profile->aws_s3_access_key_id = switch_strip_whitespace(switch_xml_txt(id)); - profile->secret_access_key = switch_strip_whitespace(switch_xml_txt(secret)); - if (zstr(profile->aws_s3_access_key_id) || zstr(profile->secret_access_key)) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Missing AWS S3 credentials for profile \"%s\"\n", profile->name); - switch_safe_free(profile->aws_s3_access_key_id); - profile->aws_s3_access_key_id = NULL; - switch_safe_free(profile->secret_access_key); - profile->secret_access_key = NULL; - } - } else { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Missing key id or secret\n"); - status = SWITCH_STATUS_FALSE; + profile->aws_s3_access_key_id = switch_strip_whitespace(switch_xml_txt(id)); + profile->secret_access_key = switch_strip_whitespace(switch_xml_txt(secret)); + if (zstr(profile->aws_s3_access_key_id) || zstr(profile->secret_access_key)) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Empty access-key-id or secret-access-key in http_cache.conf.xml for profile \"%s\"\n", profile->name); + switch_safe_free(profile->aws_s3_access_key_id); + switch_safe_free(profile->secret_access_key); + return SWITCH_STATUS_FALSE; } } + // Get region + if (!region_xml) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Missing region in http_cache.conf.xml for profile \"%s\"\n", profile->name); + return SWITCH_STATUS_FALSE; + } + profile->region = switch_strip_whitespace(switch_xml_txt(region_xml)); + if (zstr(profile->region)) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Empty region in http_cache.conf.xml for profile \"%s\"\n", profile->name); + switch_safe_free(profile->region); + return SWITCH_STATUS_FALSE; + } + + // Get base domain for AWS S3 compatible services. Default base domain is s3.amazonaws.com if (base_domain_xml) { profile->base_domain = switch_strip_whitespace(switch_xml_txt(base_domain_xml)); if (zstr(profile->base_domain)) { switch_safe_free(profile->base_domain); - profile->base_domain = NULL; + profile->base_domain = switch_mprintf(DEFAULT_BASE_DOMAIN, profile->region); } + } else + { + profile->base_domain = switch_mprintf(DEFAULT_BASE_DOMAIN, profile->region); } - return status; + + // Get expire time for URL signature + if (expires_xml) { + char* expires = switch_strip_whitespace(switch_xml_txt(expires_xml)); + if (!zstr(expires) && switch_is_number(expires)) + { + profile->expires = switch_safe_atoi(expires, DEFAULT_EXPIRATION_TIME); + } else + { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Invalid \"expires\" in http_cache.conf.xml for profile \"%s\"\n", profile->name); + profile->expires = DEFAULT_EXPIRATION_TIME; + } + switch_safe_free(expires); + } else + { + profile->expires = DEFAULT_EXPIRATION_TIME; + } + +#endif + + return SWITCH_STATUS_SUCCESS; } -/** - * 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 - */ -SWITCH_MOD_DECLARE(switch_curl_slist_t*) aws_s3_append_headers(http_profile_t *profile, switch_curl_slist_t *headers, - const char *verb, unsigned int content_length, const char *content_type, const char *url, const unsigned int block_num, char **query_string) -{ - 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, profile->base_domain, content_type, "", profile->aws_s3_access_key_id, profile->secret_access_key, date); - snprintf(header, 1024, "Authorization: %s", authenticate); - free(authenticate); - headers = switch_curl_slist_append(headers, header); - - return headers; -} - - /* For Emacs: * Local Variables: * mode:c diff --git a/src/mod/applications/mod_http_cache/aws.h b/src/mod/applications/mod_http_cache/aws.h index 3112bfe989..5ae5b933fc 100644 --- a/src/mod/applications/mod_http_cache/aws.h +++ b/src/mod/applications/mod_http_cache/aws.h @@ -22,7 +22,8 @@ * * Contributor(s): * Chris Rienzo - * + * Quoc-Bao Nguyen + * * aws.h - Some Amazon Web Services helper functions * */ @@ -33,13 +34,27 @@ #include #include "common.h" -/* (SHA1_LENGTH * 1.37 base64 bytes per byte * 3 url-encoded bytes per byte) */ -#define S3_SIGNATURE_LENGTH_MAX 83 +#define DATE_STAMP_LENGTH 9 // 20190729 +#define TIME_STAMP_LENGTH 17 // 20190729T083832Z +#define DEFAULT_BASE_DOMAIN "s3.%s.amazonaws.com" +#define DEFAULT_EXPIRATION_TIME 604800 -SWITCH_MOD_DECLARE(switch_curl_slist_t*) aws_s3_append_headers(http_profile_t *profile, switch_curl_slist_t *headers, - const char *verb, unsigned int content_length, const char *content_type, const char *url, const unsigned int block_num, char **query_string); SWITCH_MOD_DECLARE(switch_status_t) aws_s3_config_profile(switch_xml_t xml, http_profile_t *profile); -SWITCH_MOD_DECLARE(char *) aws_s3_presigned_url_create(const char *verb, const char *url, const char *base_domain, const char *content_type, const char *content_md5, const char *aws_access_key_id, const char *aws_secret_access_key, const char *expires); + +struct aws_s3_profile { + const char* base_domain; + char* bucket; + char* object; + char time_stamp[TIME_STAMP_LENGTH]; + char date_stamp[DATE_STAMP_LENGTH]; + const char* verb; + const char* access_key_id; + const char* access_key_secret; + const char* region; + switch_time_t expires; +}; + +typedef struct aws_s3_profile switch_aws_s3_profile; #endif diff --git a/src/mod/applications/mod_http_cache/common.c b/src/mod/applications/mod_http_cache/common.c index 5db61fcdf9..04e66c7079 100644 --- a/src/mod/applications/mod_http_cache/common.c +++ b/src/mod/applications/mod_http_cache/common.c @@ -22,6 +22,7 @@ * * Contributor(s): * Chris Rienzo + * Quoc-Bao Nguyen * * common.c - Functions common to the store provider * @@ -75,6 +76,7 @@ SWITCH_MOD_DECLARE(void) parse_url(char *url, const char *base_domain, const cha *object = NULL; if (zstr(url)) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "url is empty\n"); return; } @@ -86,6 +88,7 @@ SWITCH_MOD_DECLARE(void) parse_url(char *url, const char *base_domain, const cha } if (zstr(bucket_start)) { /* invalid URL */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "invalid url\n"); return; } @@ -96,6 +99,7 @@ SWITCH_MOD_DECLARE(void) parse_url(char *url, const char *base_domain, const cha bucket_end = my_strrstr(bucket_start, base_domain_match); if (!bucket_end) { /* invalid URL */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "invalid url\n"); return; } @@ -104,12 +108,14 @@ SWITCH_MOD_DECLARE(void) parse_url(char *url, const char *base_domain, const cha object_start = strchr(bucket_end + 1, '/'); if (!object_start) { /* invalid URL */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "invalid url\n"); return; } object_start++; if (zstr(bucket_start) || zstr(object_start)) { /* invalid URL */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "invalid url\n"); return; } diff --git a/src/mod/applications/mod_http_cache/common.h b/src/mod/applications/mod_http_cache/common.h index 612b7fb2ad..9092f2a13b 100644 --- a/src/mod/applications/mod_http_cache/common.h +++ b/src/mod/applications/mod_http_cache/common.h @@ -1,10 +1,40 @@ +/* + * aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2013-2014, 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 + * Quoc-Bao Nguyen + * + * common.h - Functions common to the store provider + * + */ + #ifndef COMMON_H #define COMMON_H #include /** - * An http profile. Defines optional credentials + * An http profile. Defines optional credentials * for access to Amazon S3 and Azure Blob Service */ struct http_profile { @@ -12,6 +42,8 @@ struct http_profile { char *aws_s3_access_key_id; char *secret_access_key; char *base_domain; + char *region; // AWS region. Used by AWS S3 + switch_time_t expires; // Expiration time in seconds for URL signature. Default is 604800 seconds. Used by AWS S3 switch_size_t bytes_per_block; // function to be called to add the profile specific headers to the GET/PUT requests 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 1408e550ac..bf14470ecb 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 @@ -1,47 +1,74 @@ + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 604800 + - - - - - - - - - - - - - - - - - - - - - kOOY4Y/sqZU9bsLjmN+9McVwTry+UIn1Owt4Zs/2S2FQT0eAWLKsk -Z0V6/gGFqCAKVvwXoGjqUn7PNbVjhZiNA== - - - - - - + + + + + + + + + + + + + + + + + + + + 604800 + + + + + + + + + + + + + kOOY4Y/sqZU9bsLjmN+9McVwTry+UIn1Owt4Zs/2S2FQT0eAWLKskZ0V6/gGFqCAKVvwXoGjqUn7PNbVjhZiNA== + + + + + + - diff --git a/src/mod/applications/mod_http_cache/test/s3_auth.py b/src/mod/applications/mod_http_cache/test/s3_auth.py new file mode 100755 index 0000000000..e346c5af8e --- /dev/null +++ b/src/mod/applications/mod_http_cache/test/s3_auth.py @@ -0,0 +1,144 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- +# +# s3_auth.py for unit tests in mod_http_cache +# Copyright (C) 2019 Vinadata Corporation (vinadata.vn). All rights reserved. +# +# 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 Initial Developer of the Original Code is Quoc-Bao Nguyen +# Portions created by the Initial Developer are Copyright (C) +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Quoc-Bao Nguyen +# +# s3_auth.python - Generate signature for AWS Signature version 4 for unit test +# + +import base64 +import datetime +import hashlib +import hmac +from collections import OrderedDict + +import requests +from requests.utils import quote + + +# hashing methods +def hmac256(key, msg): + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() + + +def hmac256_hex(key, msg): + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).hexdigest() + + +def sha256_hex(msg): + return hashlib.sha256(msg).hexdigest() + + +# region is a wildcard value that takes the place of the AWS region value +# as COS doesn't use regions like AWS, this parameter can accept any string +def createSignatureKey(key, date_stamp, region, service): + keyDate = hmac256(('AWS4' + key).encode('utf-8'), date_stamp) + keyRegion = hmac256(keyDate, region) + keyService = hmac256(keyRegion, service) + keySigning = hmac256(keyService, 'aws4_request') + return keySigning + + +def query_string(access_key, date_stamp, time_stamp, region): + fields = OrderedDict() + fields["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256" + fields["X-Amz-Credential"] = access_key + '/' + date_stamp + '/' + region + '/s3/aws4_request' + fields["X-Amz-Date"] = time_stamp + fields["X-Amz-Expires"] = "604800" # in seconds + fields["X-Amz-SignedHeaders"] = "host" + + queries_string = ''.join("%s=%s&" % (key, val) for (key, val) in fields.iteritems())[:-1] + + return quote(queries_string, safe='&=') + + +def main(): + access_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + secret_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + + # request elements + http_method = 'GET' + region = 'HCM' + bucket = 'bucket1' + host = 'stg.example.com' + endpoint = 'https://' + bucket + "." + host + object_name = 'document.docx' + + # assemble the standardized request + time = datetime.datetime.utcnow() + time_stamp = time.strftime('%Y%m%dT%H%M%SZ') + date_stamp = time.strftime('%Y%m%d') + + print "time_stamp: " + time_stamp + print "date_stamp: " + date_stamp + + standardized_query_string = query_string(access_key, date_stamp, time_stamp, region) + print 'standardized_query_string: \n' + standardized_query_string + + standardized_request = (http_method + '\n' + + '/' + object_name + '\n' + + standardized_query_string + '\n' + + 'host:' + bucket + '.' + host + '\n\n' + + 'host' + '\n' + + 'UNSIGNED-PAYLOAD') + + print 'standardized_request: ' + hashlib.sha256(standardized_request).hexdigest() + + print "\nStandardized request:\n" + standardized_request + + # assemble string-to-sign + string_to_sign = ('AWS4-HMAC-SHA256' + '\n' + + time_stamp + '\n' + + date_stamp + '/' + region + '/s3/aws4_request' + '\n' + + sha256_hex(standardized_request)) + + print "\nString to Sign:\n" + string_to_sign.replace('\n', "\\n") + + # generate the signature + signature_key = createSignatureKey(secret_key, date_stamp, region, 's3') + + print 'signature_key: ' + base64.b64encode(signature_key) + # signature = hmac.new(signature_key, sts.encode('utf-8'), hashlib.sha256).hexdigest() + signature = hmac256_hex(signature_key, string_to_sign) + + print 'signature: ' + signature + # create and send the request + # the 'requests' package automatically adds the required 'host' header + + request_url = (endpoint + '/' + + object_name + '?' + + standardized_query_string + + '&X-Amz-Signature=' + + signature) + + print '\nRequest URL:\n' + request_url + + request = requests.get(request_url) + + print '\nResponse code: %d\n' % request.status_code + # print '\nResponse code: %s\n' % request.content + # print request.text + + +if __name__ == "__main__": + main() diff --git a/src/mod/applications/mod_http_cache/test/test_aws.c b/src/mod/applications/mod_http_cache/test/test_aws.c index 615f336472..bc857af067 100644 --- a/src/mod/applications/mod_http_cache/test/test_aws.c +++ b/src/mod/applications/mod_http_cache/test/test_aws.c @@ -1,12 +1,66 @@ +/* + * aws.h for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2013-2014, 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 + * Quoc-Bao Nguyen + * + * test_aws.c - Unit tests for functions in aws.c + * + */ #include #include #include "../aws.c" +// Run test +// make && libtool --mode=execute valgrind --leak-check=full --log-file=vg.log ./test/test_aws && cat vg.log + FST_BEGIN() { + FST_SUITE_BEGIN(aws) { + +#if defined(HAVE_OPENSSL) + char url[100] = {'\0'}; + switch_aws_s3_profile aws_s3_profile; + + // Get bucket and object name from url + aws_s3_profile.bucket = "bucket6"; + aws_s3_profile.object = "document.docx"; + memcpy(aws_s3_profile.date_stamp, "20190729", DATE_STAMP_LENGTH); + memcpy(aws_s3_profile.time_stamp, "20190729T083832Z", TIME_STAMP_LENGTH); + aws_s3_profile.access_key_id = "cbc443a53fb06eafb2b83ca1e4233cbc"; + aws_s3_profile.access_key_secret = "4a722120f27518abbb8573ca9005d175"; + + aws_s3_profile.base_domain = "stg.vinadata.vn"; + aws_s3_profile.region = "HCM"; + aws_s3_profile.verb = "GET"; + aws_s3_profile.expires = DEFAULT_EXPIRATION_TIME; + + switch_snprintf(url, sizeof(url), "http://%s.%s/%s", aws_s3_profile.bucket, aws_s3_profile.base_domain, aws_s3_profile.object); +#endif + FST_SETUP_BEGIN() { } @@ -17,153 +71,272 @@ FST_TEARDOWN_BEGIN() } FST_TEARDOWN_END() -FST_TEST_BEGIN(test_string_to_sign) -{ - char *string_to_sign = NULL; - string_to_sign = aws_s3_string_to_sign("GET", "rienzo-vault", "troporocks.mp3", "", "", "Fri, 17 May 2013 19:35:26 GMT") ; - fst_check_string_equals("GET\n\n\nFri, 17 May 2013 19:35:26 GMT\n/rienzo-vault/troporocks.mp3", string_to_sign); - switch_safe_free(string_to_sign); - - string_to_sign = aws_s3_string_to_sign("GET", "foo", "man.chu", "audio/mpeg", "c8fdb181845a4ca6b8fec737b3581d76", "Thu, 17 Nov 2005 18:49:58 GMT"); - fst_check_string_equals("GET\nc8fdb181845a4ca6b8fec737b3581d76\naudio/mpeg\nThu, 17 Nov 2005 18:49:58 GMT\n/foo/man.chu", string_to_sign); - switch_safe_free(string_to_sign); - - string_to_sign = aws_s3_string_to_sign("", "", "", "", "", ""); - fst_check_string_equals("\n\n\n\n//", string_to_sign); - switch_safe_free(string_to_sign); - - string_to_sign = aws_s3_string_to_sign(NULL, NULL, NULL, NULL, NULL, NULL); - fst_check_string_equals("\n\n\n\n//", string_to_sign); - switch_safe_free(string_to_sign); - - string_to_sign = aws_s3_string_to_sign("PUT", "bucket", "voicemails/recording.wav", "audio/wav", "", "Wed, 12 Jun 2013 13:16:58 GMT"); - fst_check_string_equals("PUT\n\naudio/wav\nWed, 12 Jun 2013 13:16:58 GMT\n/bucket/voicemails/recording.wav", string_to_sign); - switch_safe_free(string_to_sign); -} -FST_TEST_END() - -FST_TEST_BEGIN(test_signature) -{ - char signature[S3_SIGNATURE_LENGTH_MAX]; - signature[0] = '\0'; - fst_check_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")); - fst_check_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")); - fst_check_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")); - fst_check_string_equals("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); - fst_check_string_equals("OKA87rVp3c4kd59t8D3diFmTfuo=", aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV")); - fst_check(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", "") == NULL); - fst_check(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, "", "") == NULL); - fst_check(aws_s3_signature(signature, S3_SIGNATURE_LENGTH_MAX, NULL, NULL) == NULL); - fst_check(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") == NULL); - fst_check(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") == NULL); - fst_check_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")); -} -FST_TEST_END() - -FST_TEST_BEGIN(test_parse_url) +#if defined(HAVE_OPENSSL) +FST_TEST_BEGIN(parse_url) { char *bucket; char *object; - char url[512] = { 0 }; + char url_dup[512] = { 0 }; - snprintf(url, sizeof(url), "http://quotes.s3.amazonaws.com/nelson"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), url); + parse_url(url_dup, aws_s3_profile.base_domain, "s3", &bucket, &object); + fst_check_string_equals(aws_s3_profile.bucket, bucket); + fst_check_string_equals(aws_s3_profile.object, object); + + switch_snprintf(url_dup, sizeof(url_dup), "https://bucket99.s3.amazonaws.com/image.png"); + parse_url(url_dup, NULL, "s3", &bucket, &object); + fst_check_string_equals(bucket, "bucket99"); + fst_check_string_equals(object, "image.png"); + + switch_snprintf(url_dup, sizeof(url_dup), "https://bucket99.s3.amazonaws.com/folder5/image.png"); + parse_url(url_dup, NULL, "s3", &bucket, &object); + fst_check_string_equals(bucket, "bucket99"); + fst_check_string_equals(object, "folder5/image.png"); + + switch_snprintf(url_dup, sizeof(url_dup), "https://bucket23.vn-hcm.vinadata.vn/image.png"); + parse_url(url_dup, "vn-hcm.vinadata.vn", "s3", &bucket, &object); + fst_check_string_equals(bucket, "bucket23"); + fst_check_string_equals(object, "image.png"); + + switch_snprintf(url_dup, sizeof(url_dup), "https://bucket335.s3-ap-southeast-1.amazonaws.com/vpnclient-v4.29-9680-rtm-2019.02.28-linux-x64-64bit.tar.gz"); + parse_url(url_dup, NULL, "s3", &bucket, &object); + fst_check_string_equals(bucket, "bucket335"); + fst_check_string_equals(object, "vpnclient-v4.29-9680-rtm-2019.02.28-linux-x64-64bit.tar.gz"); + + switch_snprintf(url_dup, sizeof(url_dup), "https://bucket335.s3-ap-southeast-1.amazonaws.com/vpnclient-v4.29-9680-rtm-2019.02.28-linux-x64-64bit.tar.gz"); + parse_url(url_dup, "s3-ap-southeast-1.amazonaws.com", "s3", &bucket, &object); + fst_check_string_equals(bucket, "bucket335"); + fst_check_string_equals(object, "vpnclient-v4.29-9680-rtm-2019.02.28-linux-x64-64bit.tar.gz"); + + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes.s3.amazonaws.com/nelson"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check_string_equals("quotes", bucket); fst_check_string_equals("nelson", object); - snprintf(url, sizeof(url), "https://quotes.s3.amazonaws.com/nelson.mp3"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "https://quotes.s3.amazonaws.com/nelson.mp3"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check_string_equals("quotes", bucket); fst_check_string_equals("nelson.mp3", object); - snprintf(url, sizeof(url), "http://s3.amazonaws.com/quotes/nelson"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://s3.amazonaws.com/quotes/nelson"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "http://quotes/quotes/nelson"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes/quotes/nelson"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "http://quotes.s3.amazonaws.com/"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes.s3.amazonaws.com/"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "http://quotes.s3.amazonaws.com"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes.s3.amazonaws.com"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "http://quotes"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "%s", ""); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "%s", ""); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - parse_url(NULL, NULL, "s3", &bucket, &object); + switch_snprintf(NULL, 0, "s3", &bucket, &object); fst_check(bucket == NULL); fst_check(object == NULL); - snprintf(url, sizeof(url), "http://bucket.s3.amazonaws.com/voicemails/recording.wav"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://bucket.s3.amazonaws.com/voicemails/recording.wav"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check_string_equals("bucket", bucket); fst_check_string_equals("voicemails/recording.wav", object); - snprintf(url, sizeof(url), "https://my-bucket-with-dash.s3-us-west-2.amazonaws.com/greeting/file/1002/Lumino.mp3"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "https://my-bucket-with-dash.s3-us-west-2.amazonaws.com/greeting/file/1002/Lumino.mp3"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check_string_equals("my-bucket-with-dash", bucket); fst_check_string_equals("greeting/file/1002/Lumino.mp3", object); - snprintf(url, sizeof(url), "http://quotes.s3.foo.bar.s3.amazonaws.com/greeting/file/1002/Lumino.mp3"); - parse_url(url, NULL, "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes.s3.foo.bar.s3.amazonaws.com/greeting/file/1002/Lumino.mp3"); + parse_url(url_dup, NULL, "s3", &bucket, &object); fst_check_string_equals("quotes.s3.foo.bar", bucket); fst_check_string_equals("greeting/file/1002/Lumino.mp3", object); - snprintf(url, sizeof(url), "http://quotes.s3.foo.bar.example.com/greeting/file/1002/Lumino.mp3"); - parse_url(url, "example.com", "s3", &bucket, &object); + switch_snprintf(url_dup, sizeof(url_dup), "http://quotes.s3.foo.bar.example.com/greeting/file/1002/Lumino.mp3"); + parse_url(url_dup, "example.com", "s3", &bucket, &object); fst_check_string_equals("quotes.s3.foo.bar", bucket); fst_check_string_equals("greeting/file/1002/Lumino.mp3", object); } FST_TEST_END() -FST_TEST_BEGIN(test_authorization_header) +FST_TEST_BEGIN(aws_s3_standardized_query_string) { - char *authentication_header = aws_s3_authentication_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", NULL, "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("AWS AKIAIOSFODNN7EXAMPLE:YJkomOaqUJlvEluDq4fpusID38Y=", authentication_header); - switch_safe_free(authentication_header); - - authentication_header = aws_s3_authentication_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "s3.amazonaws.com", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("AWS AKIAIOSFODNN7EXAMPLE:YJkomOaqUJlvEluDq4fpusID38Y=", authentication_header); - switch_safe_free(authentication_header); - - authentication_header = aws_s3_authentication_create("GET", "https://vault.example.com/awesome.mp3", "example.com", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("AWS AKIAIOSFODNN7EXAMPLE:YJkomOaqUJlvEluDq4fpusID38Y=", authentication_header); - switch_safe_free(authentication_header); + char* standardized_query_string = aws_s3_standardized_query_string(&aws_s3_profile); + fst_check_string_equals("X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=cbc443a53fb06eafb2b83ca1e4233cbc%2F20190729%2FHCM%2Fs3%2Faws4_request&X-Amz-Date=20190729T083832Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host", standardized_query_string); + switch_safe_free(standardized_query_string); } FST_TEST_END() -FST_TEST_BEGIN(test_presigned_url) +FST_TEST_BEGIN(get_time) { - char *presigned_url = aws_s3_presigned_url_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", NULL, "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("https://vault.s3.amazonaws.com/awesome.mp3?Signature=YJkomOaqUJlvEluDq4fpusID38Y%3D&Expires=1234567890&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE", presigned_url); - switch_safe_free(presigned_url); + char time_stamp[TIME_STAMP_LENGTH]; + char date_stamp[DATE_STAMP_LENGTH]; + char time_stamp_test[TIME_STAMP_LENGTH]; + char date_stamp_test[DATE_STAMP_LENGTH]; - presigned_url = aws_s3_presigned_url_create("GET", "https://vault.s3.amazonaws.com/awesome.mp3", "s3.amazonaws.com", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("https://vault.s3.amazonaws.com/awesome.mp3?Signature=YJkomOaqUJlvEluDq4fpusID38Y%3D&Expires=1234567890&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE", presigned_url); - switch_safe_free(presigned_url); + // Get date and time for test case + time_t rawtime; + struct tm * timeinfo; + time(&rawtime); + timeinfo = gmtime(&rawtime); - presigned_url = aws_s3_presigned_url_create("GET", "https://vault.example.com/awesome.mp3", "example.com", "audio/mpeg", "", "AKIAIOSFODNN7EXAMPLE", "0123456789012345678901234567890123456789", "1234567890"); - fst_check_string_equals("https://vault.example.com/awesome.mp3?Signature=YJkomOaqUJlvEluDq4fpusID38Y%3D&Expires=1234567890&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE", presigned_url); - switch_safe_free(presigned_url); + // Get date and time to test + get_time("%Y%m%d", date_stamp, DATE_STAMP_LENGTH); + get_time("%Y%m%dT%H%M%SZ", time_stamp, TIME_STAMP_LENGTH); + + // https://fresh2refresh.com/c-programming/c-time-related-functions/ + // https://stackoverflow.com/questions/5141960/get-the-current-time-in-c/5142028 + // https://linux.die.net/man/3/ctime + // https://stackoverflow.com/questions/153890/printing-leading-0s-in-c + switch_snprintf(date_stamp_test, DATE_STAMP_LENGTH, "%d%02d%02d", timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday); + switch_snprintf(time_stamp_test, TIME_STAMP_LENGTH, "%d%02d%02dT%02d%02d%02dZ", timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday, timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec); + + fst_check_string_equals(time_stamp_test, time_stamp); + fst_check_string_equals(date_stamp_test, date_stamp); } FST_TEST_END() +FST_TEST_BEGIN(hmac256_hex) +{ + char hex[SHA256_DIGEST_LENGTH * 2 + 1]; + + fst_check_string_equals("61d8c60f9c2cd767d3db37a52966965ef508136693d99ea533cff1b712044653", hmac256_hex(hex, "d8a1c4f68b15844de5d07960a57b1669", SHA256_DIGEST_LENGTH, "27a7d569d0c12cc576f20665651fb72c")); + fst_check_string_equals("5a98f20477a538bd29f0903cc30accaf4151b22e1f44577b75bae4cc5068df9e", hmac256_hex(hex, "66b0d6c5b3fd9c57a345b03877c902cb", SHA256_DIGEST_LENGTH, "2da091ff2a9818ce6deb5c4b6d9ad51c")); + fst_check_string_equals("6accbbef08f240dbdebf154cda91f7c66ef178023d53db7f3656d204996effaa", hmac256_hex(hex, "820f6b29b5ca8fa1077b69edf4ee456f", SHA256_DIGEST_LENGTH, "063ee28c963df34342ffb7ac0feae1d9")); +} +FST_TEST_END() + +FST_TEST_BEGIN(sha256_hex) +{ + char hex[SHA256_DIGEST_LENGTH * 2 + 1]; + + fst_check_string_equals("ebab701faffb9cd018d7fa566ca0e7f55dd7a9850cae06e088554238d6fae257", sha256_hex(hex, "eccbb6195a0f08664e2a35c0d686e892")); + fst_check_string_equals("4884c0be257758ded0381f940870a9280b367002e5c518fb42d56641b451a66b", sha256_hex(hex, "1993f63438fe482cd3040aeb2390b98c")); + fst_check_string_equals("1c930bd8e5034a418fef94b1cb753ec82b2a510429bfcdf41b597c6f6c7b21e4", sha256_hex(hex, "3705c5709dc52a04d844ebbcf59e7672")); +} +FST_TEST_END() + +FST_TEST_BEGIN(aws_s3_standardized_request) +{ + char* aws_s3_standardized_request_str = aws_s3_standardized_request(&aws_s3_profile); + + fst_check_string_equals("GET\n/document.docx\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=cbc443a53fb06eafb2b83ca1e4233cbc%2F20190729%2FHCM%2Fs3%2Faws4_request&X-Amz-Date=20190729T083832Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host\nhost:bucket6.stg.vinadata.vn\n\nhost\nUNSIGNED-PAYLOAD", aws_s3_standardized_request_str); + switch_safe_free(aws_s3_standardized_request_str); +} +FST_TEST_END() + +FST_TEST_BEGIN(aws_s3_string_to_sign) +{ + char* aws_s3_standardized_request_str = aws_s3_standardized_request(&aws_s3_profile); + char* aws_s3_string_to_sign_str = aws_s3_string_to_sign(aws_s3_standardized_request_str, &aws_s3_profile); + + fst_check_string_equals("AWS4-HMAC-SHA256\n20190729T083832Z\n20190729/HCM/s3/aws4_request\n945cd2782c8685f5b2472873252fa048eaa37cf8b132ef667bd98b6ad33238ac", aws_s3_string_to_sign_str); + + switch_safe_free(aws_s3_standardized_request_str); + switch_safe_free(aws_s3_string_to_sign_str); +} +FST_TEST_END() + +FST_TEST_BEGIN(aws_s3_signature_key) +{ + char signature_key[SHA256_DIGEST_LENGTH]; + unsigned int aws_s3_signature_key_b64_size = SHA256_DIGEST_LENGTH * 4 / 3 + 5; + unsigned char* aws_s3_signature_key_b64 = (unsigned char*)malloc(aws_s3_signature_key_b64_size); + char* aws_s3_signature_key_buffer = aws_s3_signature_key(signature_key, &aws_s3_profile); + + switch_b64_encode((unsigned char*)aws_s3_signature_key_buffer, SHA256_DIGEST_LENGTH, aws_s3_signature_key_b64, aws_s3_signature_key_b64_size); + fst_check_string_equals("2TBIZBxK1k+qh/pvEs0d2iNQ4SSX63o/8pLzzFPeA7c=", (char*)aws_s3_signature_key_b64); + + switch_safe_free(aws_s3_signature_key_b64); +} +FST_TEST_END() + +FST_TEST_BEGIN(aws_s3_authentication_create) +{ + char* query_param = aws_s3_authentication_create(&aws_s3_profile); + + fst_check_string_equals("X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=cbc443a53fb06eafb2b83ca1e4233cbc%2F20190729%2FHCM%2Fs3%2Faws4_request&X-Amz-Date=20190729T083832Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=3d0e5c18e85440a6cd38bdf8b3d07476fe6f98b8456a39ec401d1c628ce19175", query_param); + + switch_safe_free(query_param); +} +FST_TEST_END() + +FST_TEST_BEGIN(parse_xml_config_with_aws) +{ + switch_xml_t cfg, profiles, profile, aws_s3_profile; + http_profile_t http_profile; + int fd; + int i = 0; + + printf("\n"); + + fd = open("test_aws_http_cache.conf.xml", O_RDONLY); + if (fd < 0) { + fd = open("test/test_aws_http_cache.conf.xml", O_RDONLY); + } + fst_check(fd > 0); + + cfg = switch_xml_parse_fd(fd); + fst_check(cfg != NULL); + + profiles = switch_xml_child(cfg, "profiles"); + fst_check(profiles); + + for (profile = switch_xml_child(profiles, "profile"); profile; profile = profile->next) { + const char *name = NULL; + i++; + + fst_check(profile); + + name = switch_xml_attr_soft(profile, "name"); + printf("testing profile name: %s\n", name); + fst_check(name); + + http_profile.name = name; + http_profile.aws_s3_access_key_id = NULL; + http_profile.secret_access_key = NULL; + http_profile.base_domain = NULL; + http_profile.region = NULL; + http_profile.append_headers_ptr = NULL; + + aws_s3_profile = switch_xml_child(profile, "aws-s3"); + fst_check(aws_s3_profile); + + fst_check(aws_s3_config_profile(aws_s3_profile, &http_profile) == SWITCH_STATUS_SUCCESS); + + fst_check(!zstr(http_profile.region)); + fst_check(!zstr(http_profile.aws_s3_access_key_id)); + fst_check(!zstr(http_profile.secret_access_key)); + printf("base domain: %s\n", http_profile.base_domain); + fst_check(!zstr(http_profile.base_domain)); + switch_safe_free(http_profile.region); + switch_safe_free(http_profile.aws_s3_access_key_id); + switch_safe_free(http_profile.secret_access_key); + switch_safe_free(http_profile.base_domain); + } + + fst_check(i == 2); // test data contain two config + + switch_xml_free(cfg); +} +FST_TEST_END() +#endif + } FST_SUITE_END() diff --git a/src/mod/applications/mod_http_cache/test/test_aws_http_cache.conf.xml b/src/mod/applications/mod_http_cache/test/test_aws_http_cache.conf.xml new file mode 100644 index 0000000000..c21955ee21 --- /dev/null +++ b/src/mod/applications/mod_http_cache/test/test_aws_http_cache.conf.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 604800 + + + + + + + + + + + + + + + + + + + + + + + 604800 + + + + + + + + + +