/* vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
 * Copyright 2019 MariaDB Corporation Ab. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */

#include "config.h"
#include "common.h"

#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdint.h>

ms3_malloc_callback ms3_cmalloc = (ms3_malloc_callback)malloc;
ms3_free_callback ms3_cfree = (ms3_free_callback)free;
ms3_realloc_callback ms3_crealloc = (ms3_realloc_callback)realloc;
ms3_strdup_callback ms3_cstrdup = (ms3_strdup_callback)strdup;
ms3_calloc_callback ms3_ccalloc = (ms3_calloc_callback)calloc;


/* Thread locking code for OpenSSL < 1.1.0 */
#include <dlfcn.h>
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT ((void *)0)
#endif
static pthread_mutex_t *mutex_buf = NULL;
#define CRYPTO_LOCK 1
static void (*openssl_set_id_callback)(unsigned long (*func)(void));
static void (*openssl_set_locking_callback)(void (*func)(int mode,int type, const char *file,int line));
static int (*openssl_num_locks)(void);

static void locking_function(int mode, int n, const char *file, int line)
{
  (void) file;
  (void) line;
  if(mode & CRYPTO_LOCK)
    pthread_mutex_lock(&(mutex_buf[n]));
  else
    pthread_mutex_unlock(&(mutex_buf[n]));
}

static int curl_needs_openssl_locking(void)
{
  curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);

  if (data->ssl_version == NULL)
  {
    return 0;
  }

  if (strncmp(data->ssl_version, "OpenSSL", 7) != 0)
  {
    return 0;
  }
  if (data->ssl_version[8] == '0')
  {
    return 1;
  }
  if ((data->ssl_version[8] == '1') && (data->ssl_version[10] == '0'))
  {
    openssl_set_id_callback = dlsym(RTLD_DEFAULT, "CRYPTO_set_id_callback");
    openssl_set_locking_callback = dlsym(RTLD_DEFAULT, "CRYPTO_set_locking_callback");
    openssl_num_locks = dlsym(RTLD_DEFAULT, "CRYPTO_num_locks");
    return openssl_set_id_callback != NULL &&
           openssl_set_locking_callback != NULL &&
           openssl_num_locks != NULL;
  }
  return 0;
}

static unsigned long __attribute__((unused)) id_function(void)
{
  return ((unsigned long)pthread_self());
}

uint8_t ms3_library_init_malloc(ms3_malloc_callback m,
                                ms3_free_callback f, ms3_realloc_callback r,
                                ms3_strdup_callback s, ms3_calloc_callback c)
{
  if (!m || !f || !r || !s || !c)
  {
    return MS3_ERR_PARAMETER;
  }

  ms3_cmalloc = m;
  ms3_cfree = f;
  ms3_crealloc = r;
  ms3_cstrdup = s;
  ms3_ccalloc = c;

  if (curl_needs_openssl_locking())
  {
    int i;
    mutex_buf = ms3_cmalloc(openssl_num_locks() * sizeof(pthread_mutex_t));
    if(mutex_buf)
    {
      for(i = 0; i < openssl_num_locks(); i++)
        pthread_mutex_init(&(mutex_buf[i]), NULL);
      openssl_set_id_callback(id_function);
      openssl_set_locking_callback(locking_function);
    }
  }

  if (curl_global_init_mem(CURL_GLOBAL_DEFAULT, m, f, r, s, c))
  {
    return MS3_ERR_PARAMETER;
  }

  return 0;
}

void ms3_library_init(void)
{
  if (curl_needs_openssl_locking())
  {
    int i;
    mutex_buf = malloc(openssl_num_locks() * sizeof(pthread_mutex_t));
    if(mutex_buf)
    {
      for(i = 0; i < openssl_num_locks(); i++)
        pthread_mutex_init(&(mutex_buf[i]), NULL);
      openssl_set_id_callback(id_function);
      openssl_set_locking_callback(locking_function);
    }
  }
  curl_global_init(CURL_GLOBAL_DEFAULT);
}

void ms3_library_deinit(void)
{
  int i;
  if (mutex_buf)
  {
    openssl_set_id_callback(NULL);
    openssl_set_locking_callback(NULL);
    for(i = 0;  i < openssl_num_locks();  i++)
      pthread_mutex_destroy(&(mutex_buf[i]));
    ms3_cfree(mutex_buf);
    mutex_buf = NULL;
  }
  curl_global_cleanup();
}

ms3_st *ms3_init(const char *s3key, const char *s3secret,
                 const char *region,
                 const char *base_domain)
{
  ms3_st *ms3;

  if ((s3key == NULL) || (s3secret == NULL))
  {
    return NULL;
  }

  ms3 = ms3_cmalloc(sizeof(ms3_st));

  ms3->s3key = ms3_cstrdup(s3key);
  ms3->s3secret = ms3_cstrdup(s3secret);
  ms3->region = ms3_cstrdup(region);
  ms3->port = 0; /* The default value */

  if (base_domain && strlen(base_domain))
  {
    struct sockaddr_in sa;
    ms3->base_domain = ms3_cstrdup(base_domain);
    if (inet_pton(AF_INET, base_domain, &(sa.sin_addr)))
    {
      ms3->list_version = 1;
      ms3->protocol_version = 1;
    }
    else if (strcmp(base_domain, "s3.amazonaws.com") == 0)
    {
      ms3->list_version = 2;
      ms3->protocol_version = 2;
    }
    else
    {
      // Assume that S3-compatible APIs can't support v2 list
      ms3->list_version = 1;
      ms3->protocol_version = 2;
    }
  }
  else
  {
    ms3->base_domain = NULL;
    ms3->list_version = 2;
    ms3->protocol_version = 2;
  }

  ms3->buffer_chunk_size = READ_BUFFER_DEFAULT_SIZE;

  ms3->curl = curl_easy_init();
  ms3->last_error = NULL;
  ms3->use_http = false;
  ms3->disable_verification = false;
  ms3->first_run = true;
  ms3->path_buffer = ms3_cmalloc(sizeof(char) * 1024);
  ms3->query_buffer = ms3_cmalloc(sizeof(char) * 3072);
  ms3->list_container.pool = NULL;
  ms3->list_container.next = NULL;
  ms3->list_container.start = NULL;
  ms3->list_container.pool_list = NULL;
  ms3->list_container.pool_free = 0;
  ms3->read_cb= 0;
  ms3->user_data= 0;
  ms3->connect_timeout_ms = 0;
  ms3->timeout_ms = 0;

  ms3->iam_role = NULL;
  ms3->role_key = NULL;
  ms3->role_secret = NULL;
  ms3->role_session_token = NULL;
  ms3->iam_endpoint = NULL;
  ms3->sts_endpoint = NULL;
  ms3->sts_region = NULL;
  ms3->iam_role_arn = NULL;

  return ms3;
}

uint8_t ms3_init_assume_role(ms3_st *ms3, const char *iam_role, const char *sts_endpoint, const char *sts_region)
{
  uint8_t ret=0;

  if (iam_role == NULL)
  {
      return MS3_ERR_PARAMETER;
  }
  ms3->iam_role = ms3_cstrdup(iam_role);

  if (sts_endpoint && strlen(sts_endpoint))
  {
      ms3->sts_endpoint = ms3_cstrdup(sts_endpoint);
  }
  else
  {
      ms3->sts_endpoint = ms3_cstrdup("sts.amazonaws.com");
  }

  if (sts_region && strlen(sts_region))
  {
      ms3->sts_region = ms3_cstrdup(sts_region);
  }
  else
  {
      ms3->sts_region = ms3_cstrdup("us-east-1");
  }

  ms3->iam_endpoint = ms3_cstrdup("iam.amazonaws.com");

  ms3->iam_role_arn = ms3_cmalloc(sizeof(char) * 2048);
  ms3->iam_role_arn[0] = '\0';
  ms3->role_key = ms3_cmalloc(sizeof(char) * 128);
  ms3->role_key[0] = '\0';
  ms3->role_secret = ms3_cmalloc(sizeof(char) * 1024);
  ms3->role_secret[0] = '\0';
  // aws says theres no maximum length here.. 2048 might be overkill
  ms3->role_session_token = ms3_cmalloc(sizeof(char) * 2048);
  ms3->role_session_token[0] = '\0';
  // 0 will uses the default and not set a value in the request
  ms3->role_session_duration = 0;

  ret = ms3_assume_role(ms3);

  return ret;
}

uint8_t ms3_ec2_set_cred(ms3_st *ms3, const char *iam_role,
                     const char *s3key, const char *s3secret,
                     const char *token)
{
  uint8_t ret=0;

  if (iam_role == NULL || token == NULL || s3key == NULL || s3secret == NULL)
  {
      return MS3_ERR_PARAMETER;
  }
  ms3->iam_role = ms3_cstrdup(iam_role);
  ms3->role_key = ms3_cstrdup(s3key);
  ms3->role_secret = ms3_cstrdup(s3secret);
  ms3->role_session_token = ms3_cstrdup(token);

  return ret;
}

static void list_free(ms3_st *ms3)
{
  ms3_list_st *list = ms3->list_container.start;
  struct ms3_pool_alloc_list_st *plist = NULL, *next = NULL;
  while (list)
  {
    ms3_cfree(list->key);
    list = list->next;
  }
  plist = ms3->list_container.pool_list;
  while (plist)
  {
    next = plist->prev;
    ms3_cfree(plist->pool);
    ms3_cfree(plist);
    plist = next;
  }
  ms3->list_container.pool = NULL;
  ms3->list_container.next = NULL;
  ms3->list_container.start = NULL;
  ms3->list_container.pool_list = NULL;
  ms3->list_container.pool_free = 0;
}

void ms3_deinit(ms3_st *ms3)
{
  if (!ms3)
  {
    return;
  }

  ms3debug("deinit: 0x%" PRIXPTR, (uintptr_t)ms3);
  ms3_cfree(ms3->s3secret);
  ms3_cfree(ms3->s3key);
  ms3_cfree(ms3->region);
  ms3_cfree(ms3->base_domain);
  ms3_cfree(ms3->iam_role);
  ms3_cfree(ms3->role_key);
  ms3_cfree(ms3->role_secret);
  ms3_cfree(ms3->role_session_token);
  ms3_cfree(ms3->iam_endpoint);
  ms3_cfree(ms3->sts_endpoint);
  ms3_cfree(ms3->sts_region);
  ms3_cfree(ms3->iam_role_arn);
  curl_easy_cleanup(ms3->curl);
  ms3_cfree(ms3->last_error);
  ms3_cfree(ms3->path_buffer);
  ms3_cfree(ms3->query_buffer);
  list_free(ms3);
  ms3_cfree(ms3);
}

const char *ms3_server_error(ms3_st *ms3)
{
  if (!ms3)
  {
    return NULL;
  }

  return ms3->last_error;
}

void ms3_debug(int debug_state)
{
  bool state = ms3debug_get();
  if (state != (bool) debug_state)
  {
    ms3debug_set((bool) debug_state);
    if (debug_state)
    {
      ms3debug("enabling debug");
    }
  }
}

const char *ms3_error(uint8_t errcode)
{
  if (errcode >= MS3_ERR_MAX)
  {
    return baderror;
  }

  return errmsgs[errcode];
}

uint8_t ms3_list_dir(ms3_st *ms3, const char *bucket, const char *prefix,
                     ms3_list_st **list)
{
  uint8_t res = 0;

  if (!ms3 || !bucket || !list)
  {
    return MS3_ERR_PARAMETER;
  }

  list_free(ms3);
  res = execute_request(ms3, MS3_CMD_LIST, bucket, NULL, NULL, NULL, prefix, NULL,
                        0, NULL,
                        NULL);
  *list = ms3->list_container.start;
  return res;
}

uint8_t ms3_list(ms3_st *ms3, const char *bucket, const char *prefix,
                 ms3_list_st **list)
{
  uint8_t res = 0;

  if (!ms3 || !bucket || !list)
  {
    return MS3_ERR_PARAMETER;
  }

  list_free(ms3);
  res = execute_request(ms3, MS3_CMD_LIST_RECURSIVE, bucket, NULL, NULL, NULL,
                        prefix, NULL,
                        0, NULL,
                        NULL);
  *list = ms3->list_container.start;
  return res;
}

uint8_t ms3_put(ms3_st *ms3, const char *bucket, const char *key,
                const uint8_t *data, size_t length)
{
  uint8_t res;

  if (!ms3 || !bucket || !key || !data)
  {
    return MS3_ERR_PARAMETER;
  }

  if (length == 0)
  {
    return MS3_ERR_NO_DATA;
  }

  // mhash can't hash more than 4GB it seems
  if (length > UINT32_MAX)
  {
    return MS3_ERR_TOO_BIG;
  }

  res = execute_request(ms3, MS3_CMD_PUT, bucket, key, NULL, NULL, NULL, data,
                        length, NULL,
                        NULL);

  return res;
}

uint8_t ms3_get(ms3_st *ms3, const char *bucket, const char *key,
                uint8_t **data, size_t *length)
{
  uint8_t res = 0;
  struct memory_buffer_st buf;

  buf.data = NULL;
  buf.length = 0;

  if (!ms3 || !bucket || !key || key[0] == '\0')
  {
    return MS3_ERR_PARAMETER;
  }
  else if (!ms3->read_cb && (!data || !length))
  {
    return MS3_ERR_PARAMETER;
  }

  res = execute_request(ms3, MS3_CMD_GET, bucket, key, NULL, NULL, NULL, NULL, 0,
                        NULL, &buf);
  if (!ms3->read_cb)
  {
    *data = buf.data;
    *length = buf.length;
  }

  return res;
}

uint8_t ms3_copy(ms3_st *ms3, const char *source_bucket, const char *source_key,
                 const char *dest_bucket, const char *dest_key)
{
  uint8_t res = 0;

  if (!ms3 || !source_bucket || !source_key || !dest_bucket || !dest_key)
  {
    return MS3_ERR_PARAMETER;
  }

  res = execute_request(ms3, MS3_CMD_COPY, dest_bucket, dest_key, source_bucket,
                        source_key, NULL, NULL, 0, NULL, NULL);
  return res;
}

uint8_t ms3_move(ms3_st *ms3, const char *source_bucket, const char *source_key,
                 const char *dest_bucket, const char *dest_key)
{
  uint8_t res = 0;

  if (!ms3 || !source_bucket || !source_key || !dest_bucket || !dest_key)
  {
    return MS3_ERR_PARAMETER;
  }

  res = ms3_copy(ms3, source_bucket, source_key, dest_bucket, dest_key);

  if (res)
  {
    return res;
  }

  res = ms3_delete(ms3, source_bucket, source_key);

  return res;
}

uint8_t ms3_delete(ms3_st *ms3, const char *bucket, const char *key)
{
  uint8_t res;

  if (!ms3 || !bucket || !key)
  {
    return MS3_ERR_PARAMETER;
  }

  res = execute_request(ms3, MS3_CMD_DELETE, bucket, key, NULL, NULL, NULL, NULL,
                        0, NULL,
                        NULL);
  return res;
}

uint8_t ms3_status(ms3_st *ms3, const char *bucket, const char *key,
                   ms3_status_st *status)
{
  uint8_t res;

  if (!ms3 || !bucket || !key || !status)
  {
    return MS3_ERR_PARAMETER;
  }

  res = execute_request(ms3, MS3_CMD_HEAD, bucket, key, NULL, NULL, NULL, NULL, 0,
                        NULL,
                        status);
  return res;
}

void ms3_list_free(ms3_list_st *list)
{
  // Deprecated
  (void) list;
}

void ms3_free(uint8_t *data)
{
  ms3_cfree(data);
}

uint8_t ms3_set_option(ms3_st *ms3, ms3_set_option_t option, void *value)
{
  if (!ms3)
  {
    return MS3_ERR_PARAMETER;
  }

  switch (option)
  {
    case MS3_OPT_USE_HTTP:
    {
      ms3->use_http = ms3->use_http ? 0 : 1;
      break;
    }

    case MS3_OPT_DISABLE_SSL_VERIFY:
    {
      ms3->disable_verification = ms3->disable_verification ? 0 : 1;
      break;
    }

    case MS3_OPT_BUFFER_CHUNK_SIZE:
    {
      size_t new_size;

      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }

      new_size = *(size_t *)value;

      if (new_size < 1)
      {
        return MS3_ERR_PARAMETER;
      }

      ms3->buffer_chunk_size = new_size;
      break;
    }

    case MS3_OPT_FORCE_LIST_VERSION:
    {
      uint8_t list_version;

      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }

      list_version = *(uint8_t *)value;

      if (list_version < 1 || list_version > 2)
      {
        return MS3_ERR_PARAMETER;
      }

      ms3->list_version = list_version;
      break;
    }

    case MS3_OPT_FORCE_PROTOCOL_VERSION:
    {
      uint8_t protocol_version;

      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }

      protocol_version = *(uint8_t *)value;

      if (protocol_version < 1 || protocol_version > 2)
      {
        return MS3_ERR_PARAMETER;
      }

      ms3->protocol_version = protocol_version;
      break;
    }

    case MS3_OPT_PORT_NUMBER:
    {
      int port_number;

      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }
      memcpy(&port_number, (void*)value, sizeof(int));

      ms3->port = port_number;
      break;
    }

    case MS3_OPT_READ_CB:
    {
      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }

      ms3->read_cb = value;
      break;
    }

    case MS3_OPT_USER_DATA:
    {
      ms3->user_data = value;
      break;
    }

    case MS3_OPT_CONNECT_TIMEOUT:
    {
      float timeout;
      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }
      timeout = *(float *)value;
      if (timeout < 0 || timeout >= UINT32_MAX / 1000)
      {
        return MS3_ERR_PARAMETER;
      }
      ms3->connect_timeout_ms = timeout * 1000;
      break;
    }

    case MS3_OPT_TIMEOUT:
    {
      float timeout;
      if (!value)
      {
        return MS3_ERR_PARAMETER;
      }
      timeout = *(float *)value;
      if (timeout < 0 || timeout >= UINT32_MAX / 1000)
      {
        return MS3_ERR_PARAMETER;
      }
      ms3->timeout_ms = timeout * 1000;
      break;
    }

    default:
      return MS3_ERR_PARAMETER;
  }

  return 0;
}

uint8_t ms3_assume_role(ms3_st *ms3)
{
    uint8_t res = 0;

    if (!ms3 || !ms3->iam_role)
    {
      return MS3_ERR_PARAMETER;
    }

    if (!strstr(ms3->iam_role_arn, ms3->iam_role))
    {
        ms3debug("Lookup IAM role ARN");
        res = execute_assume_role_request(ms3, MS3_CMD_LIST_ROLE, NULL, 0, NULL);
        if(res)
        {
          return res;
        }

    }
    ms3debug("Assume IAM role");
    res = execute_assume_role_request(ms3, MS3_CMD_ASSUME_ROLE, NULL, 0, NULL);

    return res;
}

