/*
 * Copyright (c) 2016 MariaDB Corporation Ab
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 *
 * Change Date: 2020-01-01
 *
 * On the date above, in accordance with the Business Source License, use
 * of this software will be governed by version 2 or later of the General
 * Public License.
 */

/**
 * @file cdc_auth.c
 *
 * CDC Authentication module for handling the checking of clients credentials
 * in the CDC protocol.
 *
 * @verbatim
 * Revision History
 * Date         Who                     Description
 * 11/03/2016   Massimiliano Pinto      Initial version
 *
 * @endverbatim
 */

#define MXS_MODULE_NAME "CDCPlainAuth"

#include <maxscale/authenticator.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <cdc.h>
#include <maxscale/alloc.h>
#include <maxscale/modulecmd.h>
#include <maxscale/modutil.h>
#include <maxscale/paths.h>
#include <maxscale/secrets.h>
#include <maxscale/users.h>
#include <maxscale/utils.h>

/* Allowed time interval (in seconds) after last update*/
#define CDC_USERS_REFRESH_TIME 30
/* Max number of load calls within the time interval */
#define CDC_USERS_REFRESH_MAX_PER_TIME 4

const char CDC_USERS_FILENAME[] = "cdcusers";

static bool cdc_auth_set_protocol_data(DCB *dcb, GWBUF *buf);
static bool cdc_auth_is_client_ssl_capable(DCB *dcb);
static int  cdc_auth_authenticate(DCB *dcb);
static void cdc_auth_free_client_data(DCB *dcb);

static int cdc_set_service_user(SERV_LISTENER *listener);
static int cdc_replace_users(SERV_LISTENER *listener);

static int cdc_auth_check(
    DCB           *dcb,
    CDC_protocol  *protocol,
    char          *username,
    uint8_t       *auth_data,
    unsigned int  *flags
);

static bool cdc_auth_set_client_data(
    CDC_session *client_data,
    CDC_protocol *protocol,
    uint8_t *client_auth_packet,
    int client_auth_packet_size
);

/**
 * @brief Add a new CDC user
 *
 * This function should not be called directly. The module command system will
 * call it when necessary.
 *
 * @param args Arguments for this command
 * @return True if user was successfully added
 */
static bool cdc_add_new_user(const MODULECMD_ARG *args, json_t** output)
{
    const char *user = args->argv[1].value.string;
    size_t userlen = strlen(user);
    const char *password = args->argv[2].value.string;
    uint8_t phase1[SHA_DIGEST_LENGTH];
    uint8_t phase2[SHA_DIGEST_LENGTH];
    SHA1((uint8_t*)password, strlen(password), phase1);
    SHA1(phase1, sizeof(phase1), phase2);

    size_t data_size = userlen + 2 + SHA_DIGEST_LENGTH * 2; // Extra for the : and newline
    char final_data[data_size];
    strcpy(final_data, user);
    strcat(final_data, ":");
    gw_bin2hex(final_data + userlen + 1, phase2, sizeof(phase2));
    final_data[data_size - 1] = '\n';

    SERVICE *service = args->argv[0].value.service;
    char path[PATH_MAX + 1];
    snprintf(path, PATH_MAX, "%s/%s/", get_datadir(), service->name);
    bool rval = false;

    if (mxs_mkdir_all(path, 0777))
    {
        strcat(path, CDC_USERS_FILENAME);
        int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0660);

        if (fd != -1)
        {
            if (write(fd, final_data, sizeof(final_data)) == sizeof(final_data))
            {
                MXS_NOTICE("Added user '%s' to service '%s'", user, service->name);
                rval = true;
            }
            else
            {
                const char *real_err = mxs_strerror(errno);
                MXS_NOTICE("Failed to write to file '%s': %s", path, real_err);
                modulecmd_set_error("Failed to write to file '%s': %s", path, real_err);
            }

            close(fd);
        }
        else
        {
            const char *real_err = mxs_strerror(errno);
            MXS_NOTICE("Failed to open file '%s': %s", path, real_err);
            modulecmd_set_error("Failed to open file '%s': %s", path, real_err);
        }
    }
    else
    {
        modulecmd_set_error("Failed to create directory '%s'. Read the MaxScale "
                            "log for more details.", path);
    }

    return rval;
}

/**
 * The module entry point routine. It is this routine that
 * must populate the structure that is referred to as the
 * "module object", this is a structure with the set of
 * external entry points for this module.
 *
 * @return The module object
 */
MXS_MODULE* MXS_CREATE_MODULE()
{
    static modulecmd_arg_type_t args[] =
    {
        { MODULECMD_ARG_SERVICE, "Service where the user is added"},
        { MODULECMD_ARG_STRING, "User to add"},
        { MODULECMD_ARG_STRING, "Password of the user"}
    };

    modulecmd_register_command("cdc", "add_user", MODULECMD_TYPE_ACTIVE,
                               cdc_add_new_user, 3, args,
                               "Add a new CDC user");

    static MXS_AUTHENTICATOR MyObject =
    {
        NULL,                           /* No initialize entry point */
        NULL,                           /* No create entry point */
        cdc_auth_set_protocol_data,     /* Extract data into structure   */
        cdc_auth_is_client_ssl_capable, /* Check if client supports SSL  */
        cdc_auth_authenticate,          /* Authenticate user credentials */
        cdc_auth_free_client_data,      /* Free the client data held in DCB */
        NULL,                           /* No destroy entry point */
        cdc_replace_users,              /* Load CDC users */
        users_default_diagnostic,       /* Default diagnostic */
        users_default_diagnostic_json,  /* Default diagnostic */
        NULL                            /* No user reauthentication */
    };

    static MXS_MODULE info =
    {
        MXS_MODULE_API_AUTHENTICATOR,
        MXS_MODULE_GA,
        MXS_AUTHENTICATOR_VERSION,
        "The CDC client to MaxScale authenticator implementation",
        "V1.1.0",
        MXS_NO_MODULE_CAPABILITIES,
        &MyObject,
        NULL, /* Process init. */
        NULL, /* Process finish. */
        NULL, /* Thread init. */
        NULL, /* Thread finish. */
        { { MXS_END_MODULE_PARAMS} }
    };

    return &info;
}

/**
 * @brief Function to easily call authentication check.
 *
 * @param dcb Request handler DCB connected to the client
 * @param protocol  The protocol structure for the connection
 * @param username  String containing username
 * @param auth_data  The encrypted password for authentication
 * @return Authentication status
 * @note Authentication status codes are defined in cdc.h
 */
static int cdc_auth_check(DCB *dcb, CDC_protocol *protocol, char *username,
                          uint8_t *auth_data, unsigned int *flags)
{
    int rval = CDC_STATE_AUTH_FAILED;

    if (dcb->listener->users)
    {
        /* compute SHA1 of auth_data */
        uint8_t sha1_step1[SHA_DIGEST_LENGTH] = "";
        char hex_step1[2 * SHA_DIGEST_LENGTH + 1] = "";

        gw_sha1_str(auth_data, SHA_DIGEST_LENGTH, sha1_step1);
        gw_bin2hex(hex_step1, sha1_step1, SHA_DIGEST_LENGTH);

        if (users_auth(dcb->listener->users, username, hex_step1))
        {
            rval = CDC_STATE_AUTH_OK;
        }
    }

    return rval;
}

/**
 * @brief Authenticates a CDC user who is a client to MaxScale.
 *
 * @param dcb Request handler DCB connected to the client
 * @return Authentication status
 * @note Authentication status codes are defined in cdc.h
 */
static int
cdc_auth_authenticate(DCB *dcb)
{
    CDC_protocol *protocol = DCB_PROTOCOL(dcb, CDC_protocol);
    CDC_session *client_data = (CDC_session *)dcb->data;
    int auth_ret;

    if (0 == strlen(client_data->user))
    {
        auth_ret = CDC_STATE_AUTH_ERR;
    }
    else
    {
        MXS_DEBUG("Receiving connection from '%s'",
                  client_data->user);

        auth_ret = cdc_auth_check(dcb, protocol, client_data->user, client_data->auth_data, client_data->flags);

        /* On failed authentication try to reload users and authenticate again */
        if (CDC_STATE_AUTH_OK != auth_ret && cdc_replace_users(dcb->listener) == MXS_AUTH_LOADUSERS_OK)
        {
            auth_ret = cdc_auth_check(dcb, protocol, client_data->user, client_data->auth_data, client_data->flags);
        }

        /* on successful authentication, set user into dcb field */
        if (CDC_STATE_AUTH_OK == auth_ret)
        {
            dcb->user = MXS_STRDUP_A(client_data->user);
        }
        else if (dcb->service->log_auth_warnings)
        {
            MXS_NOTICE("%s: login attempt for user '%s', authentication failed.",
                       dcb->service->name, client_data->user);
        }
    }

    return auth_ret;
}

/**
 * @brief Transfer data from the authentication request to the DCB.
 *
 * The request handler DCB has a field called data that contains protocol
 * specific information. This function examines a buffer containing CDC
 * authentication data and puts it into a structure that is referred to
 * by the DCB. If the information in the buffer is invalid, then a failure
 * code is returned. A call to cdc_auth_set_client_data does the
 * detailed work.
 *
 * @param dcb Request handler DCB connected to the client
 * @param buffer Pointer to pointer to buffer containing data from client
 * @return True on success, false on error
 */
static bool
cdc_auth_set_protocol_data(DCB *dcb, GWBUF *buf)
{
    uint8_t *client_auth_packet = GWBUF_DATA(buf);
    CDC_protocol *protocol = NULL;
    CDC_session *client_data = NULL;
    int client_auth_packet_size = 0;

    protocol = DCB_PROTOCOL(dcb, CDC_protocol);
    CHK_PROTOCOL(protocol);
    if (dcb->data == NULL)
    {
        if (NULL == (client_data = (CDC_session *)MXS_CALLOC(1, sizeof(CDC_session))))
        {
            return false;
        }
        dcb->data = client_data;
    }
    else
    {
        client_data = (CDC_session *)dcb->data;
    }

    client_auth_packet_size = gwbuf_length(buf);

    return cdc_auth_set_client_data(client_data, protocol, client_auth_packet,
                                    client_auth_packet_size);
}

/**
 * @brief Transfer detailed data from the authentication request to the DCB.
 *
 * The caller has created the data structure pointed to by the DCB, and this
 * function fills in the details. If problems are found with the data, the
 * return code indicates failure.
 *
 * @param client_data The data structure for the DCB
 * @param protocol The protocol structure for this connection
 * @param client_auth_packet The data from the buffer received from client
 * @param client_auth_packet size An integer giving the size of the data
 * @return True on success, false on error
 */
static bool
cdc_auth_set_client_data(CDC_session *client_data,
                         CDC_protocol *protocol,
                         uint8_t *client_auth_packet,
                         int client_auth_packet_size)
{
    if (client_auth_packet_size % 2 != 0)
    {
        /** gw_hex2bin expects an even number of bytes */
        client_auth_packet_size--;
    }

    bool rval = false;
    int decoded_size = client_auth_packet_size / 2;
    char decoded_buffer[decoded_size + 1]; // Extra for terminating null

    /* decode input data */
    if (client_auth_packet_size <= CDC_USER_MAXLEN)
    {
        gw_hex2bin((uint8_t*)decoded_buffer, (const char *)client_auth_packet,
                   client_auth_packet_size);
        decoded_buffer[decoded_size] = '\0';
        char *tmp_ptr = strchr(decoded_buffer, ':');

        if (tmp_ptr)
        {
            size_t user_len = tmp_ptr - decoded_buffer;
            *tmp_ptr++ = '\0';
            size_t auth_len = decoded_size - (tmp_ptr - decoded_buffer);

            if (user_len <= CDC_USER_MAXLEN && auth_len == SHA_DIGEST_LENGTH)
            {
                strcpy(client_data->user, decoded_buffer);
                memcpy(client_data->auth_data, tmp_ptr, auth_len);
                rval = true;
            }
        }
        else
        {
            MXS_ERROR("Authentication failed, the decoded client authentication "
                      "packet is malformed. Expected <username>:SHA1(<password>)");
        }
    }
    else
    {
        MXS_ERROR("Authentication failed, client authentication packet length "
                  "exceeds the maximum allowed length of %d bytes.", CDC_USER_MAXLEN);
    }

    return rval;
}

/**
 * @brief Determine whether the client is SSL capable
 *
 * The authentication request from the client will indicate whether the client
 * is expecting to make an SSL connection. The information has been extracted
 * in the previous functions.
 *
 * @param dcb Request handler DCB connected to the client
 * @return Boolean indicating whether client is SSL capable
 */
static bool
cdc_auth_is_client_ssl_capable(DCB *dcb)
{
    return false;
}

/**
 * @brief Free the client data pointed to by the passed DCB.
 *
 * Currently all that is required is to free the storage pointed to by
 * dcb->data.  But this is intended to be implemented as part of the
 * authentication API at which time this code will be moved into the
 * CDC authenticator.  If the data structure were to become more complex
 * the mechanism would still work and be the responsibility of the authenticator.
 * The DCB should not know authenticator implementation details.
 *
 * @param dcb Request handler DCB connected to the client
 */
static void
cdc_auth_free_client_data(DCB *dcb)
{
    MXS_FREE(dcb->data);
}

/*
 * Add the service user to CDC dbusers (service->users)
 * via cdc_users_alloc
 *
 * @param service   The current service
 * @return      0 on success, 1 on failure
 */
static int
cdc_set_service_user(SERV_LISTENER *listener)
{
    SERVICE *service = listener->service;
    char *dpwd = NULL;
    char *newpasswd = NULL;
    char *service_user = NULL;
    char *service_passwd = NULL;

    if (serviceGetUser(service, &service_user, &service_passwd) == 0)
    {
        MXS_ERROR("failed to get service user details for service %s",
                  service->name);

        return 1;
    }

    dpwd = decrypt_password(service->credentials.authdata);

    if (!dpwd)
    {
        MXS_ERROR("decrypt password failed for service user %s, service %s",
                  service_user,
                  service->name);

        return 1;
    }

    newpasswd = create_hex_sha1_sha1_passwd(dpwd);

    if (!newpasswd)
    {
        MXS_ERROR("create hex_sha1_sha1_password failed for service user %s",
                  service_user);

        MXS_FREE(dpwd);
        return 1;
    }

    /* add service user */
    (void)users_add(listener->users, service->credentials.name, newpasswd, USER_ACCOUNT_ADMIN);

    MXS_FREE(newpasswd);
    MXS_FREE(dpwd);

    return 0;
}

/**
 * Load the AVRO users
 *
 * @param service    Current service
 * @param usersfile  File with users
 * @return -1 on error or users loaded (including 0)
 */

static int
cdc_read_users(USERS *users, char *usersfile)
{
    FILE  *fp;
    int loaded = 0;
    char *avro_user;
    char *user_passwd;
    /* user maxlen ':' password hash  '\n' '\0' */
    char read_buffer[CDC_USER_MAXLEN + 1 + SHA_DIGEST_LENGTH + 1 + 1];

    int max_line_size = sizeof(read_buffer) - 1;

    if ((fp = fopen(usersfile, "r")) == NULL)
    {
        return -1;
    }

    while (!feof(fp))
    {
        if (fgets(read_buffer, max_line_size, fp) != NULL)
        {
            char *tmp_ptr = read_buffer;

            if ((tmp_ptr = strchr(read_buffer, ':')) != NULL)
            {
                *tmp_ptr++ = '\0';
                avro_user = read_buffer;
                user_passwd = tmp_ptr;
                if ((tmp_ptr = strchr(user_passwd, '\n')) != NULL)
                {
                    *tmp_ptr = '\0';
                }

                /* add user */
                users_add(users, avro_user, user_passwd, USER_ACCOUNT_ADMIN);

                loaded++;
            }
        }
    }

    fclose(fp);

    return loaded;
}

/**
 * @brief Replace the user/passwd in the servicei users tbale from a db file
 *
 * @param service The current service
 */
int cdc_replace_users(SERV_LISTENER *listener)
{
    int rc = MXS_AUTH_LOADUSERS_ERROR;
    USERS *newusers = users_alloc();

    if (newusers)
    {
        char path[PATH_MAX + 1];
        snprintf(path, PATH_MAX, "%s/%s/%s", get_datadir(), listener->service->name,
                 CDC_USERS_FILENAME);

        int i = cdc_read_users(newusers, path);
        USERS *oldusers = NULL;

        spinlock_acquire(&listener->lock);

        if (i > 0)
        {
            /** Successfully loaded at least one user */
            oldusers = listener->users;
            listener->users = newusers;
            rc = MXS_AUTH_LOADUSERS_OK;
        }
        else if (listener->users)
        {
            /** Failed to load users, use the old users table */
            users_free(newusers);
        }
        else
        {
            /** No existing users, use the new empty users table */
            listener->users = newusers;
        }

        cdc_set_service_user(listener);

        spinlock_release(&listener->lock);

        if (oldusers)
        {
            users_free(oldusers);
        }
    }
    return rc;
}
