/* saslclient.c -- shared SASL code for server-server authentication
 *
 * Copyright (c) 1994-2008 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

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

#include "xmalloc.h"
#include "saslclient.h"
#include "global.h"

/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"

static int mysasl_simple_cb(void *context, int id, const char **result,
                            unsigned int *len)
{
    if (!result) {
        return SASL_BADPARAM;
    }

    switch (id) {
    case SASL_CB_USER:
        *result = (char *) context;
        break;
    case SASL_CB_AUTHNAME:
        *result = (char *) context;
        break;
    case SASL_CB_LANGUAGE:
        *result = NULL;
        break;
    default:
        return SASL_BADPARAM;
    }
    if (len) {
        *len = *result ? strlen(*result) : 0;
    }

    return SASL_OK;
}

static int mysasl_getrealm_cb(void *context, int id,
                              const char **availrealms __attribute__((unused)),
                              const char **result)
{
    if (id != SASL_CB_GETREALM || !result) {
        return SASL_BADPARAM;
    }

    *result = (char *) context;
    return SASL_OK;
}

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

    *result = (sasl_secret_t *)context;

    return SASL_OK;
}

EXPORTED sasl_callback_t *mysasl_callbacks(const char *username,
                                  const char *authname,
                                  const char *realm,
                                  const char *password)
{
    sasl_callback_t *ret = xmalloc(5 * sizeof(sasl_callback_t));
    int n = 0;

    if (username) {
        /* user callback */
        ret[n].id = SASL_CB_USER;
        ret[n].proc = (mysasl_cb_ft *) &mysasl_simple_cb;
        ret[n].context = (char *) username;
        n++;
    }

    if (authname) {
        /* authname */
        ret[n].id = SASL_CB_AUTHNAME;
        ret[n].proc = (mysasl_cb_ft *) &mysasl_simple_cb;
        ret[n].context = (char *) authname;
        n++;
    }

    if (realm) {
        /* realm */
        ret[n].id = SASL_CB_GETREALM;
        ret[n].proc = (mysasl_cb_ft *) &mysasl_getrealm_cb;
        ret[n].context = (char *) realm;
        n++;
    }

    if (password) {
        sasl_secret_t *secret;
        size_t len = strlen(password);

        secret = (sasl_secret_t *)xmalloc(sizeof(sasl_secret_t) + len);
        strcpy((char *) secret->data, password);
        secret->len = len;

        /* password */
        ret[n].id = SASL_CB_PASS;
        ret[n].proc = (mysasl_cb_ft *) &mysasl_getsecret_cb;
        ret[n].context = secret;
        n++;
    }

    ret[n].id = SASL_CB_LIST_END;
    ret[n].proc = NULL;
    ret[n].context = NULL;

    return ret;
}

EXPORTED void free_callbacks(sasl_callback_t *in)
{
    int i;
    if(!in) return;

    for(i=0; in[i].id != SASL_CB_LIST_END; i++)
        if(in[i].id == SASL_CB_PASS)
            free(in[i].context);

    free(in);
}

#define BASE64_BUF_SIZE 21848   /* per RFC 2222bis: ((16K / 3) + 1) * 4  */
#define AUTH_BUF_SIZE   BASE64_BUF_SIZE+50      /* data + response overhead */

HIDDEN int saslclient(sasl_conn_t *conn, struct sasl_cmd_t *sasl_cmd,
               const char *mechlist,
               struct protstream *pin, struct protstream *pout,
               int *sasl_result, const char **status)
{
    static char buf[AUTH_BUF_SIZE+1];
    char *base64, *serverin;
    unsigned int serverinlen = 0;
    const char *mech, *clientout = NULL;
    unsigned int clientoutlen = 0;
    char cmdbuf[40];
    int sendliteral = sasl_cmd->quote;
    int r;

    if (status) *status = NULL;

    r = sasl_client_start(conn, mechlist, NULL,
                          /* do we support initial response? */
                          sasl_cmd->maxlen ? &clientout : NULL,
                          &clientoutlen, &mech);

    if (r != SASL_OK && r != SASL_CONTINUE) {
        if (sasl_result) *sasl_result = r;
        if (status) *status = sasl_errdetail(conn);
        return IMAP_SASL_FAIL;
    }

    /* build the auth command */
    if (sasl_cmd->quote)
        sprintf(cmdbuf, "%s \"%s\"", sasl_cmd->cmd, mech);
    else
        sprintf(cmdbuf, "%s %s", sasl_cmd->cmd, mech);
    prot_printf(pout, "%s", cmdbuf);

    if (!clientout) goto noinitresp;  /* no initial response */

    /* initial response */
    if (!clientoutlen) { /* zero-length initial response */
        prot_printf(pout, " =");

        clientout = NULL;
    }
    else if (!sendliteral &&
             ((strlen(cmdbuf) + clientoutlen + 3) > sasl_cmd->maxlen)) {
        /* initial response is too long for auth command,
           so wait for a server challenge before sending it */
        goto noinitresp;
    }
    else { /* full response -- encoded below */
        prot_printf(pout, " ");
    }

    do {
        char *p;

        base64 = buf;
        *base64 = '\0';

        if (clientout) { /* response */
            /* convert to base64 */
            r = sasl_encode64(clientout, clientoutlen,
                              base64, BASE64_BUF_SIZE, NULL);

            clientout = NULL;
        }

        /* send to server */
        if (sendliteral) {
            prot_printf(pout, "{" SIZE_T_FMT "+}\r\n", strlen(base64));
            prot_flush(pout);
        }
        prot_printf(pout, "%s", base64);

      noinitresp:
        prot_printf(pout, "\r\n");
        prot_flush(pout);

        /* get challenge/reply from the server */
        if (!prot_fgets(buf, AUTH_BUF_SIZE, pin)) {
            if (sasl_result) *sasl_result = SASL_FAIL;
            if (status) *status = "EOF from server";
            return IMAP_SASL_PROTERR;
        }

        /* check response code */
        base64 = NULL;
        if (!strncasecmp(buf, sasl_cmd->ok, strlen(sasl_cmd->ok))) {
            /* success */
            if (sasl_cmd->parse_success) /* parse success data */
                base64 = sasl_cmd->parse_success(buf, status);

            if (!base64 /* no success data */
                && status) *status = buf + strlen(sasl_cmd->ok);

            r = SASL_OK;
        }
        else if (!strncasecmp(buf, sasl_cmd->fail, strlen(sasl_cmd->fail))) {
            /* failure */
            if (status) *status = buf + strlen(sasl_cmd->fail);
            r = SASL_BADAUTH;
            break;
        }
        else if (sasl_cmd->cont &&
                 !strncasecmp(buf, sasl_cmd->cont, strlen(sasl_cmd->cont))) {
            /* continue */
            base64 = buf + strlen(sasl_cmd->cont);
        }
        else if (!sasl_cmd->cont && buf[0] == '{') {
            unsigned int n, litsize = atoi(buf+1);

            /* get actual literal data */
            litsize += 2; /* +2 for \r\n */
            p = buf;
            while (litsize) {
                if (!(n = prot_read(pin, p, litsize))) {
                    if (sasl_result) *sasl_result = SASL_FAIL;
                    if (status) *status = "EOF from server";
                    return IMAP_SASL_PROTERR;
                }
                litsize -= n;
                p += n;
            }

            *p = '\0';
            base64 = buf;
        }
        else {
            /* unknown response */
            if (status) *status = buf;
            r = SASL_BADPROT;
        }

        if (base64) { /* challenge/success data */
            /* trim CRLF */
            p = base64 + strlen(base64) - 1;
            if (p >= base64 && *p == '\n') *p-- = '\0';
            if (p >= base64 && *p == '\r') *p-- = '\0';

            /* decode the challenge */
            serverin = buf;
            r = sasl_decode64(base64, strlen(base64),
                              serverin, BASE64_BUF_SIZE, &serverinlen);

            if (r == SASL_OK &&
                (serverinlen || !clientout)) { /* no delayed initial response */
                /* do the next step */
                r = sasl_client_step(conn, serverin, serverinlen, NULL,
                                     &clientout, &clientoutlen);
            }
        }

        if (r != SASL_OK && r != SASL_CONTINUE) {
            /* cancel the exchange */
            prot_printf(pout, "%s\r\n", sasl_cmd->cancel);
            prot_flush(pout);
        }

        sendliteral = !sasl_cmd->cont;

    } while (r == SASL_CONTINUE || (r == SASL_OK && clientout));

    if (sasl_result) *sasl_result = r;

    return (r == SASL_OK ? 0 : IMAP_SASL_FAIL);
}
