/*
 * Copyright (C) 2013 10gen, Inc.
 *
 *    Licensed 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 "mongo/base/init.h"

#ifdef _WIN32

#define SECURITY_WIN32 1  // Required for SSPI support.

#include "mongo/platform/basic.h"

#include <boost/scoped_array.hpp>
#include <sasl/sasl.h>
#include <sasl/saslplug.h>
#include <sspi.h>

#include "mongo/util/mongoutils/str.h"
#include "mongo/base/status.h"
#include "mongo/util/scopeguard.h"
#include "mongo/util/text.h"

extern "C" int plain_client_plug_init(const sasl_utils_t* utils,
                                      int maxversion,
                                      int* out_version,
                                      sasl_client_plug_t** pluglist,
                                      int* plugcount);

extern "C" int crammd5_client_plug_init(const sasl_utils_t* utils,
                                        int maxversion,
                                        int* out_version,
                                        sasl_client_plug_t** pluglist,
                                        int* plugcount);

namespace mongo {
namespace {
/*
 * SSPI client plugin impl
 */

// The SSPI plugin implements the GSSAPI interface.
char sspiPluginName[] = "GSSAPI";

// This structure is passed through each callback to us by the sasl glue code.
struct SspiConnContext {
    CredHandle cred;
    bool haveCred;
    CtxtHandle ctx;
    bool haveCtxt;
    bool authComplete;
    std::wstring nameToken;
    std::string userPlusRealm;

    SspiConnContext() : haveCred(false), haveCtxt(false), authComplete(false) {}
    ~SspiConnContext() {
        if (haveCtxt) {
            DeleteSecurityContext(&ctx);
        }
        if (haveCred) {
            FreeCredentialsHandle(&cred);
        }
    }
};

// Utility function for fetching error text from Windows API function calls.
void HandleLastError(const sasl_utils_t* utils, DWORD errCode, const char* msg) {
    char* err;
    if (!FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
                            FORMAT_MESSAGE_IGNORE_INSERTS,
                        NULL,
                        errCode,
                        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                        (LPSTR)&err,
                        0,
                        NULL)) {
        return;
    }

    std::string buffer(mongoutils::str::stream() << "SSPI: " << msg << ": " << err);
    utils->seterror(utils->conn, 0, "%s", buffer.c_str());
    LocalFree(err);
}

int sspiClientMechNew(void* glob_context,
                      sasl_client_params_t* cparams,
                      void** conn_context) throw() {
    // Prepare auth identity to pass to AcquireCredentialsHandle
    SEC_WINNT_AUTH_IDENTITY authIdentity;
    authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;

    // Fetch username@realm.
    sasl_getsimple_t* user_cb;
    void* user_context;
    int ret = cparams->utils->getcallback(
        cparams->utils->conn, SASL_CB_USER, (sasl_callback_ft*)&user_cb, &user_context);
    if (ret != SASL_OK) {
        cparams->utils->seterror(cparams->utils->conn, 0, "getcallback user failed");
        return ret;
    }
    const char* rawUserPlusRealm;
    unsigned rawUserPlusRealmLength = 0;

    ret = user_cb(user_context, SASL_CB_USER, &rawUserPlusRealm, &rawUserPlusRealmLength);
    if (ret != SASL_OK) {
        cparams->utils->seterror(cparams->utils->conn, 0, "user callback failed");
        return ret;
    }
    std::string userPlusRealm(rawUserPlusRealm, rawUserPlusRealmLength);

    // Parse out the username and realm.
    size_t atSign = userPlusRealm.find('@');
    if (atSign == std::string::npos) {
        cparams->utils->seterror(cparams->utils->conn, 0, "no @REALM found in username");
        return SASL_BADPARAM;
    }
    std::string utf8Username(userPlusRealm, 0, atSign);
    std::wstring utf16Username(toWideString(utf8Username.c_str()));

    authIdentity.UserLength = utf16Username.length();
    authIdentity.User =
        reinterpret_cast<unsigned short*>(const_cast<wchar_t*>(utf16Username.c_str()));

    std::string utf8Domain(userPlusRealm, atSign + 1);
    std::wstring utf16Domain(toWideString(utf8Domain.c_str()));
    authIdentity.DomainLength = utf16Domain.length();
    authIdentity.Domain =
        reinterpret_cast<unsigned short*>(const_cast<wchar_t*>(utf16Domain.c_str()));

    // Fetch password, if available.
    authIdentity.PasswordLength = 0;
    authIdentity.Password = NULL;
    std::wstring utf16Password;

    sasl_secret_t* password = NULL;
    sasl_getsecret_t* pass_cb;
    void* pass_context;
    ret = cparams->utils->getcallback(
        cparams->utils->conn, SASL_CB_PASS, (sasl_callback_ft*)&pass_cb, &pass_context);

    if ((ret == SASL_OK) && pass_cb) {
        ret = pass_cb(cparams->utils->conn, pass_context, SASL_CB_PASS, &password);
        if ((ret == SASL_OK) && password) {
            std::string utf8Password(reinterpret_cast<char*>(password->data), password->len);
            utf16Password = toWideString(utf8Password.c_str());
            authIdentity.PasswordLength = utf16Password.length();
            authIdentity.Password =
                reinterpret_cast<unsigned short*>(const_cast<wchar_t*>(utf16Password.c_str()));
        }
    }

    // Actually acquire the handle to the client credentials.
    std::auto_ptr<SspiConnContext> pcctx(new SspiConnContext());
    pcctx->userPlusRealm = userPlusRealm;
    TimeStamp ignored;
    SECURITY_STATUS status = AcquireCredentialsHandleW(NULL,  // principal
                                                       L"kerberos",
                                                       SECPKG_CRED_OUTBOUND,
                                                       NULL,           // LOGON id
                                                       &authIdentity,  // auth data
                                                       NULL,           // get key fn
                                                       NULL,           // get key arg
                                                       &pcctx->cred,
                                                       &ignored);
    if (status != SEC_E_OK) {
        HandleLastError(cparams->utils, status, "AcquireCredentialsHandle");
        return SASL_FAIL;
    }

    pcctx->haveCred = true;

    // Compose target name token.
    if (cparams->serverFQDN == NULL || strlen(cparams->serverFQDN) == 0) {
        cparams->utils->seterror(cparams->utils->conn, 0, "SSPI: no serverFQDN");
        return SASL_FAIL;
    }
    pcctx->nameToken =
        toWideString((std::string(cparams->service) + "/" + cparams->serverFQDN).c_str());

    *conn_context = pcctx.release();

    return SASL_OK;
}

int sspiValidateServerSecurityLayerOffering(SspiConnContext* pcctx,
                                            sasl_client_params_t* cparams,
                                            const char* serverin,
                                            unsigned serverinlen) {
    boost::scoped_array<char> message(new char[serverinlen]);
    memcpy(message.get(), serverin, serverinlen);

    SecBuffer wrapBufs[2];
    SecBufferDesc wrapBufDesc;
    wrapBufDesc.cBuffers = 2;
    wrapBufDesc.pBuffers = wrapBufs;
    wrapBufDesc.ulVersion = SECBUFFER_VERSION;

    wrapBufs[0].cbBuffer = serverinlen;
    wrapBufs[0].BufferType = SECBUFFER_STREAM;
    wrapBufs[0].pvBuffer = message.get();

    wrapBufs[1].cbBuffer = 0;
    wrapBufs[1].BufferType = SECBUFFER_DATA;
    wrapBufs[1].pvBuffer = NULL;

    SECURITY_STATUS status = DecryptMessage(&pcctx->ctx, &wrapBufDesc, 0, NULL);
    if (status != SEC_E_OK) {
        HandleLastError(cparams->utils, status, "DecryptMessage");
        return SASL_FAIL;
    }

    // Validate the server's plaintext message.
    // Length (as per RFC 4752)
    if (wrapBufs[1].cbBuffer < 4) {
        cparams->utils->seterror(cparams->utils->conn, 0, "SSPI: server message is too short");
        return SASL_FAIL;
    }
    // First bit of first byte set, indicating that the client may elect to use no
    // security layer. As a client we are uninterested in any of the other features the
    // server offers and thus we ignore the other bits.
    if (!(static_cast<char*>(wrapBufs[1].pvBuffer)[0] & 1)) {
        cparams->utils->seterror(
            cparams->utils->conn, 0, "SSPI: server does not support the required security layer");
        return SASL_BADAUTH;
    }
    return SASL_OK;
}


int sspiSendClientAuthzId(SspiConnContext* pcctx,
                          sasl_client_params_t* cparams,
                          const char* serverin,
                          unsigned serverinlen,
                          const char** clientout,
                          unsigned* clientoutlen,
                          sasl_out_params_t* oparams) {
    // Ensure server response is decryptable.
    int decryptStatus =
        sspiValidateServerSecurityLayerOffering(pcctx, cparams, serverin, serverinlen);
    if (decryptStatus != SASL_OK) {
        return decryptStatus;
    }

    // Fill in AUTHID and AUTHZID fields in oparams.
    int ret = cparams->canon_user(cparams->utils->conn,
                                  pcctx->userPlusRealm.c_str(),
                                  0,
                                  SASL_CU_AUTHID | SASL_CU_AUTHZID,
                                  oparams);

    // Reply to server with security capability and authz name.
    SecPkgContext_Sizes sizes;
    SECURITY_STATUS status = QueryContextAttributes(&pcctx->ctx, SECPKG_ATTR_SIZES, &sizes);
    if (status != SEC_E_OK) {
        HandleLastError(cparams->utils, status, "QueryContextAttributes(sizes)");
        return SASL_FAIL;
    }

    // See RFC4752.
    int plaintextMessageSize = 4 + pcctx->userPlusRealm.size();
    boost::scoped_array<char> message(
        new char[sizes.cbSecurityTrailer + plaintextMessageSize + sizes.cbBlockSize]);
    char* plaintextMessage = message.get() + sizes.cbSecurityTrailer;
    plaintextMessage[0] = 1;  // LAYER_NONE
    plaintextMessage[1] = 0;
    plaintextMessage[2] = 0;
    plaintextMessage[3] = 0;
    memcpy(&plaintextMessage[4], pcctx->userPlusRealm.c_str(), pcctx->userPlusRealm.size());

    SecBuffer wrapBufs[3];
    SecBufferDesc wrapBufDesc;
    wrapBufDesc.cBuffers = 3;
    wrapBufDesc.pBuffers = wrapBufs;
    wrapBufDesc.ulVersion = SECBUFFER_VERSION;

    wrapBufs[0].cbBuffer = sizes.cbSecurityTrailer;
    wrapBufs[0].BufferType = SECBUFFER_TOKEN;
    wrapBufs[0].pvBuffer = message.get();

    wrapBufs[1].cbBuffer = plaintextMessageSize;
    wrapBufs[1].BufferType = SECBUFFER_DATA;
    wrapBufs[1].pvBuffer = message.get() + sizes.cbSecurityTrailer;

    wrapBufs[2].cbBuffer = sizes.cbBlockSize;
    wrapBufs[2].BufferType = SECBUFFER_PADDING;
    wrapBufs[2].pvBuffer = message.get() + sizes.cbSecurityTrailer + plaintextMessageSize;

    status = EncryptMessage(&pcctx->ctx, SECQOP_WRAP_NO_ENCRYPT, &wrapBufDesc, 0);

    if (status != SEC_E_OK) {
        HandleLastError(cparams->utils, status, "EncryptMessage");
        return SASL_FAIL;
    }

    // Create the message to send to server.
    *clientoutlen = wrapBufs[0].cbBuffer + wrapBufs[1].cbBuffer + wrapBufs[2].cbBuffer;
    char* newoutbuf = static_cast<char*>(cparams->utils->malloc(*clientoutlen));
    memcpy(newoutbuf, wrapBufs[0].pvBuffer, wrapBufs[0].cbBuffer);
    memcpy(newoutbuf + wrapBufs[0].cbBuffer, wrapBufs[1].pvBuffer, wrapBufs[1].cbBuffer);
    memcpy(newoutbuf + wrapBufs[0].cbBuffer + wrapBufs[1].cbBuffer,
           wrapBufs[2].pvBuffer,
           wrapBufs[2].cbBuffer);
    *clientout = newoutbuf;

    return SASL_OK;
}


int sspiClientMechStep(void* conn_context,
                       sasl_client_params_t* cparams,
                       const char* serverin,
                       unsigned serverinlen,
                       sasl_interact_t** prompt_need,
                       const char** clientout,
                       unsigned* clientoutlen,
                       sasl_out_params_t* oparams) throw() {
    SspiConnContext* pcctx = static_cast<SspiConnContext*>(conn_context);
    *clientout = NULL;
    *clientoutlen = 0;

    if (pcctx->authComplete) {
        return sspiSendClientAuthzId(
            pcctx, cparams, serverin, serverinlen, clientout, clientoutlen, oparams);
    }

    SecBufferDesc inbuf;
    SecBuffer inBufs[1];
    SecBufferDesc outbuf;
    SecBuffer outBufs[1];

    if (pcctx->haveCtxt) {
        // If we already have a context, we now have data to send.
        // Put this data in an inbuf.
        inbuf.ulVersion = SECBUFFER_VERSION;
        inbuf.cBuffers = 1;
        inbuf.pBuffers = inBufs;
        inBufs[0].pvBuffer = const_cast<char*>(serverin);
        inBufs[0].cbBuffer = serverinlen;
        inBufs[0].BufferType = SECBUFFER_TOKEN;
    }

    outbuf.ulVersion = SECBUFFER_VERSION;
    outbuf.cBuffers = 1;
    outbuf.pBuffers = outBufs;
    outBufs[0].pvBuffer = NULL;
    outBufs[0].cbBuffer = 0;
    outBufs[0].BufferType = SECBUFFER_TOKEN;

    ULONG contextAttr = 0;
    SECURITY_STATUS status =
        InitializeSecurityContextW(&pcctx->cred,
                                   pcctx->haveCtxt ? &pcctx->ctx : NULL,
                                   const_cast<wchar_t*>(pcctx->nameToken.c_str()),
                                   ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_MUTUAL_AUTH,
                                   0,
                                   SECURITY_NETWORK_DREP,
                                   (pcctx->haveCtxt ? &inbuf : NULL),
                                   0,
                                   &pcctx->ctx,
                                   &outbuf,
                                   &contextAttr,
                                   NULL);

    if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) {
        HandleLastError(cparams->utils, status, "InitializeSecurityContext");
        return SASL_FAIL;
    }

    ON_BLOCK_EXIT(FreeContextBuffer, outbuf.pBuffers[0].pvBuffer);
    pcctx->haveCtxt = true;

    if (status == SEC_E_OK) {
        // Send back nothing and wait for the server to reply with the security capabilities
        *clientout = NULL;
        *clientoutlen = 0;
        pcctx->authComplete = true;
        return SASL_CONTINUE;
    }

    char* newoutbuf = static_cast<char*>(cparams->utils->malloc(outBufs[0].cbBuffer));
    *clientoutlen = outBufs[0].cbBuffer;
    memcpy(newoutbuf, outBufs[0].pvBuffer, *clientoutlen);
    *clientout = newoutbuf;
    return SASL_CONTINUE;
}

void sspiClientMechDispose(void* conn_context, const sasl_utils_t* utils) {
    SspiConnContext* pcctx = static_cast<SspiConnContext*>(conn_context);
    delete pcctx;
}

void sspiClientMechFree(void* glob_context, const sasl_utils_t* utils) {}

sasl_client_plug_t sspiClientPlugin[] = {
    {sspiPluginName, /* mechanism name */
     112, /* TODO: (taken from gssapi) best mech additional security layer strength factor */
     SASL_SEC_NOPLAINTEXT /* eam: copied from gssapi */
         |
         SASL_SEC_NOACTIVE | SASL_SEC_NOANONYMOUS | SASL_SEC_MUTUAL_AUTH |
         SASL_SEC_PASS_CREDENTIALS, /* security_flags */
     SASL_FEAT_NEEDSERVERFQDN | SASL_FEAT_WANT_CLIENT_FIRST | SASL_FEAT_ALLOWS_PROXY,
     NULL, /* required prompt ids, NULL = user/pass only */
     NULL, /* global state for mechanism */
     sspiClientMechNew,
     sspiClientMechStep,
     sspiClientMechDispose,
     sspiClientMechFree,
     NULL,
     NULL,
     NULL}};

int sspiClientPluginInit(const sasl_utils_t* utils,
                         int max_version,
                         int* out_version,
                         sasl_client_plug_t** pluglist,
                         int* plugcount) {
    if (max_version < SASL_CLIENT_PLUG_VERSION) {
        utils->seterror(utils->conn, 0, "Wrong SSPI version");
        return SASL_BADVERS;
    }

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

    return SASL_OK;
}

/**
 * Registers the plugin at process initialization time.
 * Must be run after the AllocatorsAndMutexes are registered, but before the ClientContext is
 * created.
 */
MONGO_INITIALIZER_WITH_PREREQUISITES(SaslSspiClientPlugin,
                                     ("CyrusSaslAllocatorsAndMutexes", "CyrusSaslClientContext"))
(InitializerContext*) {
    int ret = sasl_client_add_plugin(sspiPluginName, sspiClientPluginInit);
    if (SASL_OK != ret) {
        return Status(ErrorCodes::UnknownError,
                      mongoutils::str::stream() << "could not add SASL Client SSPI plugin "
                                                << sspiPluginName << ": "
                                                << sasl_errstring(ret, NULL, NULL));
    }

    return Status::OK();
}
MONGO_INITIALIZER_WITH_PREREQUISITES(SaslCramClientPlugin,
                                     ("CyrusSaslAllocatorsAndMutexes", "CyrusSaslClientContext"))
(InitializerContext*) {
    int ret = sasl_client_add_plugin("CRAMMD5", crammd5_client_plug_init);
    if (SASL_OK != ret) {
        return Status(ErrorCodes::UnknownError,
                      mongoutils::str::stream() << "Could not add SASL Client CRAM-MD5 plugin "
                                                << sspiPluginName << ": "
                                                << sasl_errstring(ret, NULL, NULL));
    }

    return Status::OK();
}

MONGO_INITIALIZER_WITH_PREREQUISITES(SaslPlainClientPlugin,
                                     ("CyrusSaslAllocatorsAndMutexes", "CyrusSaslClientContext"))
(InitializerContext*) {
    int ret = sasl_client_add_plugin("PLAIN", plain_client_plug_init);
    if (SASL_OK != ret) {
        return Status(ErrorCodes::UnknownError,
                      mongoutils::str::stream() << "Could not add SASL Client PLAIN plugin "
                                                << sspiPluginName << ": "
                                                << sasl_errstring(ret, NULL, NULL));
    }

    return Status::OK();
}

}  // namespace
}  // namespace mongo

#endif  // ifdef _WIN32

MONGO_INITIALIZER_FUNCTION_ASSURE_FILE(client_sasl_sspi)
