/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include "proton/sasl.h"
#include "proton/sasl-plugin.h"
#include "proton/transport.h"

#include <sasl/sasl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>

#ifndef CYRUS_SASL_MAX_BUFFSIZE
# define CYRUS_SASL_MAX_BUFFSIZE (32768) /* bytes */
#endif

// SASL implementation entry points
static void cyrus_sasl_prepare(pn_transport_t *transport);
static void cyrus_sasl_free(pn_transport_t *transport);
static const char *cyrus_sasl_list_mechs(pn_transport_t *transport);

static bool cyrus_sasl_init_server(pn_transport_t *transport);
static void cyrus_sasl_process_init(pn_transport_t *transport, const char *mechanism, const pn_bytes_t *recv);
static void cyrus_sasl_process_response(pn_transport_t *transport, const pn_bytes_t *recv);

static bool cyrus_sasl_init_client(pn_transport_t *transport);
static bool cyrus_sasl_process_mechanisms(pn_transport_t *transport, const char *mechs);
static void cyrus_sasl_process_challenge(pn_transport_t *transport, const pn_bytes_t *recv);
static void cyrus_sasl_process_outcome(pn_transport_t *transport);

static bool cyrus_sasl_can_encrypt(pn_transport_t *transport);
static ssize_t cyrus_sasl_max_encrypt_size(pn_transport_t *transport);
static ssize_t cyrus_sasl_encode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out);
static ssize_t cyrus_sasl_decode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out);

static const pnx_sasl_implementation sasl_impl = {
    cyrus_sasl_free,

    cyrus_sasl_list_mechs,

    cyrus_sasl_init_server,
    cyrus_sasl_init_client,

    cyrus_sasl_prepare,

    cyrus_sasl_process_init,
    cyrus_sasl_process_response,

    cyrus_sasl_process_mechanisms,
    cyrus_sasl_process_challenge,
    cyrus_sasl_process_outcome,

    cyrus_sasl_can_encrypt,
    cyrus_sasl_max_encrypt_size,
    cyrus_sasl_encode,
    cyrus_sasl_decode
};

extern const pnx_sasl_implementation * const cyrus_sasl_impl;
const pnx_sasl_implementation * const cyrus_sasl_impl = &sasl_impl;

// If the version of Cyrus SASL is too early for sasl_client_done()/sasl_server_done()
// don't do any global clean up as it's not safe to use just sasl_done() for an
// executable that uses both client and server parts of Cyrus SASL, because it can't
// be called twice.
#if SASL_VERSION_FULL<0x020118
# define sasl_client_done()
# define sasl_server_done()
#endif

static const char *amqp_service = "amqp";

static inline bool pni_check_result(sasl_conn_t *conn, int r, pn_transport_t *logger, const char* condition_name)
{
    if (r==SASL_OK) return true;

    const char* err = conn ? sasl_errdetail(conn) : sasl_errstring(r, NULL, NULL);
    pnx_sasl_logf(logger, "sasl error: %s", err);
    pn_condition_t* c = pn_transport_condition(logger);
    pn_condition_set_name(c, condition_name);
    pn_condition_set_description(c, err);
    return false;
}

static bool pni_check_io_result(sasl_conn_t *conn, int r, pn_transport_t *logger)
{
    return pni_check_result(conn, r, logger, "proton:io:sasl_error");
}

static bool pni_check_sasl_result(sasl_conn_t *conn, int r, pn_transport_t *logger)
{
    return pni_check_result(conn, r, logger, "amqp:unauthorized-access");
}

// Cyrus wrappers
static void pni_cyrus_interact(pn_transport_t *transport, sasl_interact_t *interact)
{
  for (sasl_interact_t *i = interact; i->id!=SASL_CB_LIST_END; i++) {
    switch (i->id) {
    case SASL_CB_USER:
      i->result = 0;
      i->len = 0;
      break;
    case SASL_CB_AUTHNAME: {
      const char *username = pnx_sasl_get_username(transport);
      i->result = username;
      i->len = strlen(username);
      break;
    }
    case SASL_CB_PASS: {
      const char *password = pnx_sasl_get_password(transport);
      i->result = password;
      i->len = strlen(password);
      break;
    }
    default:
      fprintf(stderr, "(%s): %s - %s\n", i->challenge, i->prompt, i->defresult);
    }
  }
}

const char *cyrus_sasl_list_mechs(pn_transport_t *transport)
{
  sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
  if (!cyrus_conn) return NULL;

  int count = 0;
  const char *result = NULL;
  int r = sasl_listmech(cyrus_conn, NULL, "", " ", "", &result, NULL, &count);
  pni_check_sasl_result(cyrus_conn, r, transport);
  return result;
}

// Set up callbacks to use interact
static const sasl_callback_t pni_user_password_callbacks[] = {
    {SASL_CB_USER, NULL, NULL},
    {SASL_CB_AUTHNAME, NULL, NULL},
    {SASL_CB_PASS, NULL, NULL},
    {SASL_CB_LIST_END, NULL, NULL},
};

static const sasl_callback_t pni_user_callbacks[] = {
    {SASL_CB_USER, NULL, NULL},
    {SASL_CB_AUTHNAME, NULL, NULL},
    {SASL_CB_LIST_END, NULL, NULL},
};

// Machinery to initialise the cyrus library only once even in a multithreaded environment
// Relies on pthreads.
static const char * const default_config_name = "proton-server";
static char *pni_cyrus_config_dir = NULL;
static char *pni_cyrus_config_name = NULL;
static pthread_mutex_t pni_cyrus_mutex = PTHREAD_MUTEX_INITIALIZER;
static bool pni_cyrus_client_started = false;
static bool pni_cyrus_server_started = false;

bool pn_sasl_extended(void)
{
  return true;
}

void pn_sasl_config_name(pn_sasl_t *sasl0, const char *name)
{
    if (!pni_cyrus_config_name) {
      pni_cyrus_config_name = strdup(name);
    }
}

void pn_sasl_config_path(pn_sasl_t *sasl0, const char *dir)
{
  if (!pni_cyrus_config_dir) {
    pni_cyrus_config_dir = strdup(dir);
  }
}

__attribute__((destructor))
static void pni_cyrus_finish(void) {
  pthread_mutex_lock(&pni_cyrus_mutex);
  if (pni_cyrus_client_started) sasl_client_done();
  if (pni_cyrus_server_started) sasl_server_done();
  free(pni_cyrus_config_dir);
  free(pni_cyrus_config_name);
  pthread_mutex_unlock(&pni_cyrus_mutex);
}

static int pni_cyrus_client_init_rc = SASL_OK;
static void pni_cyrus_client_once(void) {
  pthread_mutex_lock(&pni_cyrus_mutex);
  int result = SASL_OK;
  if (pni_cyrus_config_dir) {
    result = sasl_set_path(SASL_PATH_TYPE_CONFIG, pni_cyrus_config_dir);
  } else {
    char *config_dir = getenv("PN_SASL_CONFIG_PATH");
    if (config_dir) {
      result = sasl_set_path(SASL_PATH_TYPE_CONFIG, config_dir);
    }
  }
  if (result==SASL_OK) {
    result = sasl_client_init(NULL);
  }
  pni_cyrus_client_started = true;
  pni_cyrus_client_init_rc = result;
  pthread_mutex_unlock(&pni_cyrus_mutex);
}

static int pni_cyrus_server_init_rc = SASL_OK;
static void pni_cyrus_server_once(void) {
  pthread_mutex_lock(&pni_cyrus_mutex);
  int result = SASL_OK;
  if (pni_cyrus_config_dir) {
    result = sasl_set_path(SASL_PATH_TYPE_CONFIG, pni_cyrus_config_dir);
  } else {
    char *config_dir = getenv("PN_SASL_CONFIG_PATH");
    if (config_dir) {
      result = sasl_set_path(SASL_PATH_TYPE_CONFIG, config_dir);
    }
  }
  if (result==SASL_OK) {
    result = sasl_server_init(NULL, pni_cyrus_config_name ? pni_cyrus_config_name : default_config_name);
  }
  pni_cyrus_server_started = true;
  pni_cyrus_server_init_rc = result;
  pthread_mutex_unlock(&pni_cyrus_mutex);
}

static pthread_once_t pni_cyrus_client_init = PTHREAD_ONCE_INIT;
static void pni_cyrus_client_start(void) {
  pthread_once(&pni_cyrus_client_init, pni_cyrus_client_once);
}
static pthread_once_t pni_cyrus_server_init = PTHREAD_ONCE_INIT;
static void pni_cyrus_server_start(void) {
  pthread_once(&pni_cyrus_server_init, pni_cyrus_server_once);
}

void cyrus_sasl_prepare(pn_transport_t* transport)
{
}

bool cyrus_sasl_init_client(pn_transport_t* transport) {
  int result;
  sasl_conn_t *cyrus_conn = NULL;
  do {
    pni_cyrus_client_start();
    result = pni_cyrus_client_init_rc;
    if (result!=SASL_OK) break;

    const sasl_callback_t *callbacks =
      pnx_sasl_get_username(transport) ? pnx_sasl_get_password(transport) ? pni_user_password_callbacks : pni_user_callbacks : NULL;
    result = sasl_client_new(amqp_service,
                             pnx_sasl_get_remote_fqdn(transport),
                             NULL, NULL,
                             callbacks, 0,
                             &cyrus_conn);
    if (result!=SASL_OK) break;
    pnx_sasl_set_context(transport, cyrus_conn);

    sasl_security_properties_t secprops = {0};
    secprops.security_flags =
      ( pnx_sasl_get_allow_insecure_mechs(transport) ? 0 : SASL_SEC_NOPLAINTEXT ) |
      ( pnx_sasl_get_auth_required(transport) ? SASL_SEC_NOANONYMOUS : 0 ) ;
    secprops.min_ssf = 0;
    secprops.max_ssf = 2048;
    secprops.maxbufsize = CYRUS_SASL_MAX_BUFFSIZE;

    result = sasl_setprop(cyrus_conn, SASL_SEC_PROPS, &secprops);
    if (result!=SASL_OK) break;

    sasl_ssf_t ssf = pnx_sasl_get_external_ssf(transport);
    result = sasl_setprop(cyrus_conn, SASL_SSF_EXTERNAL, &ssf);
    if (result!=SASL_OK) break;

    const char *extid = pnx_sasl_get_external_username(transport);
    if (extid) {
      result = sasl_setprop(cyrus_conn, SASL_AUTH_EXTERNAL, extid);
    }
  } while (false);
  cyrus_conn = (sasl_conn_t*) pnx_sasl_get_context(transport);
  return pni_check_sasl_result(cyrus_conn, result, transport);
}

static int pni_wrap_client_start(pn_transport_t *transport, const char *mechs, const char **mechusing)
{
    int result;
    sasl_interact_t *client_interact=NULL;
    const char *out;
    unsigned outlen;

    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    do {

        result = sasl_client_start(cyrus_conn,
                                   mechs,
                                   &client_interact,
                                   &out, &outlen,
                                   mechusing);
        if (result==SASL_INTERACT) {
            pni_cyrus_interact(transport, client_interact);
        }
    } while (result==SASL_INTERACT);

    pnx_sasl_set_bytes_out(transport, pn_bytes(outlen, out));
    return result;
}

bool cyrus_sasl_process_mechanisms(pn_transport_t *transport, const char *mechs)
{
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    const char *mech_selected;
    int result = pni_wrap_client_start(transport, mechs, &mech_selected);
    switch (result) {
        case SASL_OK:
        case SASL_CONTINUE:
          pnx_sasl_set_selected_mechanism(transport, mech_selected);
          pnx_sasl_set_desired_state(transport, SASL_POSTED_INIT);
          return true;
        case SASL_NOMECH:
        default:
          pni_check_sasl_result(cyrus_conn, result, transport);
          return false;
    }
}


static int pni_wrap_client_step(pn_transport_t *transport, const pn_bytes_t *in)
{
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    sasl_interact_t *client_interact=NULL;
    const char *out;
    unsigned outlen;

    int result;
    do {

        result = sasl_client_step(cyrus_conn,
                                  in->start, in->size,
                                  &client_interact,
                                  &out, &outlen);
        if (result==SASL_INTERACT) {
            pni_cyrus_interact(transport, client_interact);
        }
    } while (result==SASL_INTERACT);

    pnx_sasl_set_bytes_out(transport, pn_bytes(outlen, out));
    return result;
}

void cyrus_sasl_process_challenge(pn_transport_t *transport, const pn_bytes_t *recv)
{
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    int result = pni_wrap_client_step(transport, recv);
    switch (result) {
        case SASL_OK:
            // Authenticated
            // TODO: Documented that we need to call sasl_client_step() again to be sure!;
        case SASL_CONTINUE:
            // Need to send a response
            pnx_sasl_set_desired_state(transport, SASL_POSTED_RESPONSE);
            break;
        default:
            pni_check_sasl_result(cyrus_conn, result, transport);

            // Failed somehow - equivalent to failing authentication
            pnx_sasl_fail_authentication(transport);
            pnx_sasl_set_desired_state(transport, SASL_RECVED_OUTCOME_FAIL);
            break;
    }
}

void cyrus_sasl_process_outcome(pn_transport_t* transport)
{
}

bool cyrus_sasl_init_server(pn_transport_t* transport)
{
  int result;
  sasl_conn_t *cyrus_conn = NULL;
  do {
    pni_cyrus_server_start();
    result = pni_cyrus_server_init_rc;
    if (result!=SASL_OK) break;

    result = sasl_server_new(amqp_service, NULL, NULL, NULL, NULL, NULL, 0, &cyrus_conn);
    if (result!=SASL_OK) break;
    pnx_sasl_set_context(transport, cyrus_conn);

    sasl_security_properties_t secprops = {0};
    secprops.security_flags =
      ( pnx_sasl_get_allow_insecure_mechs(transport) ? 0 : SASL_SEC_NOPLAINTEXT ) |
      ( pnx_sasl_get_auth_required(transport) ? SASL_SEC_NOANONYMOUS : 0 ) ;
    secprops.min_ssf = 0;
    secprops.max_ssf = 2048;
    secprops.maxbufsize = CYRUS_SASL_MAX_BUFFSIZE;

    result = sasl_setprop(cyrus_conn, SASL_SEC_PROPS, &secprops);
    if (result!=SASL_OK) break;

    sasl_ssf_t ssf = pnx_sasl_get_external_ssf(transport);
    result = sasl_setprop(cyrus_conn, SASL_SSF_EXTERNAL, &ssf);
    if (result!=SASL_OK) break;

    const char *extid = pnx_sasl_get_external_username(transport);
    if (extid) {
    result = sasl_setprop(cyrus_conn, SASL_AUTH_EXTERNAL, extid);
    }
  } while (false);
  cyrus_conn = (sasl_conn_t*) pnx_sasl_get_context(transport);
  if (pni_check_sasl_result(cyrus_conn, result, transport)) {
      // Setup to send SASL mechanisms frame
      pnx_sasl_set_desired_state(transport, SASL_POSTED_MECHANISMS);
      return true;
  } else {
      return false;
  }
}

static int pni_wrap_server_start(pn_transport_t *transport, const char *mech_selected, const pn_bytes_t *in)
{
    int result;
    const char *out;
    unsigned outlen;
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    const char *in_bytes = in->start;
    size_t in_size = in->size;
    // Interop hack for ANONYMOUS - some of the earlier versions of proton will send and no data
    // with an anonymous init because it is optional. It seems that Cyrus wants an empty string here
    // or it will challenge, which the earlier implementation is not prepared for.
    // However we can't just always use an empty string as the CRAM-MD5 mech won't allow any data in the server start
    if (!in_bytes && strcmp(mech_selected, "ANONYMOUS")==0) {
        in_bytes = "";
        in_size = 0;
    } else if (in_bytes && strcmp(mech_selected, "CRAM-MD5")==0) {
        in_bytes = 0;
        in_size = 0;
    }
    result = sasl_server_start(cyrus_conn,
                               mech_selected,
                               in_bytes, in_size,
                               &out, &outlen);

    pnx_sasl_set_bytes_out(transport, pn_bytes(outlen, out));
    return result;
}

static void pni_process_server_result(pn_transport_t *transport, int result)
{
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    switch (result) {
        case SASL_OK: {
            // Authenticated
            // Get username from SASL
            const void* value;
            sasl_getprop(cyrus_conn, SASL_USERNAME, &value);
            pnx_sasl_succeed_authentication(transport, (const char*) value);
            pnx_sasl_logf(transport, "Authenticated user: %s with mechanism %s",
                          pnx_sasl_get_username(transport), pnx_sasl_get_selected_mechanism(transport));
            pnx_sasl_set_desired_state(transport, SASL_POSTED_OUTCOME);
            break;
        }
        case SASL_CONTINUE:
            // Need to send a challenge
            pnx_sasl_set_desired_state(transport, SASL_POSTED_CHALLENGE);
            break;
        default:
            pni_check_sasl_result(cyrus_conn, result, transport);

            // Failed to authenticate
            pnx_sasl_fail_authentication(transport);
            pnx_sasl_set_desired_state(transport, SASL_POSTED_OUTCOME);
            break;
    }
}

void cyrus_sasl_process_init(pn_transport_t *transport, const char *mechanism, const pn_bytes_t *recv)
{
    int result = pni_wrap_server_start(transport, mechanism, recv);
    if (result==SASL_OK) {
        // We need to filter out a supplied mech in in the inclusion list
        // as the client could have used a mech that we support, but that
        // wasn't on the list we sent.
        if (!pnx_sasl_is_included_mech(transport, pn_bytes(strlen(mechanism), mechanism))) {
            sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
            sasl_seterror(cyrus_conn, 0, "Client mechanism not in mechanism inclusion list.");
            result = SASL_FAIL;
        }
    }
    pni_process_server_result(transport, result);
}

static int pni_wrap_server_step(pn_transport_t *transport, const pn_bytes_t *in)
{
    int result;
    const char *out;
    unsigned outlen;
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    result = sasl_server_step(cyrus_conn,
                              in->start, in->size,
                              &out, &outlen);

    pnx_sasl_set_bytes_out(transport, pn_bytes(outlen, out));
    return result;
}

void cyrus_sasl_process_response(pn_transport_t *transport, const pn_bytes_t *recv)
{
    int result = pni_wrap_server_step(transport, recv);
    pni_process_server_result(transport, result);
}

bool cyrus_sasl_can_encrypt(pn_transport_t *transport)
{
  sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
  if (!cyrus_conn) return false;

  // Get SSF to find out if we need to encrypt or not
  const void* value;
  int r = sasl_getprop(cyrus_conn, SASL_SSF, &value);
  if (r != SASL_OK) {
    // TODO: Should log an error here too, maybe assert here
    return false;
  }
  int ssf = *(int *) value;
  if (ssf > 0) {
    return true;
  }
  return false;
}

ssize_t cyrus_sasl_max_encrypt_size(pn_transport_t *transport)
{
  sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
  if (!cyrus_conn) return PN_ERR;

  const void* value;
  int r = sasl_getprop(cyrus_conn, SASL_MAXOUTBUF, &value);
  if (r != SASL_OK) {
    // TODO: Should log an error here too, maybe assert here
    return PN_ERR;
  }
  int outbuf_size = *(int *) value;
  return outbuf_size -
    // XXX: this  is a clientside workaround/hack to make GSSAPI work as the Cyrus SASL
    // GSSAPI plugin seems to return an incorrect value for the buffer size on the client
    // side, which is greater than the value returned on the server side. Actually using
    // the entire client side buffer will cause a server side error due to a buffer overrun.
    (pnx_sasl_is_client(transport)? 60 : 0);
}

ssize_t cyrus_sasl_encode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out)
{
  if ( in.size==0 ) return 0;
  sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
  const char *output;
  unsigned int outlen;
  int r = sasl_encode(cyrus_conn, in.start, in.size, &output, &outlen);
  if (outlen==0) return 0;
  if ( pni_check_io_result(cyrus_conn, r, transport) ) {
    *out = pn_bytes(outlen, output);
    return outlen;
  }
  return PN_ERR;
}

ssize_t cyrus_sasl_decode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out)
{
  if ( in.size==0 ) return 0;
  sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
  const char *output;
  unsigned int outlen;
  int r = sasl_decode(cyrus_conn, in.start, in.size, &output, &outlen);
  if (outlen==0) return 0;
  if ( pni_check_io_result(cyrus_conn, r, transport) ) {
    *out = pn_bytes(outlen, output);
    return outlen;
  }
  return PN_ERR;
}

void cyrus_sasl_free(pn_transport_t *transport)
{
    sasl_conn_t *cyrus_conn = (sasl_conn_t*)pnx_sasl_get_context(transport);
    sasl_dispose(&cyrus_conn);
    pnx_sasl_set_context(transport, cyrus_conn);
}
