Add v4 sig to http cache (#741)

* 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>
This commit is contained in:
Chris Rienzo 2020-07-20 15:34:55 -04:00 committed by GitHub
parent 90fb233ac6
commit 17ebce7e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 931 additions and 300 deletions

View File

@ -22,6 +22,7 @@
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
* Quoc-Bao Nguyen <baonq5@vng.com.vn>
*
* aws.c -- Some Amazon Web Services helper functions
*
@ -34,211 +35,378 @@
#include <openssl/sha.h>
#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:
* <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);
}
/**
* 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 <openssl/hmac.h>
#include <openssl/sha.h>
#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

View File

@ -22,7 +22,8 @@
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
*
* Quoc-Bao Nguyen <baonq5@vng.com.vn>
*
* aws.h - Some Amazon Web Services helper functions
*
*/
@ -33,13 +34,27 @@
#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

View File

@ -22,6 +22,7 @@
*
* Contributor(s):
* Chris Rienzo <chris.rienzo@grasshopper.com>
* Quoc-Bao Nguyen <baonq5@vng.com.vn>
*
* 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;
}

View File

@ -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 <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 {
@ -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

View File

@ -1,47 +1,74 @@
<?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>

View File

@ -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 <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()

View File

@ -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 <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()
{
}
@ -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()

View File

@ -0,0 +1,66 @@
<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>