#!/usr/bin/env bash
#
# Common functions for s3-bash4 commands
# (c) 2015 Chi Vinh Le <cvl@winged.kiwi>

# 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[@]}"
}

