/* LibMemcached
 * Copyright (C) 2006-2010 Brian Aker
 * All rights reserved.
 *
 * Use and distribution licensed under the BSD license.  See
 * the COPYING file in the parent directory for full text.
 *
 * Summary: interface for memcached server
 * Description: main include file for libmemcached
 *
 */
#include "common.h"

void memcached_set_sasl_callbacks(memcached_st *ptr,
                                  const sasl_callback_t *callbacks)
{
  ptr->sasl.callbacks= callbacks;
  ptr->sasl.is_allocated= false;
}

const sasl_callback_t *memcached_get_sasl_callbacks(memcached_st *ptr)
{
  return ptr->sasl.callbacks;
}

/**
 * Resolve the names for both ends of a connection
 * @param fd socket to check
 * @param laddr local address (out)
 * @param raddr remote address (out)
 * @return true on success false otherwise (errno contains more info)
 */
static bool resolve_names(int fd, char *laddr, size_t laddr_length, char *raddr, size_t raddr_length)
{
  char host[NI_MAXHOST];
  char port[NI_MAXSERV];
  struct sockaddr_storage saddr;
  socklen_t salen= sizeof(saddr);

  if ((getsockname(fd, (struct sockaddr *)&saddr, &salen) < 0) ||
      (getnameinfo((struct sockaddr *)&saddr, salen, host, sizeof(host),
                   port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV) < 0))
  {
    return false;
  }

  (void)snprintf(laddr, laddr_length, "%s;%s", host, port);
  salen= sizeof(saddr);

  if ((getpeername(fd, (struct sockaddr *)&saddr, &salen) < 0) ||
      (getnameinfo((struct sockaddr *)&saddr, salen, host, sizeof(host),
                   port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV) < 0))
  {
    return false;
  }

  (void)snprintf(raddr, raddr_length, "%s;%s", host, port);

  return true;
}

memcached_return_t memcached_sasl_authenticate_connection(memcached_server_st *server)
{
  memcached_return_t rc;

  /* SANITY CHECK: SASL can only be used with the binary protocol */
  unlikely (!server->root->flags.binary_protocol)
    return MEMCACHED_FAILURE;

  /* Try to get the supported mech from the server. Servers without SASL
   * support will return UNKNOWN COMMAND, so we can just treat that
   * as authenticated
   */
  protocol_binary_request_no_extras request= {
    .message.header.request= {
      .magic= PROTOCOL_BINARY_REQ,
      .opcode= PROTOCOL_BINARY_CMD_SASL_LIST_MECHS
    }
  };

  if (memcached_io_write(server, request.bytes,
			 sizeof(request.bytes), 1) != sizeof(request.bytes))
  {
    return MEMCACHED_WRITE_FAILURE;
  }

  memcached_server_response_increment(server);

  char mech[MEMCACHED_MAX_BUFFER];
  rc= memcached_response(server, mech, sizeof(mech), NULL);
  if (rc != MEMCACHED_SUCCESS)
  {
    if (rc == MEMCACHED_PROTOCOL_ERROR)
    {
      /* If the server doesn't support SASL it will return PROTOCOL_ERROR.
       * This error may also be returned for other errors, but let's assume
       * that the server don't support SASL and treat it as success and
       * let the client fail with the next operation if the error was
       * caused by another problem....
       */
      rc= MEMCACHED_SUCCESS;
    }

    return rc;
  }

  /* set ip addresses */
  char laddr[NI_MAXHOST + NI_MAXSERV];
  char raddr[NI_MAXHOST + NI_MAXSERV];

  unlikely (!resolve_names(server->fd, laddr, sizeof(laddr), raddr, sizeof(raddr)))
  {
    server->cached_errno= errno;
    return MEMCACHED_ERRNO;
  }

  sasl_conn_t *conn;
  int ret= sasl_client_new("memcached", server->hostname, laddr, raddr,
			   server->root->sasl.callbacks, 0, &conn);
  if (ret != SASL_OK)
  {
    return MEMCACHED_AUTH_PROBLEM;
  }

  const char *data;
  const char *chosenmech;
  unsigned int len;
  ret= sasl_client_start(conn, mech, NULL, &data, &len, &chosenmech);

  if (ret != SASL_OK && ret != SASL_CONTINUE)
  {
    rc= MEMCACHED_AUTH_PROBLEM;
    goto end;
  }

  uint16_t keylen= (uint16_t)strlen(chosenmech);
  request.message.header.request.opcode= PROTOCOL_BINARY_CMD_SASL_AUTH;
  request.message.header.request.keylen= htons(keylen);
  request.message.header.request.bodylen= htonl(len + keylen);

  do {
    /* send the packet */

    struct libmemcached_io_vector_st vector[]=
    {
      { .length= sizeof(request.bytes), .buffer= request.bytes },
      { .length= keylen, .buffer= chosenmech },
      { .length= len, .buffer= data }
    };

    if (memcached_io_writev(server, vector, 3, true) == -1)
    {
      rc= MEMCACHED_WRITE_FAILURE;
      goto end;
    }
    memcached_server_response_increment(server);

    /* read the response */
    rc= memcached_response(server, NULL, 0, NULL);
    if (rc != MEMCACHED_AUTH_CONTINUE)
    {
      goto end;
    }

    ret= sasl_client_step(conn, memcached_result_value(&server->root->result),
                          (unsigned int)memcached_result_length(&server->root->result),
                          NULL, &data, &len);

    if (ret != SASL_OK && ret != SASL_CONTINUE)
    {
      rc= MEMCACHED_AUTH_PROBLEM;
      goto end;
    }

    request.message.header.request.opcode= PROTOCOL_BINARY_CMD_SASL_STEP;
    request.message.header.request.bodylen= htonl(len + keylen);
  } while (true);

end:
  /* Release resources */
  sasl_dispose(&conn);

  return rc;
}

static int get_username(void *context, int id, const char **result,
                        unsigned int *len)
{
  if (!context || !result || (id != SASL_CB_USER && id != SASL_CB_AUTHNAME))
  {
    return SASL_BADPARAM;
  }

  *result= context;
  if (len)
  {
    *len= (unsigned int)strlen(*result);
  }

  return SASL_OK;
}

static int get_password(sasl_conn_t *conn, void *context, int id,
                        sasl_secret_t **psecret)
{
  if (!conn || ! psecret || id != SASL_CB_PASS)
  {
    return SASL_BADPARAM;
  }

  *psecret= context;

  return SASL_OK;
}

memcached_return_t memcached_set_sasl_auth_data(memcached_st *ptr,
                                                const char *username,
                                                const char *password)
{
  if (ptr == NULL || username == NULL ||
      password == NULL || ptr->sasl.callbacks != NULL)
  {
    return MEMCACHED_FAILURE;
  }

  sasl_callback_t *cb= libmemcached_calloc(ptr, 4, sizeof(sasl_callback_t));
  char *name= libmemcached_malloc(ptr, strlen(username) + 1);
  size_t password_length= strlen(password);
  sasl_secret_t *secret= libmemcached_malloc(ptr, password_length +1 + sizeof(*secret));
  if (cb == NULL || name == NULL || secret == NULL)
  {
    libmemcached_free(ptr, cb);
    libmemcached_free(ptr, name);
    libmemcached_free(ptr, secret);
    return MEMCACHED_MEMORY_ALLOCATION_FAILURE;
  }

  secret->len= strlen(password);
  memcpy(secret->data, password, password_length);
  secret->data[password_length]= 0;

  cb[0].id= SASL_CB_USER;
  cb[0].proc= get_username;
  cb[0].context= strncpy(name, username, sizeof(cb[0].context));
  cb[1].id= SASL_CB_AUTHNAME;
  cb[1].proc= get_username;
  cb[1].context= name;
  cb[2].id= SASL_CB_PASS;
  cb[2].proc= get_password;
  cb[2].context= secret;
  cb[3].id= SASL_CB_LIST_END;

  ptr->sasl.callbacks= cb;
  ptr->sasl.is_allocated= true;

  return MEMCACHED_SUCCESS;
}

memcached_return_t memcached_destroy_sasl_auth_data(memcached_st *ptr)
{
   if (ptr == NULL || ptr->sasl.callbacks == NULL)
   {
     return MEMCACHED_FAILURE;
   }

   if (ptr->sasl.is_allocated)
   {
     libmemcached_free(ptr, ptr->sasl.callbacks[0].context);
     libmemcached_free(ptr, ptr->sasl.callbacks[2].context);
     libmemcached_free(ptr, (void*)ptr->sasl.callbacks);
     ptr->sasl.is_allocated= false;
   }

   ptr->sasl.callbacks= NULL;

   return MEMCACHED_SUCCESS;
}

memcached_return_t memcached_clone_sasl(memcached_st *clone, const  memcached_st *source)
{

  if (source->sasl.callbacks == NULL)
  {
    return MEMCACHED_SUCCESS;
  }

  /* Hopefully we are using our own callback mechanisms.. */
  if (source->sasl.callbacks[0].id == SASL_CB_USER &&
      source->sasl.callbacks[0].proc == get_username &&
      source->sasl.callbacks[1].id == SASL_CB_AUTHNAME &&
      source->sasl.callbacks[1].proc == get_username &&
      source->sasl.callbacks[2].id == SASL_CB_PASS &&
      source->sasl.callbacks[2].proc == get_password &&
      source->sasl.callbacks[3].id == SASL_CB_LIST_END)
  {
    sasl_secret_t *secret= source->sasl.callbacks[2].context;
    return memcached_set_sasl_auth_data(clone,
                                        source->sasl.callbacks[0].context,
                                        (const char*)secret->data);
  }

  /*
   * But we're not. It may work if we know what the user tries to pass
   * into the list, but if we don't know the ID we don't know how to handle
   * the context...
   */
  size_t total= 0;

  while (source->sasl.callbacks[total].id != SASL_CB_LIST_END)
  {
    switch (source->sasl.callbacks[total].id)
    {
    case SASL_CB_USER:
    case SASL_CB_AUTHNAME:
    case SASL_CB_PASS:
       break;
    default:
       /* I don't know how to deal with this... */
       return MEMCACHED_NOT_SUPPORTED;
    }

    ++total;
  }

  sasl_callback_t *cb= libmemcached_calloc(clone, total + 1, sizeof(sasl_callback_t));
  if (cb == NULL)
  {
    return MEMCACHED_MEMORY_ALLOCATION_FAILURE;
  }
  memcpy(cb, source->sasl.callbacks, (total + 1) * sizeof(sasl_callback_t));

  /* Now update the context... */
  for (size_t x= 0; x < total; ++x)
  {
    if (cb[x].id == SASL_CB_USER || cb[x].id == SASL_CB_AUTHNAME)
    {
      cb[x].context= libmemcached_malloc(clone, strlen(source->sasl.callbacks[x].context));

      if (cb[x].context == NULL)
      {
        /* Failed to allocate memory, clean up previously allocated memory */
        for (size_t y= 0; y < x; ++y)
        {
          libmemcached_free(clone, clone->sasl.callbacks[y].context);
        }

        libmemcached_free(clone, cb);
        return MEMCACHED_MEMORY_ALLOCATION_FAILURE;
      }
      strncpy(cb[x].context, source->sasl.callbacks[x].context, sizeof(cb[x].context));
    }
    else
    {
      sasl_secret_t *src = source->sasl.callbacks[x].context;
      sasl_secret_t *n = libmemcached_malloc(clone, src->len + 1 + sizeof(*n));
      if (n == NULL)
      {
        /* Failed to allocate memory, clean up previously allocated memory */
        for (size_t y= 0; y < x; ++y)
        {
          libmemcached_free(clone, clone->sasl.callbacks[y].context);
        }

        libmemcached_free(clone, cb);
        return MEMCACHED_MEMORY_ALLOCATION_FAILURE;
      }
      memcpy(n, src, src->len + 1 + sizeof(*n));
      cb[x].context= n;
    }
  }

  clone->sasl.callbacks= cb;
  clone->sasl.is_allocated= true;

  return MEMCACHED_SUCCESS;
}
