#!/usr/bin/env bash # # Common functions for s3-bash4 commands # (c) 2015 Chi Vinh Le # Constants readonly VERSION="0.0.1" # Exit codes readonly INVALID_USAGE_EXIT_CODE=1 readonly INVALID_USER_DATA_EXIT_CODE=2 readonly INVALID_ENVIRONMENT_EXIT_CODE=3 ## # Write error to stderr # Arguments: # $1 string to output ## err() { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] Error: $@" >&2 } ## # Display version and exit ## showVersionAndExit() { printf "$VERSION\n" exit } ## # Helper for parsing the command line. ## assertArgument() { if [[ $# -lt 2 ]]; then err "Option $1 needs an argument." exit $INVALID_USAGE_EXIT_CODE fi } ## # Asserts given resource path # Arguments: # $1 string resource path ## assertResourcePath() { if [[ $1 = !(/*) ]]; then err "Resource should start with / e.g. /bucket/file.ext" exit $INVALID_USAGE_EXIT_CODE fi } ## # Asserts given file exists. # Arguments: # $1 string file path ## assertFileExists() { if [[ ! -f $1 ]]; then err "$1 file doesn't exists" exit $INVALID_USER_DATA_EXIT_CODE fi } ## # Check for valid environment. Exit if invalid. ## checkEnvironment() { programs=(openssl curl printf echo sed awk od date shasum pwd dirname) for program in "${programs[@]}"; do if [ ! -x "$(which $program)" ]; then err "$program is required to run" exit $INVALID_ENVIRONMENT_EXIT_CODE fi done } ## # Reads, validates and return aws secret stored in a file # Arguments: # $1 path to secret file # Output: # string AWS secret ## processAWSSecretFile() { local errStr="The Amazon AWS secret key must be 40 bytes long. Make sure that there is no carriage return at the end of line." if ! [[ -f $1 ]]; then err "The file $1 does not exist." exit $INVALID_USER_DATA_EXIT_CODE fi # limit file size to max 41 characters. 40 + potential null terminating character. local fileSize="$(ls -l "$1" | awk '{ print $5 }')" if [[ $fileSize -gt 41 ]]; then err $errStr exit $INVALID_USER_DATA_EXIT_CODE fi secret=$(<$1) # exact string size should be 40. if [[ ${#secret} != 40 ]]; then err $errStr exit $INVALID_USER_DATA_EXIT_CODE fi echo $secret } ## # Convert string to hex with max line size of 256 # Arguments: # $1 string to convert # Returns: # string hex ## hex256() { printf "$1" | od -A n -t x1 | sed ':a;N;$!ba;s/[\n ]//g' } ## # Calculate sha256 hash # Arguments: # $1 string to hash # Returns: # string hash ## sha256Hash() { local output=$(printf "$1" | shasum -a 256) echo "${output%% *}" } ## # Calculate sha256 hash of file # Arguments: # $1 file path # Returns: # string hash ## sha256HashFile() { local output=$(shasum -a 256 $1) echo "${output%% *}" } ## # Generate HMAC signature using SHA256 # Arguments: # $1 signing key in hex # $2 string data to sign # Returns: # string signature ## hmac_sha256() { printf "$2" | openssl dgst -binary -hex -sha256 -mac HMAC -macopt hexkey:$1 \ | sed 's/^.* //' } ## # Sign data using AWS Signature Version 4 # Arguments: # $1 AWS Secret Access Key # $2 yyyymmdd # $3 AWS Region # $4 AWS Service # $5 string data to sign # Returns: # signature ## sign() { local kSigning=$(hmac_sha256 $(hmac_sha256 $(hmac_sha256 \ $(hmac_sha256 $(hex256 "AWS4$1") $2) $3) $4) "aws4_request") hmac_sha256 "${kSigning}" "$5" } ## # Get endpoint of specified region # Arguments: # $1 region # Returns: # amazon andpoint ## convS3RegionToEndpoint() { case "$1" in us-east-1) echo "s3.amazonaws.com" ;; *) echo ${1}.digitaloceanspaces.com ;; esac } ## # Perform request to S3 # Uses the following Globals: # METHOD string # AWS_ACCESS_KEY_ID string # AWS_SECRET_ACCESS_KEY string # AWS_REGION string # RESOURCE_PATH string # FILE_TO_UPLOAD string # CONTENT_TYPE string # PUBLISH bool # DEBUG bool # VERBOSE bool # INSECURE bool ## performRequest() { local timestamp=$(date -u "+%Y-%m-%d %H:%M:%S") local isoTimestamp=$(date -ud "${timestamp}" "+%Y%m%dT%H%M%SZ") local dateScope=$(date -ud "${timestamp}" "+%Y%m%d") local host=$(convS3RegionToEndpoint "${AWS_REGION}") # Generate payload hash if [[ $METHOD == "PUT" ]]; then local payloadHash=$(sha256HashFile $FILE_TO_UPLOAD) else local payloadHash=$(sha256Hash "") fi local cmd=("curl") local headers= local headerList= cmd+=("--silent") if [[ ${DEBUG} != true ]]; then cmd+=("--fail") fi if [[ ${VERBOSE} == true ]]; then cmd+=("--verbose") fi if [[ ${METHOD} == "PUT" ]]; then cmd+=("-T" "${FILE_TO_UPLOAD}") fi if [[ ${METHOD} == "HEAD" ]]; then cmd+=("--head") else cmd+=("-X" "${METHOD}") fi if [[ ${METHOD} == "PUT" && ! -z "${CONTENT_TYPE}" ]]; then cmd+=("-H" "Content-Type: ${CONTENT_TYPE}") headers+="content-type:${CONTENT_TYPE}\n" headerList+="content-type;" fi cmd+=("-H" "Host: ${host}") headers+="host:${host}\n" headerList+="host;" if [[ ${METHOD} == "PUT" && "${PUBLISH}" == true ]]; then cmd+=("-H" "x-amz-acl: public-read") headers+="x-amz-acl:public-read\n" headerList+="x-amz-acl;" fi cmd+=("-H" "x-amz-content-sha256: ${payloadHash}") headers+="x-amz-content-sha256:${payloadHash}\n" headerList+="x-amz-content-sha256;" cmd+=("-H" "x-amz-date: ${isoTimestamp}") headers+="x-amz-date:${isoTimestamp}" headerList+="x-amz-date" # Generate canonical request local canonicalRequest="${METHOD} ${RESOURCE_PATH} ${headers} ${headerList} ${payloadHash}" # Generated request hash local hashedRequest=$(sha256Hash "${canonicalRequest}") # Generate signing data local stringToSign="AWS4-HMAC-SHA256 ${isoTimestamp} ${dateScope}/${AWS_REGION}/s3/aws4_request ${hashedRequest}" # Sign data local signature=$(sign "${AWS_SECRET_ACCESS_KEY}" "${dateScope}" "${AWS_REGION}" \ "s3" "${stringToSign}") local authorizationHeader="AWS4-HMAC-SHA256 Credential=${AWS_ACCESS_KEY_ID}/${dateScope}/${AWS_REGION}/s3/aws4_request, SignedHeaders=${headerList}, Signature=${signature}" cmd+=("-H" "Authorization: ${authorizationHeader}") local protocol="https" # if [[ $INSECURE == false ]]; then # protocol="http" # fi cmd+=("${protocol}://${host}${RESOURCE_PATH}") # Curl "${cmd[@]}" }