/*
 *  SPDX-FileCopyrightText: 2014 Daniel Vrátil <dvratil@redhat.com>
 *            (c) 2016 - 2018  Daniel Vrátil <dvratil@kde.org>
 *
 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
 */


/**
 * This is a very simple and single-purpose implementation of XOAUTH2 protocol
 * for SASL used by KDE's KIMAP and IMAP resource in order to support OAuth
 * Gmail authentication.
 *
 * This plugin does not even have a server-side part, and the client-side makes
 * assumptions about it's use related to how KIMAP's LoginJob is implemented,
 * so it's unsuitable for general-purpose use.
 *
 * The plugin is called libkdexoauth2.so to avoid conflict in case anyone ever
 * writes a proper XOAUTH2 plugin for SASL.
 */

#include <config.h>
#include <stdio.h>
#include <string.h>

#include <sasl/sasl.h>
#include <sasl/saslplug.h>
#include <sasl/saslutil.h>

#include "plugin_common.h"

typedef struct client_context {
    char *out_buf;
    unsigned out_buf_len;
} client_context_t;

static int xoauth2_client_mech_new(void *glob_context,
                                   sasl_client_params_t *params,
                                   void **conn_context)
{
    client_context_t *context;

    /* Q_UNUSED */
    (void)glob_context;

    /* holds state are in */
    context = params->utils->malloc(sizeof(client_context_t));
    if (context == NULL) {
        MEMERROR(params->utils);
        return SASL_NOMEM;
    }

    memset(context, 0, sizeof(client_context_t));

    *conn_context = context;

    return SASL_OK;
}

static int xoauth2_client_mech_step(void *conn_context,
                                    sasl_client_params_t *params,
                                    const char *serverin,
                                    unsigned serverinlen,
                                    sasl_interact_t **prompt_need,
                                    const char **clientout,
                                    unsigned *clientoutlen,
                                    sasl_out_params_t *oparams)
{
    client_context_t *context = (client_context_t *) conn_context;
    const sasl_utils_t *utils = params->utils;
    const char *authid = NULL;
    const char *token = NULL;
    int auth_result = SASL_OK;
    int token_result = SASL_OK;
    int result;
    *clientout = NULL;
    *clientoutlen = 0;

    /* Q_UNUSED */
    (void) serverin;
    (void) serverinlen;

    if (params->props.min_ssf > params->external_ssf) {
        SETERROR(params->utils, "SSF requested of XOAUTH2 plugin");
        return SASL_TOOWEAK;
    }

    /* try to get the authid */
    if (oparams->authid == NULL) {
        fprintf(stderr, "[SASL-XOAUTH2] - Requesting authID!\n");
        auth_result = _plug_get_authid(utils, &authid, prompt_need);
        if ((auth_result != SASL_OK) && (auth_result != SASL_INTERACT)) {
            fprintf(stderr, "[SASL-XOAUTH2] - _plug_get_authid FAILED!\n");
            return auth_result;
        }
    }

    /* try to get oauth token */
    if (token == NULL) {
        fprintf(stderr, "[SASL-XOAUTH2] - Requesting token!\n");
        /* We don't use _plug_get_password because we don't really care much about
           safety of the OAuth token */
        token_result = _plug_get_simple(utils, SASL_CB_PASS, 1, &token, prompt_need);
        if ((token_result != SASL_OK) && (token_result != SASL_INTERACT)) {
            fprintf(stderr, "[SASL-XOAUTH2] - _plug_get_simple FAILED!\n");
            return token_result;
        }
    }

    /* free prompts we got */
    if (prompt_need && *prompt_need) {
        utils->free(*prompt_need);
        *prompt_need = NULL;
    }

    /* if there are prompts not filled in */
    if ((auth_result == SASL_INTERACT) || (token_result == SASL_INTERACT)) {
        /* make the prompt list */
        fprintf(stderr, "[SASL-XOAUTH2] - filling prompts!\n");
        result =
            _plug_make_prompts(utils, prompt_need,
                               NULL, NULL,
                               auth_result == SASL_INTERACT ?
                               "Please enter your authentication name" : NULL,
                               NULL,
                               token_result == SASL_INTERACT ?
                               "Please enter OAuth token" : NULL, NULL,
                               NULL, NULL, NULL,
                               NULL, NULL, NULL);
        if (result != SASL_OK) {
            fprintf(stderr, "[SASL-XOAUTH2] - filling prompts failed FAILED!\n");
            return result;
        }

        return SASL_INTERACT;
    }

    /* FIXME: Fail if username is missing too? */
    if (!token) {
        PARAMERROR(params->utils);
        return SASL_BADPARAM;
    }

    result = params->canon_user(utils->conn, authid, 0,
                                SASL_CU_AUTHID | SASL_CU_AUTHZID, oparams);
    if (result != SASL_OK) {
        fprintf(stderr, "[SASL-XOAUTH2] - canon user FAILED!\n");
        return result;
    }

    /* https://developers.google.com/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */
    *clientoutlen = 5                                               /* user=*/
                    + ((authid && *authid) ? strlen(authid) : 0)      /* %s */
                    + 1                                               /* \001 */
                    + 12                                              /* auth=Bearer{space} */
                    + ((token && *token) ? strlen(token) : 0)         /* %s */
                    + 2;                                              /* \001\001 */

    /* remember the extra NUL on the end for stupid clients */
    result = _plug_buf_alloc(params->utils, &(context->out_buf),
                             &(context->out_buf_len), *clientoutlen + 1);
    if (result != SASL_OK) {
        fprintf(stderr, "[SASL-XOAUTH2] - _plug_buf_alloc FAILED!\n");
        return result;
    }

    snprintf(context->out_buf, context->out_buf_len, "user=%s\1auth=Bearer %s\1\1", authid, token);

    *clientout = context->out_buf;

    /* set oparams */
    oparams->doneflag = 1;
    oparams->mech_ssf = 0;
    oparams->maxoutbuf = 0;
    oparams->encode_context = NULL;
    oparams->encode = NULL;
    oparams->decode_context = NULL;
    oparams->decode = NULL;
    oparams->param_version = 0;

    return SASL_OK;
}

static void xoauth2_client_mech_dispose(void *conn_context,
                                        const sasl_utils_t *utils)
{
    client_context_t *context = (client_context_t *) conn_context;
    if (!context) {
        return;
    }

    if (context->out_buf) {
        utils->free(context->out_buf);
    }
    utils->free(context);
}

static sasl_client_plug_t xoauth2_client_plugins[] = {
    {
        "XOAUTH2",                      /* mech_name */
        0,                              /* max_ssf */
        SASL_SEC_NOANONYMOUS
        | SASL_SEC_PASS_CREDENTIALS,    /* security_flags */
        SASL_FEAT_WANT_CLIENT_FIRST
        | SASL_FEAT_ALLOWS_PROXY,       /* features */
        NULL,                           /* required_prompts */
        NULL,                           /* glob_context */
        &xoauth2_client_mech_new,       /* mech_new */
        &xoauth2_client_mech_step,      /* mech_step */
        &xoauth2_client_mech_dispose,   /* mech_dispose */
        NULL,                           /* mech_free */
        NULL,                           /* idle */
        NULL,                           /* spare */
        NULL                            /* spare */
    }
};

int xoauth2_client_plug_init(sasl_utils_t *utils,
                             int maxversion,
                             int *out_version,
                             sasl_client_plug_t **pluglist,
                             int *plugcount)
{
    if (maxversion < SASL_CLIENT_PLUG_VERSION) {
        SETERROR(utils, "XOAUTH2 version mismatch");
        return SASL_BADVERS;
    }

    *out_version = SASL_CLIENT_PLUG_VERSION;
    *pluglist = xoauth2_client_plugins;
    *plugcount = 1;

    return SASL_OK;
}
