/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/

#include "curl_setup.h"

#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH)

#include "urldata.h"
#include "strcase.h"
#include "strdup.h"
#include "http_aws_sigv4.h"
#include "curl_sha256.h"
#include "transfer.h"
#include "parsedate.h"
#include "sendf.h"

#include <time.h>

/* The last 3 #include files should be in this order */
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"

#include "slist.h"

#define HMAC_SHA256(k, kl, d, dl, o)        \
  do {                                      \
    ret = Curl_hmacit(Curl_HMAC_SHA256,     \
                      (unsigned char *)k,   \
                      kl,                   \
                      (unsigned char *)d,   \
                      dl, o);               \
    if(ret) {                               \
      goto fail;                            \
    }                                       \
  } while(0)

#define TIMESTAMP_SIZE 17

/* hex-encoded with trailing null */
#define SHA256_HEX_LENGTH (2 * SHA256_DIGEST_LENGTH + 1)

static void sha256_to_hex(char *dst, unsigned char *sha)
{
  int i;

  for(i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
    msnprintf(dst + (i * 2), SHA256_HEX_LENGTH - (i * 2), "%02x", sha[i]);
  }
}

static char *find_date_hdr(struct Curl_easy *data, const char *sig_hdr)
{
  char *tmp = Curl_checkheaders(data, sig_hdr, strlen(sig_hdr));

  if(tmp)
    return tmp;
  return Curl_checkheaders(data, STRCONST("Date"));
}

/* remove whitespace, and lowercase all headers */
static void trim_headers(struct curl_slist *head)
{
  struct curl_slist *l;
  for(l = head; l; l = l->next) {
    char *value; /* to read from */
    char *store;
    size_t colon = strcspn(l->data, ":");
    Curl_strntolower(l->data, l->data, colon);

    value = &l->data[colon];
    if(!*value)
      continue;
    ++value;
    store = value;

    /* skip leading whitespace */
    while(*value && ISBLANK(*value))
      value++;

    while(*value) {
      int space = 0;
      while(*value && ISBLANK(*value)) {
        value++;
        space++;
      }
      if(space) {
        /* replace any number of consecutive whitespace with a single space,
           unless at the end of the string, then nothing */
        if(*value)
          *store++ = ' ';
      }
      else
        *store++ = *value++;
    }
    *store = 0; /* null terminate */
  }
}

/* maximum length for the aws sivg4 parts */
#define MAX_SIGV4_LEN 64
#define MAX_SIGV4_LEN_TXT "64"

#define DATE_HDR_KEY_LEN (MAX_SIGV4_LEN + sizeof("X--Date"))

#define MAX_HOST_LEN 255
/* FQDN + host: */
#define FULL_HOST_LEN (MAX_HOST_LEN + sizeof("host:"))

/* string been x-PROVIDER-date:TIMESTAMP, I need +1 for ':' */
#define DATE_FULL_HDR_LEN (DATE_HDR_KEY_LEN + TIMESTAMP_SIZE + 1)

/* timestamp should point to a buffer of at last TIMESTAMP_SIZE bytes */
static CURLcode make_headers(struct Curl_easy *data,
                             const char *hostname,
                             char *timestamp,
                             char *provider1,
                             char **date_header,
                             char *content_sha256_header,
                             struct dynbuf *canonical_headers,
                             struct dynbuf *signed_headers)
{
  char date_hdr_key[DATE_HDR_KEY_LEN];
  char date_full_hdr[DATE_FULL_HDR_LEN];
  struct curl_slist *head = NULL;
  struct curl_slist *tmp_head = NULL;
  CURLcode ret = CURLE_OUT_OF_MEMORY;
  struct curl_slist *l;
  int again = 1;

  /* provider1 mid */
  Curl_strntolower(provider1, provider1, strlen(provider1));
  provider1[0] = Curl_raw_toupper(provider1[0]);

  msnprintf(date_hdr_key, DATE_HDR_KEY_LEN, "X-%s-Date", provider1);

  /* provider1 lowercase */
  Curl_strntolower(provider1, provider1, 1); /* first byte only */
  msnprintf(date_full_hdr, DATE_FULL_HDR_LEN,
            "x-%s-date:%s", provider1, timestamp);

  if(Curl_checkheaders(data, STRCONST("Host"))) {
    head = NULL;
  }
  else {
    char full_host[FULL_HOST_LEN + 1];

    if(data->state.aptr.host) {
      size_t pos;

      if(strlen(data->state.aptr.host) > FULL_HOST_LEN) {
        ret = CURLE_URL_MALFORMAT;
        goto fail;
      }
      strcpy(full_host, data->state.aptr.host);
      /* remove /r/n as the separator for canonical request must be '\n' */
      pos = strcspn(full_host, "\n\r");
      full_host[pos] = 0;
    }
    else {
      if(strlen(hostname) > MAX_HOST_LEN) {
        ret = CURLE_URL_MALFORMAT;
        goto fail;
      }
      msnprintf(full_host, FULL_HOST_LEN, "host:%s", hostname);
    }

    head = curl_slist_append(NULL, full_host);
    if(!head)
      goto fail;
  }


  if(*content_sha256_header) {
    tmp_head = curl_slist_append(head, content_sha256_header);
    if(!tmp_head)
      goto fail;
    head = tmp_head;
  }

  for(l = data->set.headers; l; l = l->next) {
    tmp_head = curl_slist_append(head, l->data);
    if(!tmp_head)
      goto fail;
    head = tmp_head;
  }

  trim_headers(head);

  *date_header = find_date_hdr(data, date_hdr_key);
  if(!*date_header) {
    tmp_head = curl_slist_append(head, date_full_hdr);
    if(!tmp_head)
      goto fail;
    head = tmp_head;
    *date_header = curl_maprintf("%s: %s", date_hdr_key, timestamp);
  }
  else {
    char *value;

    *date_header = strdup(*date_header);
    if(!*date_header)
      goto fail;

    value = strchr(*date_header, ':');
    if(!value)
      goto fail;
    ++value;
    while(ISBLANK(*value))
      ++value;
    strncpy(timestamp, value, TIMESTAMP_SIZE - 1);
    timestamp[TIMESTAMP_SIZE - 1] = 0;
  }

  /* alpha-sort in a case sensitive manner */
  do {
    again = 0;
    for(l = head; l; l = l->next) {
      struct curl_slist *next = l->next;

      if(next && strcmp(l->data, next->data) > 0) {
        char *tmp = l->data;

        l->data = next->data;
        next->data = tmp;
        again = 1;
      }
    }
  } while(again);

  for(l = head; l; l = l->next) {
    char *tmp;

    if(Curl_dyn_add(canonical_headers, l->data))
      goto fail;
    if(Curl_dyn_add(canonical_headers, "\n"))
      goto fail;

    tmp = strchr(l->data, ':');
    if(tmp)
      *tmp = 0;

    if(l != head) {
      if(Curl_dyn_add(signed_headers, ";"))
        goto fail;
    }
    if(Curl_dyn_add(signed_headers, l->data))
      goto fail;
  }

  ret = CURLE_OK;
fail:
  curl_slist_free_all(head);

  return ret;
}

#define CONTENT_SHA256_KEY_LEN (MAX_SIGV4_LEN + sizeof("X--Content-Sha256"))
/* add 2 for ": " between header name and value */
#define CONTENT_SHA256_HDR_LEN (CONTENT_SHA256_KEY_LEN + 2 + \
                                SHA256_HEX_LENGTH)

/* try to parse a payload hash from the content-sha256 header */
static char *parse_content_sha_hdr(struct Curl_easy *data,
                                   const char *provider1,
                                   size_t *value_len)
{
  char key[CONTENT_SHA256_KEY_LEN];
  size_t key_len;
  char *value;
  size_t len;

  key_len = msnprintf(key, sizeof(key), "x-%s-content-sha256", provider1);

  value = Curl_checkheaders(data, key, key_len);
  if(!value)
    return NULL;

  value = strchr(value, ':');
  if(!value)
    return NULL;
  ++value;

  while(*value && ISBLANK(*value))
    ++value;

  len = strlen(value);
  while(len > 0 && ISBLANK(value[len-1]))
    --len;

  *value_len = len;
  return value;
}

static CURLcode calc_payload_hash(struct Curl_easy *data,
                                  unsigned char *sha_hash, char *sha_hex)
{
  const char *post_data = data->set.postfields;
  size_t post_data_len = 0;
  CURLcode result;

  if(post_data) {
    if(data->set.postfieldsize < 0)
      post_data_len = strlen(post_data);
    else
      post_data_len = (size_t)data->set.postfieldsize;
  }
  result = Curl_sha256it(sha_hash, (const unsigned char *) post_data,
                         post_data_len);
  if(!result)
    sha256_to_hex(sha_hex, sha_hash);
  return result;
}

#define S3_UNSIGNED_PAYLOAD "UNSIGNED-PAYLOAD"

static CURLcode calc_s3_payload_hash(struct Curl_easy *data,
                                     Curl_HttpReq httpreq, char *provider1,
                                     unsigned char *sha_hash,
                                     char *sha_hex, char *header)
{
  bool empty_method = (httpreq == HTTPREQ_GET || httpreq == HTTPREQ_HEAD);
  /* The request method or filesize indicate no request payload */
  bool empty_payload = (empty_method || data->set.filesize == 0);
  /* The POST payload is in memory */
  bool post_payload = (httpreq == HTTPREQ_POST && data->set.postfields);
  CURLcode ret = CURLE_OUT_OF_MEMORY;

  if(empty_payload || post_payload) {
    /* Calculate a real hash when we know the request payload */
    ret = calc_payload_hash(data, sha_hash, sha_hex);
    if(ret)
      goto fail;
  }
  else {
    /* Fall back to s3's UNSIGNED-PAYLOAD */
    size_t len = sizeof(S3_UNSIGNED_PAYLOAD) - 1;
    DEBUGASSERT(len < SHA256_HEX_LENGTH); /* 16 < 65 */
    memcpy(sha_hex, S3_UNSIGNED_PAYLOAD, len);
    sha_hex[len] = 0;
  }

  /* format the required content-sha256 header */
  msnprintf(header, CONTENT_SHA256_HDR_LEN,
            "x-%s-content-sha256: %s", provider1, sha_hex);

  ret = CURLE_OK;
fail:
  return ret;
}

CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
{
  CURLcode ret = CURLE_OUT_OF_MEMORY;
  struct connectdata *conn = data->conn;
  size_t len;
  const char *arg;
  char provider0[MAX_SIGV4_LEN + 1]="";
  char provider1[MAX_SIGV4_LEN + 1]="";
  char region[MAX_SIGV4_LEN + 1]="";
  char service[MAX_SIGV4_LEN + 1]="";
  bool sign_as_s3 = false;
  const char *hostname = conn->host.name;
  time_t clock;
  struct tm tm;
  char timestamp[TIMESTAMP_SIZE];
  char date[9];
  struct dynbuf canonical_headers;
  struct dynbuf signed_headers;
  char *date_header = NULL;
  Curl_HttpReq httpreq;
  const char *method = NULL;
  char *payload_hash = NULL;
  size_t payload_hash_len = 0;
  unsigned char sha_hash[SHA256_DIGEST_LENGTH];
  char sha_hex[SHA256_HEX_LENGTH];
  char content_sha256_hdr[CONTENT_SHA256_HDR_LEN + 2] = ""; /* add \r\n */
  char *canonical_request = NULL;
  char *request_type = NULL;
  char *credential_scope = NULL;
  char *str_to_sign = NULL;
  const char *user = data->state.aptr.user ? data->state.aptr.user : "";
  char *secret = NULL;
  unsigned char sign0[SHA256_DIGEST_LENGTH] = {0};
  unsigned char sign1[SHA256_DIGEST_LENGTH] = {0};
  char *auth_headers = NULL;

  DEBUGASSERT(!proxy);
  (void)proxy;

  if(Curl_checkheaders(data, STRCONST("Authorization"))) {
    /* Authorization already present, Bailing out */
    return CURLE_OK;
  }

  /* we init those buffers here, so goto fail will free initialized dynbuf */
  Curl_dyn_init(&canonical_headers, CURL_MAX_HTTP_HEADER);
  Curl_dyn_init(&signed_headers, CURL_MAX_HTTP_HEADER);

  /*
   * Parameters parsing
   * Google and Outscale use the same OSC or GOOG,
   * but Amazon uses AWS and AMZ for header arguments.
   * AWS is the default because most of non-amazon providers
   * are still using aws:amz as a prefix.
   */
  arg = data->set.str[STRING_AWS_SIGV4] ?
    data->set.str[STRING_AWS_SIGV4] : "aws:amz";

  /* provider1[:provider2[:region[:service]]]

     No string can be longer than N bytes of non-whitespace
   */
  (void)sscanf(arg, "%" MAX_SIGV4_LEN_TXT "[^:]"
               ":%" MAX_SIGV4_LEN_TXT "[^:]"
               ":%" MAX_SIGV4_LEN_TXT "[^:]"
               ":%" MAX_SIGV4_LEN_TXT "s",
               provider0, provider1, region, service);
  if(!provider0[0]) {
    failf(data, "first provider can't be empty");
    ret = CURLE_BAD_FUNCTION_ARGUMENT;
    goto fail;
  }
  else if(!provider1[0])
    strcpy(provider1, provider0);

  if(!service[0]) {
    char *hostdot = strchr(hostname, '.');
    if(!hostdot) {
      failf(data, "service missing in parameters and hostname");
      ret = CURLE_URL_MALFORMAT;
      goto fail;
    }
    len = hostdot - hostname;
    if(len > MAX_SIGV4_LEN) {
      failf(data, "service too long in hostname");
      ret = CURLE_URL_MALFORMAT;
      goto fail;
    }
    strncpy(service, hostname, len);
    service[len] = '\0';

    if(!region[0]) {
      const char *reg = hostdot + 1;
      const char *hostreg = strchr(reg, '.');
      if(!hostreg) {
        failf(data, "region missing in parameters and hostname");
        ret = CURLE_URL_MALFORMAT;
        goto fail;
      }
      len = hostreg - reg;
      if(len > MAX_SIGV4_LEN) {
        failf(data, "region too long in hostname");
        ret = CURLE_URL_MALFORMAT;
        goto fail;
      }
      strncpy(region, reg, len);
      region[len] = '\0';
    }
  }

  Curl_http_method(data, conn, &method, &httpreq);

  /* AWS S3 requires a x-amz-content-sha256 header, and supports special
   * values like UNSIGNED-PAYLOAD */
  sign_as_s3 = (strcasecompare(provider0, "aws") &&
                strcasecompare(service, "s3"));

  payload_hash = parse_content_sha_hdr(data, provider1, &payload_hash_len);

  if(!payload_hash) {
    if(sign_as_s3)
      ret = calc_s3_payload_hash(data, httpreq, provider1, sha_hash,
                                 sha_hex, content_sha256_hdr);
    else
      ret = calc_payload_hash(data, sha_hash, sha_hex);
    if(ret)
      goto fail;

    payload_hash = sha_hex;
    /* may be shorter than SHA256_HEX_LENGTH, like S3_UNSIGNED_PAYLOAD */
    payload_hash_len = strlen(sha_hex);
  }

#ifdef DEBUGBUILD
  {
    char *force_timestamp = getenv("CURL_FORCETIME");
    if(force_timestamp)
      clock = 0;
    else
      time(&clock);
  }
#else
  time(&clock);
#endif
  ret = Curl_gmtime(clock, &tm);
  if(ret) {
    goto fail;
  }
  if(!strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%SZ", &tm)) {
    ret = CURLE_OUT_OF_MEMORY;
    goto fail;
  }

  ret = make_headers(data, hostname, timestamp, provider1,
                     &date_header, content_sha256_hdr,
                     &canonical_headers, &signed_headers);
  if(ret)
    goto fail;
  ret = CURLE_OUT_OF_MEMORY;

  if(*content_sha256_hdr) {
    /* make_headers() needed this without the \r\n for canonicalization */
    size_t hdrlen = strlen(content_sha256_hdr);
    DEBUGASSERT(hdrlen + 3 < sizeof(content_sha256_hdr));
    memcpy(content_sha256_hdr + hdrlen, "\r\n", 3);
  }

  memcpy(date, timestamp, sizeof(date));
  date[sizeof(date) - 1] = 0;

  canonical_request =
    curl_maprintf("%s\n" /* HTTPRequestMethod */
                  "%s\n" /* CanonicalURI */
                  "%s\n" /* CanonicalQueryString */
                  "%s\n" /* CanonicalHeaders */
                  "%s\n" /* SignedHeaders */
                  "%.*s",  /* HashedRequestPayload in hex */
                  method,
                  data->state.up.path,
                  data->state.up.query ? data->state.up.query : "",
                  Curl_dyn_ptr(&canonical_headers),
                  Curl_dyn_ptr(&signed_headers),
                  (int)payload_hash_len, payload_hash);
  if(!canonical_request)
    goto fail;

  /* provider 0 lowercase */
  Curl_strntolower(provider0, provider0, strlen(provider0));
  request_type = curl_maprintf("%s4_request", provider0);
  if(!request_type)
    goto fail;

  credential_scope = curl_maprintf("%s/%s/%s/%s",
                                   date, region, service, request_type);
  if(!credential_scope)
    goto fail;

  if(Curl_sha256it(sha_hash, (unsigned char *) canonical_request,
                   strlen(canonical_request)))
    goto fail;

  sha256_to_hex(sha_hex, sha_hash);

  /* provider 0 uppercase */
  Curl_strntoupper(provider0, provider0, strlen(provider0));

  /*
   * Google allows using RSA key instead of HMAC, so this code might change
   * in the future. For now we only support HMAC.
   */
  str_to_sign = curl_maprintf("%s4-HMAC-SHA256\n" /* Algorithm */
                              "%s\n" /* RequestDateTime */
                              "%s\n" /* CredentialScope */
                              "%s",  /* HashedCanonicalRequest in hex */
                              provider0,
                              timestamp,
                              credential_scope,
                              sha_hex);
  if(!str_to_sign) {
    goto fail;
  }

  /* provider 0 uppercase */
  secret = curl_maprintf("%s4%s", provider0,
                         data->state.aptr.passwd ?
                         data->state.aptr.passwd : "");
  if(!secret)
    goto fail;

  HMAC_SHA256(secret, strlen(secret), date, strlen(date), sign0);
  HMAC_SHA256(sign0, sizeof(sign0), region, strlen(region), sign1);
  HMAC_SHA256(sign1, sizeof(sign1), service, strlen(service), sign0);
  HMAC_SHA256(sign0, sizeof(sign0), request_type, strlen(request_type), sign1);
  HMAC_SHA256(sign1, sizeof(sign1), str_to_sign, strlen(str_to_sign), sign0);

  sha256_to_hex(sha_hex, sign0);

  /* provider 0 uppercase */
  auth_headers = curl_maprintf("Authorization: %s4-HMAC-SHA256 "
                               "Credential=%s/%s, "
                               "SignedHeaders=%s, "
                               "Signature=%s\r\n"
                               "%s\r\n"
                               "%s", /* optional sha256 header includes \r\n */
                               provider0,
                               user,
                               credential_scope,
                               Curl_dyn_ptr(&signed_headers),
                               sha_hex,
                               date_header,
                               content_sha256_hdr);
  if(!auth_headers) {
    goto fail;
  }

  Curl_safefree(data->state.aptr.userpwd);
  data->state.aptr.userpwd = auth_headers;
  data->state.authhost.done = TRUE;
  ret = CURLE_OK;

fail:
  Curl_dyn_free(&canonical_headers);
  Curl_dyn_free(&signed_headers);
  free(canonical_request);
  free(request_type);
  free(credential_scope);
  free(str_to_sign);
  free(secret);
  free(date_header);
  return ret;
}

#endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH) */
