* FS-9676 [mod_http_cache] Support for AWS Signature version 4 and drop support for version 2
Co-authored-by: baonq-me <quocbao747@gmail.com>
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
+ * Quoc-Bao Nguyen <baonq5@vng.com.vn>
*
* aws.c -- Some Amazon Web Services helper functions
*
#include <openssl/sha.h>
#endif
-/* 160 bits / 8 bits per byte */
-#define SHA1_LENGTH 20
+
+#if defined(HAVE_OPENSSL)
+#include <openssl/hmac.h>
+#include <openssl/sha.h>
+#endif
+
+#if defined(HAVE_OPENSSL)
/**
- * 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)
+ * 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 *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)
+static char *hmac256(char* buffer, unsigned int buffer_length, const char* key, unsigned int key_length, const char* message)
{
- /*
- * String to sign has the following format:
- * <HTTP-VERB>\n<Content-MD5>\n<Content-Type>\n<Expires/Date>\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);
+ 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;
}
+
/**
- * 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
+ * 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 *aws_s3_signature(char *signature, int signature_length, const char *string_to_sign, const char *aws_secret_access_key)
+static char *hmac256_hex(char* buffer, const char* key, unsigned int key_length, const char* message)
{
-#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) {
+ char hmac256_raw[SHA256_DIGEST_LENGTH] = { 0 };
+
+ if (hmac256(hmac256_raw, SHA256_DIGEST_LENGTH, key, key_length, message) == NULL) {
return NULL;
}
- if (zstr(aws_secret_access_key)) {
- return NULL;
+
+ for (unsigned int i = 0; i < SHA256_DIGEST_LENGTH; i++)
+ {
+ snprintf(buffer + i*2, 3, "%02x", (unsigned char)hmac256_raw[i]);
}
- if (!string_to_sign) {
- string_to_sign = "";
+ 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]);
}
- 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);
-#endif
- return signature;
+ buffer[SHA256_DIGEST_LENGTH * 2] = '\0';
+
+ return buffer;
}
+
/**
- * 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 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
*/
-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 *get_time(char* format, char* buffer, unsigned int buffer_length)
{
- 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 */
- 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);
-
- /* create the presigned URL */
- return switch_mprintf("%s?Signature=%s&Expires=%s&AWSAccessKeyId=%s", url, signature_url_encoded, expires, aws_access_key_id);
+ 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 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 query string that will be put together with the signature
+ * @param aws_s3_profile AWS profile
+ * @return the query 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)
+static char* aws_s3_standardized_query_string(switch_aws_s3_profile* aws_s3_profile)
{
- char signature[S3_SIGNATURE_LENGTH_MAX];
+ char* credential;
+ char expires[10];
+ char* standardized_query_string;
+
+ 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);
+
+ 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;
+}
+
+/**
+ * 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_standardized_request(switch_aws_s3_profile* aws_s3_profile) {
+
+ char* standardized_query_string = aws_s3_standardized_query_string(aws_s3_profile);
+
+ 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 *url_dup = strdup(url);
- char *bucket;
- char *object;
-
- /* 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);
-
- return switch_mprintf("AWS %s:%s", aws_access_key_id, signature);
+
+ 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;
-}
-
-/**
- * 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);
+ // 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;
+ }
- /* 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);
+#endif
- return headers;
+ return SWITCH_STATUS_SUCCESS;
}
-
/* For Emacs:
* Local Variables:
* mode:c
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
- *
+ * Quoc-Bao Nguyen <baonq5@vng.com.vn>
+ *
* aws.h - Some Amazon Web Services helper functions
*
*/
#include <switch_curl.h>
#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
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
+ * Quoc-Bao Nguyen <baonq5@vng.com.vn>
*
* common.c - Functions common to the store provider
*
*object = NULL;
if (zstr(url)) {
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "url is empty\n");
return;
}
}
if (zstr(bucket_start)) { /* invalid URL */
+ switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "invalid url\n");
return;
}
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;
}
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;
}
+/*
+ * 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 <chris.rienzo@grasshopper.com>
+ * Quoc-Bao Nguyen <baonq5@vng.com.vn>
+ *
+ * common.h - Functions common to the store provider
+ *
+ */
+
#ifndef COMMON_H
#define COMMON_H
#include <switch.h>
/**
- * An http profile. Defines optional credentials
+ * An http profile. Defines optional credentials
* for access to Amazon S3 and Azure Blob Service
*/
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
+<?xml version="1.0"?>
<configuration name="http_cache.conf" description="HTTP GET cache">
- <settings>
- <!-- set to true if you want to enable http:// and https:// formats. Do not use if mod_httapi is also loaded -->
- <param name="enable-file-formats" value="false"/>
- <param name="max-urls" value="10000"/>
- <param name="location" value="$${base_dir}/http_cache"/>
- <param name="default-max-age" value="86400"/>
- <param name="ssl-cacert" value="$${base_dir}/conf/cacert.pem"/>
- <param name="ssl-verifyhost" value="true"/>
- <param name="ssl-verifypeer" value="true"/>
- <!-- default is 300 seconds, override here -->
- <!--param name="connect-timeout" value="300"/-->
- <!-- default is 300 seconds, override here -->
- <!--param name="download-timeout" value="300"/-->
- </settings>
+ <settings>
+ <!-- set to true if you want to enable http:// and https:// formats. Do not use if mod_httapi is also loaded -->
+ <param name="enable-file-formats" value="false"/>
+ <param name="max-urls" value="10000"/>
+ <param name="location" value="$${base_dir}/http_cache"/>
+ <param name="default-max-age" value="86400"/>
+ <param name="ssl-cacert" value="$${base_dir}/conf/cacert.pem"/>
+ <param name="ssl-verifyhost" value="true"/>
+ <param name="ssl-verifypeer" value="true"/>
+ <!-- default is 300 seconds, override here -->
+ <!--param name="connect-timeout" value="300"/-->
+ <!-- default is 300 seconds, override here -->
+ <!--param name="download-timeout" value="300"/-->
+ </settings>
+ <profiles>
+ <profile name="s3">
+ <!-- Credentials for AWS account. -->
+ <aws-s3>
+ <!-- Required: Key identifier -->
+ <access-key-id><![CDATA[AKIAIOSFODNN7EXAMPLE]]></access-key-id>
+ <!-- Required: Key secret -->
+ <secret-access-key><![CDATA[wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY]]></secret-access-key>
+ <!-- Optional: Backup folder to save uploaded file in case of failure (for example recording a .wav file to a webserver). Must ended without a slash !-->
+ <!-- If you want to use your own s3-compatible service, base domain MUST be set -->
+ <!--<base-domain><![CDATA[stg.vinadata.vn]]></base-domain>-->
+ <!-- Required: AWS region -->
+ <region><![CDATA[ap-southeast-1]]></region> <!-- base domain is s3-ap-southeast-1.amazonaws.com -->
+ <!-- Optional: Expiration time in seconds for URL signature. Default is 604800s -->
+ <expires>604800</expires>
+ </aws-s3>
- <profiles>
- <profile name="s3">
- <!-- Credentials for AWS account. -->
- <aws-s3>
- <!-- 20 character key identifier, can override with AWS_ACCESS_KEY_ID environment variable -->
- <access-key-id><![CDATA[AKIAIOSFODNN7EXAMPLE]]></access-key-id>
- <!-- 40 character secret, can override with AWS_SECRET_ACCESS_KEY environment variable -->
- <secret-access-key><![CDATA[wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY]]></secret-access-key>
- <!--base-url><![CDATA[s3.example.com]]></base-url-->
- </aws-s3>
- <!-- Domains that this profile applies to -->
- <domains>
- <domain name="bucket.s3.amazonaws.com"/>
- <domain name="bucket2.s3.amazonaws.com"/>
- </domains>
- </profile>
-
- <profile name="blob">
- <azure-blob>
- <!-- key identifier, can override with AZURE_STORAGE_ACCESS_KEY environment variable -->
- <secret-access-key>kOOY4Y/sqZU9bsLjmN+9McVwTry+UIn1Owt4Zs/2S2FQT0eAWLKsk
-Z0V6/gGFqCAKVvwXoGjqUn7PNbVjhZiNA==</secret-access-key>
- </azure-blob>
- <domains>
- <domain name="account.blob.core.windows.net"/>
- </domains>
- </profile>
- </profiles>
+ <!-- Required: Domains that this profile will automatically be applied. Domains must have the same base domain (s3-ap-southeast-1.amazonaws.com) -->
+ <domains>
+ <domain name="bucket1.s3-ap-southeast-1.amazonaws.com"/>
+ <domain name="bucket2.s3-ap-southeast-1.amazonaws.com"/>
+ </domains>
+ </profile>
+ <profile name="s3-compatible">
+ <!-- Credentials for AWS account or any account on s3-like storage service -->
+ <aws-s3>
+ <!-- Required: Key identifier -->
+ <access-key-id><![CDATA[506665ebbbaffc1701aaf5a61ad88421]]></access-key-id>
+ <!-- Required: Key secret -->
+ <secret-access-key><![CDATA[2dd63d700744e2c1c277be7dc81bfb1b]]></secret-access-key>
+ <!-- Optional: Base domain for the service -->
+ <!-- If you want to use your own s3-compatible service, base domain MUST be set -->
+ <base-domain><![CDATA[stg.vinadata.vn]]></base-domain>
+ <!-- Required: Storage region -->
+ <region><![CDATA[HCM]]></region>
+ <!-- Optional: Expiration time in seconds for URL signature. Default is 604800s (a week) -->
+ <expires>604800</expires>
+ </aws-s3>
+ <!-- Optional: List of domains that this profile will automatically be applied to -->
+ <!-- Domains in this list must have the same base domain with base-domain (if base-domain is set) -->
+ <!-- If you wish to apply the s3 credentials to a domain not listed here, then use {profile=s3}http://foo.s3... -->
+ <domains>
+ <domain name="bucket1.stg.vinadata.vn"/>
+ <domain name="bucket2.stg.vinadata.vn"/>
+ </domains>
+ </profile>
+ <profile name="blob">
+ <azure-blob>
+ <!-- key identifier, can override with AZURE_STORAGE_ACCESS_KEY environment variable -->
+ <secret-access-key>kOOY4Y/sqZU9bsLjmN+9McVwTry+UIn1Owt4Zs/2S2FQT0eAWLKskZ0V6/gGFqCAKVvwXoGjqUn7PNbVjhZiNA==</secret-access-key>
+ </azure-blob>
+ <domains>
+ <domain name="account.blob.core.windows.net"/>
+ </domains>
+ </profile>
+ </profiles>
</configuration>
-
--- /dev/null
+#!/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 <baonq5@vng.com.vn>
+# Portions created by the Initial Developer are Copyright (C)
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Quoc-Bao Nguyen <baonq5@vng.com.vn>
+#
+# 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()
+/*
+ * 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 <chris.rienzo@grasshopper.com>
+ * Quoc-Bao Nguyen <baonq5@vng.com.vn>
+ *
+ * test_aws.c - Unit tests for functions in aws.c
+ *
+ */
#include <switch.h>
#include <test/switch_test.h>
#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()
{
}
}
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 };
+
+ 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");
- snprintf(url, sizeof(url), "http://quotes.s3.amazonaws.com/nelson");
- parse_url(url, NULL, "s3", &bucket, &object);
+ 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);
+ 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(get_time)
+{
+ 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];
+
+ // Get date and time for test case
+ time_t rawtime;
+ struct tm * timeinfo;
+ time(&rawtime);
+ timeinfo = gmtime(&rawtime);
+
+ // 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);
- 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);
+ // 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);
- 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);
+ fst_check_string_equals(time_stamp_test, time_stamp);
+ fst_check_string_equals(date_stamp_test, date_stamp);
}
FST_TEST_END()
-FST_TEST_BEGIN(test_presigned_url)
+FST_TEST_BEGIN(hmac256_hex)
{
- 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 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);
+ }
- 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);
+ fst_check(i == 2); // test data contain two config
- 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);
+ switch_xml_free(cfg);
}
FST_TEST_END()
+#endif
}
FST_SUITE_END()
--- /dev/null
+<configuration name="http_cache.conf" description="HTTP GET cache">
+ <settings>
+ <!-- set to true if you want to enable http:// and https:// formats. Do not use if mod_httapi is also loaded -->
+ <param name="enable-file-formats" value="false"/>
+ <param name="max-urls" value="10000"/>
+ <param name="location" value="$${cache_dir}"/>
+ <param name="default-max-age" value="86400"/>
+ <param name="prefetch-thread-count" value="8"/>
+ <param name="prefetch-queue-size" value="100"/>
+ <!-- absolute path to CA bundle file -->
+ <param name="ssl-cacert" value="$${certs_dir}/cacert.pem"/>
+ <!-- verify certificates -->
+ <param name="ssl-verifypeer" value="true"/>
+ <!-- verify host name matches certificate -->
+ <param name="ssl-verifyhost" value="true"/>
+ <!-- default is 300 seconds, override here -->
+ <!--param name="connect-timeout" value="300"/-->
+ <!-- default is 300 seconds, override here -->
+ <!--param name="download-timeout" value="300"/-->
+ </settings>
+
+ <profiles>
+ <profile name="s3">
+ <!-- Credentials for AWS account -->
+ <aws-s3>
+ <!-- Required: Key identifier -->
+ <access-key-id><![CDATA[AKIAIOSFODNN7EXAMPLE]]></access-key-id>
+ <!-- Required: Key secret -->
+ <secret-access-key><![CDATA[wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY]]></secret-access-key>
+ <!-- Required: Storage region -->
+ <region><![CDATA[ap-southeast-1]]></region>
+ <!-- Optional: Expiration time in seconds for URL signature. Default is 604800s (a week) -->
+ <expires>604800</expires>
+ </aws-s3>
+ <!-- Optional: List of domains that this profile will automatically be applied to -->
+ <!-- Domains in this list must have the same base domain with base-domain (if base-domain is set) -->
+ <!-- If you wish to apply the s3 credentials to a domain not listed here, then use {profile=s3}http://foo.s3... -->
+ <domains>
+ <domain name="bucket1.s3-ap-southeast-1.amazonaws.com"/>
+ </domains>
+ </profile>
+
+ <profile name="s3-compatible">
+ <!-- Credentials for account on s3-like service -->
+ <aws-s3>
+ <!-- Required: Key identifier -->
+ <access-key-id><![CDATA[XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]]></access-key-id>
+ <!-- Required: Key secret -->
+ <secret-access-key><![CDATA[XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]]></secret-access-key>
+ <!-- Optional: Base domain for the service -->
+ <!-- If you want to use your own s3-compatible service, base domain MUST be set -->
+ <base-domain><![CDATA[stg.example.com]]></base-domain>
+ <!-- Required: Storage region -->
+ <region><![CDATA[ap-southeast-1]]></region>
+ <!-- Optional: Expiration time in seconds for URL signature. Default is 604800s (a week) -->
+ <expires>604800</expires>
+ </aws-s3>
+ <!-- Optional: List of domains that this profile will automatically be applied to -->
+ <!-- Domains in this list must have the same base domain with base-domain (if base-domain is set) -->
+ <!-- If you wish to apply the s3 credentials to a domain not listed here, then use {profile=s3}http://foo.s3... -->
+ <domains>
+ <domain name="bucket2.stg.example.com"/>
+ </domains>
+ </profile>
+ </profiles>
+</configuration>