/*
 * 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.
*/

#include <stdio.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include <sasl/sasl.h>
#include <ruby.h>

static VALUE mSasl;

#define INPUT_SIZE 512
#define MECH_SIZE 32

typedef void* sasl_context_t;

#define QSASL_OK 0
#define QSASL_CONTINUE 1
#define QSASL_FAILED 2 

typedef struct {
    char magic[8];
    sasl_conn_t*  conn;
    sasl_callback_t callbacks[8];
    char* userName;
    char* password;
    unsigned int minSsf;
    unsigned int maxSsf;
    char mechanism[MECH_SIZE];
    char input[INPUT_SIZE];
} context_t;

//
// Resolve forward references
//
static VALUE qsasl_free(int, VALUE*, VALUE);

//
// Validate an input string to ensure that it is either NULL or of reasonable size.
//
static int qsasl_valid(char* str)
{
    int idx;

    if (str == 0)
        return 1;

    for (idx = 0; idx < INPUT_SIZE; idx++) {
        if (str[idx] == '\0')
            return 1;
    }

    return 0;
}

//
// SASL callback for identity and authentication identity.
//
static int qsasl_cb_user(void* _context, int id, const char **result, unsigned *len)
{
    context_t* context = (context_t*) _context;

    if (context->userName)
        *result = context->userName;

    return SASL_OK;
}

//
// SASL callback for passwords.
//
static int qsasl_cb_password(sasl_conn_t* conn, void* _context, int id, sasl_secret_t **psecret)
{
    context_t* context = (context_t*) _context;
    sasl_secret_t* secret;
    size_t length;

    if (context->password)
        length = strlen(context->password);
    else
        length = 0;

    secret = (sasl_secret_t*) malloc(sizeof(sasl_secret_t) + length);
    secret->len = length;
    if (length)
        memcpy(secret->data, context->password, length);
    *psecret = secret;

    return SASL_OK;
}

//
// Interactively prompt the user for authentication data.
//
static void qsasl_prompt(sasl_context_t _context, sasl_interact_t* interact)
{
    context_t* context = (context_t*) _context;
    char *pass;
    char *input;
    char passwdPrompt[100];

    if (interact->id == SASL_CB_PASS) {
        strncpy(passwdPrompt, interact->prompt, 95);
        strcat(passwdPrompt, ": ");
        pass = getpass(passwdPrompt);
        strncpy(context->input, pass, INPUT_SIZE - 1);
        context->input[INPUT_SIZE - 1] = '\0';
    } else {
        printf(interact->prompt);
        if (interact->defresult) {
            printf(" (%s)", interact->defresult);
        }
        printf(": ");
        input = fgets(context->input, INPUT_SIZE, stdin);
        if (input != context->input) {
            rb_raise(rb_eRuntimeError, "Unexpected EOF on interactive prompt");
        }
    }

    interact->result = context->input;
    interact->len = strlen(context->input);
}

//
// Initialize the SASL client library.
//
static VALUE qsasl_client_init()
{
    int result;

    result = sasl_client_init(0);
    if (result != SASL_OK)
        rb_raise(rb_eRuntimeError,
                 "sasl_client_init failed: %d - %s",
                 result, sasl_errstring(result, -0, 0));
    return Qnil;
}

//
// Allocate a new SASL client context.
//
static VALUE qsasl_client_new(int argc, VALUE *argv, VALUE obj)
{
    char* mechanism = 0;
    char* serviceName = 0;
    char* hostName = 0;
    char* userName = 0;
    char* password = 0;
    unsigned int minSsf = 0;
    unsigned int maxSsf = 65535;

    int result;
    int i = 0;
    context_t *context;
    sasl_security_properties_t secprops;

    if (argc != 7)
        rb_raise(rb_eRuntimeError, "Wrong number of arguments");

    if (!NIL_P(argv[0]))
        mechanism = StringValuePtr(argv[0]);
    if (!NIL_P(argv[1]))
        serviceName = StringValuePtr(argv[1]);
    if (!NIL_P(argv[2]))
        hostName = StringValuePtr(argv[2]);
    if (!NIL_P(argv[3]))
        userName = StringValuePtr(argv[3]);
    if (!NIL_P(argv[4]))
        password = StringValuePtr(argv[4]);
    minSsf = FIX2INT(argv[5]);
    maxSsf = FIX2INT(argv[6]);

    if (!qsasl_valid(mechanism) || !qsasl_valid(serviceName) ||
        !qsasl_valid(hostName) || !qsasl_valid(userName) ||
        !qsasl_valid(password)) {
        rb_raise(rb_eRuntimeError, "Invalid string argument");
    }

    context = (context_t*) malloc(sizeof(context_t));
    memset(context, 0, sizeof(context_t));
    strcpy(context->magic, "QSASL01");

    context->minSsf = minSsf;
    context->maxSsf = maxSsf;
    if (mechanism != 0) {
        strncpy(context->mechanism, mechanism, MECH_SIZE - 1);
        context->mechanism[MECH_SIZE - 1] = '\0';
    }

    context->callbacks[i].id = SASL_CB_GETREALM;
    context->callbacks[i].proc = 0;
    context->callbacks[i++].context = 0;

    if (userName != 0 && userName[0] != '\0') {
        context->userName = (char*) malloc(strlen(userName) + 1);
        strcpy(context->userName, userName);

        context->callbacks[i].id = SASL_CB_USER;
        context->callbacks[i].proc = qsasl_cb_user;
        context->callbacks[i++].context = context;
        
        context->callbacks[i].id = SASL_CB_AUTHNAME;
        context->callbacks[i].proc = qsasl_cb_user;
        context->callbacks[i++].context = context;
    }

    context->callbacks[i].id = SASL_CB_PASS;
    if (password != 0 && password[0] != '\0') {
        context->password = (char*) malloc(strlen(password) + 1);
        strcpy(context->password, password);

        context->callbacks[i].proc = qsasl_cb_password;
    } else
        context->callbacks[i].proc = 0;
    context->callbacks[i++].context = context;

    context->callbacks[i].id = SASL_CB_LIST_END;
    context->callbacks[i].proc = 0;
    context->callbacks[i++].context = 0;
    
    result = sasl_client_new(serviceName, hostName, 0, 0,
                             context->callbacks, 0, &context->conn);

    if (result != SASL_OK) {
        context->conn = 0;
        qsasl_free(1, (VALUE*) &context, Qnil);
        rb_raise(rb_eRuntimeError, "sasl_client_new failed: %d - %s",
                 result, sasl_errstring(result, 0, 0));
    }

    secprops.min_ssf = minSsf;
    secprops.max_ssf = maxSsf;
    secprops.maxbufsize = 65535;
    secprops.property_names = 0;
    secprops.property_values = 0;
    secprops.security_flags = 0;//TODO: provide means for application to configure these

    result = sasl_setprop(context->conn, SASL_SEC_PROPS, &secprops);
    if (result != SASL_OK) {
        qsasl_free(1, (VALUE*) &context, Qnil);
        rb_raise(rb_eRuntimeError, "sasl_setprop failed: %d - %s",
                 result, sasl_errdetail(context->conn));
    }

    return (VALUE) context;
}

//
// Free a SASL client context.
//
static VALUE qsasl_free(int argc, VALUE *argv, VALUE obj)
{
    context_t* context;

    if (argc == 1)
        context = (context_t*) argv[0];
    else
        rb_raise(rb_eRuntimeError, "Wrong Number of Arguments");

    if (context->conn)
        sasl_dispose(&context->conn);
    if (context->userName)
        free(context->userName);
    if (context->password)
        free(context->password);
    free(context);

    return Qnil;
}

//
// Start the SASL exchange from the client's point of view.
//
static VALUE qsasl_client_start(int argc, VALUE *argv, VALUE obj)
{
    context_t* context;
    char* mechList;
    char* mechToUse;
    int result;
    const char* response;
    unsigned int len;
    sasl_interact_t* interact = 0;
    const char* chosen;

    if (argc == 2) {
        context = (context_t*) argv[0];
        mechList = StringValuePtr(argv[1]);
    } else
        rb_raise(rb_eRuntimeError, "Wrong Number of Arguments");

    if (strlen(context->mechanism) == 0)
        mechToUse = mechList;
    else
        mechToUse = context->mechanism;

    do {
        result = sasl_client_start(context->conn, mechToUse, &interact,
                                   &response, &len, &chosen);
        if (result == SASL_INTERACT) {
            qsasl_prompt(context, interact);
        }
    } while (result == SASL_INTERACT);

    if (result != SASL_OK && result != SASL_CONTINUE)
        rb_raise(rb_eRuntimeError, "sasl_client_start failed: %d - %s",
                 result, sasl_errdetail(context->conn));

    return rb_ary_new3(3, INT2NUM(result), rb_str_new(response, len), rb_str_new2(chosen));
}

//
// Take a step in the SASL exchange (only needed for multi-challenge mechanisms).
//
static VALUE qsasl_client_step(int argc, VALUE *argv, VALUE obj)
{
    context_t* context;
    VALUE challenge;
    int result;
    const char* response;
    unsigned int len;
    sasl_interact_t* interact = 0;

    if (argc == 2) {
        context = (context_t*) argv[0];
        challenge = argv[1];
    }
    else
        rb_raise(rb_eRuntimeError, "Wrong Number of Arguments");

    do {
        result = sasl_client_step(context->conn,
                                  RSTRING(challenge)->ptr, RSTRING(challenge)->len,
                                  &interact, &response, &len);
        if (result == SASL_INTERACT) {
            qsasl_prompt(context, interact);
        }
    } while (result == SASL_INTERACT);

    if (result != SASL_OK && result != SASL_CONTINUE)
        return QSASL_FAILED;

    return rb_ary_new3(2, INT2NUM(result), rb_str_new(response, len));
}

//
// Encode transport data for the security layer.
//
static VALUE qsasl_encode(int argc, VALUE *argv, VALUE obj)
{
    context_t* context;
    VALUE clearText;
    const char* outBuffer;
    unsigned int outSize;
    int result;

    if (argc == 2) {
        context = (context_t*) argv[0];
        clearText = argv[1];
    }
    else
        rb_raise(rb_eRuntimeError, "Wrong Number of Arguments");

    result = sasl_encode(context->conn,
                         RSTRING(clearText)->ptr, RSTRING(clearText)->len,
                         &outBuffer, &outSize);
    if (result != SASL_OK)
        rb_raise(rb_eRuntimeError, "sasl_encode failed: %d - %s",
                 result, sasl_errdetail(context->conn));

    return rb_str_new(outBuffer, outSize);
}

//
// Decode transport data for the security layer.
//
static VALUE qsasl_decode(int argc, VALUE *argv, VALUE obj)
{
    context_t* context;
    VALUE cipherText;
    const char* outBuffer;
    unsigned int outSize;
    int result;

    if (argc == 2) {
        context = (context_t*) argv[0];
        cipherText = argv[1];
    }
    else
        rb_raise(rb_eRuntimeError, "Wrong Number of Arguments");

    result = sasl_decode(context->conn,
                         RSTRING(cipherText)->ptr, RSTRING(cipherText)->len,
                         &outBuffer, &outSize);
    if (result != SASL_OK)
        rb_raise(rb_eRuntimeError, "sasl_decode failed: %d - %s",
                 result, sasl_errdetail(context->conn));

    return rb_str_new(outBuffer, outSize);
}

//
// Initialize the Sasl module.
//
void Init_sasl()
{
    mSasl = rb_define_module("Sasl");

    rb_define_module_function(mSasl, "client_init", qsasl_client_init, -1);
    rb_define_module_function(mSasl, "client_new", qsasl_client_new, -1);
    rb_define_module_function(mSasl, "free", qsasl_free, -1);
    rb_define_module_function(mSasl, "client_start", qsasl_client_start, -1);
    rb_define_module_function(mSasl, "client_step", qsasl_client_step, -1);
    rb_define_module_function(mSasl, "encode", qsasl_encode, -1);
    rb_define_module_function(mSasl, "decode", qsasl_decode, -1);
}
