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

#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork

#include "mongo/platform/basic.h"

#include "mongo/util/net/ssl_manager.h"

#include <boost/thread/locks.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/recursive_mutex.hpp>
#include <boost/thread/tss.hpp>
#include <ostream>
#include <string>
#include <vector>

#include "mongo/base/init.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/client/options.h"
#include "mongo/util/debug_util.h"
#include "mongo/util/log.h"
#include "mongo/util/mongoutils/str.h"
#include "mongo/util/net/sock.h"
#include "mongo/util/scopeguard.h"

#ifdef MONGO_SSL
#include <openssl/evp.h>
#include <openssl/x509v3.h>
#endif

using std::endl;

namespace mongo {

#ifndef MONGO_SSL
const std::string getSSLVersion(const std::string& prefix, const std::string& suffix) {
    return "";
}
#else

// Old copies of OpenSSL will not have constants to disable protocols they don't support.
// Define them to values we can OR together safely to generically disable these protocols across
// all versions of OpenSSL.
#ifndef SSL_OP_NO_TLSv1_1
#define SSL_OP_NO_TLSv1_1 0
#endif
#ifndef SSL_OP_NO_TLSv1_2
#define SSL_OP_NO_TLSv1_2 0
#endif


const std::string getSSLVersion(const std::string& prefix, const std::string& suffix) {
    return prefix + SSLeay_version(SSLEAY_VERSION) + suffix;
}

namespace {

/**
 * Multithreaded Support for SSL.
 *
 * In order to allow OpenSSL to work in a multithreaded environment, you
 * must provide some callbacks for it to use for locking.  The following code
 * sets up a vector of mutexes and uses thread-local storage to assign an id
 * to each thread.
 * The so-called SSLThreadInfo class encapsulates most of the logic required for
 * OpenSSL multithreaded support.
 */

unsigned long _ssl_id_callback();
void _ssl_locking_callback(int mode, int type, const char* file, int line);

class SSLThreadInfo {
public:
    SSLThreadInfo() {
        _id = _next.fetchAndAdd(1);
    }

    ~SSLThreadInfo() {}

    unsigned long id() const {
        return _id;
    }

    void lock_callback(int mode, int type, const char* file, int line) {
        if (mode & CRYPTO_LOCK) {
            _mutex[type]->lock();
        } else {
            _mutex[type]->unlock();
        }
    }

    static void init() {
        while ((int)_mutex.size() < CRYPTO_num_locks())
            _mutex.push_back(new boost::recursive_mutex);
    }

    static SSLThreadInfo* get() {
        SSLThreadInfo* me = _thread.get();
        if (!me) {
            me = new SSLThreadInfo();
            _thread.reset(me);
        }
        return me;
    }

private:
    unsigned _id;

    static AtomicUInt32 _next;
    // Note: see SERVER-8734 for why we are using a recursive mutex here.
    // Once the deadlock fix in OpenSSL is incorporated into most distros of
    // Linux, this can be changed back to a nonrecursive mutex.
    static std::vector<boost::recursive_mutex*> _mutex;
    static boost::thread_specific_ptr<SSLThreadInfo> _thread;
};

unsigned long _ssl_id_callback() {
    return SSLThreadInfo::get()->id();
}

void _ssl_locking_callback(int mode, int type, const char* file, int line) {
    SSLThreadInfo::get()->lock_callback(mode, type, file, line);
}

AtomicUInt32 SSLThreadInfo::_next;
std::vector<boost::recursive_mutex*> SSLThreadInfo::_mutex;
boost::thread_specific_ptr<SSLThreadInfo> SSLThreadInfo::_thread;

////////////////////////////////////////////////////////////////

boost::mutex sslManagerMtx;
SSLManagerInterface* theSSLManager = NULL;
static const int BUFFER_SIZE = 8 * 1024;

struct Params {
    Params(const std::string& pemfile,
           const std::string& pempwd,
           const std::string& clusterfile,
           const std::string& clusterpwd,
           const std::vector<client::Options::TLSProtocol>& disabledProtocols,
           const std::string& cafile = "",
           const std::string& crlfile = "",
           const std::string& cipherConfig = "",
           bool weakCertificateValidation = false,
           bool allowInvalidCertificates = false,
           bool allowInvalidHostnames = false,
           bool fipsMode = false)
        : pemfile(pemfile),
          pempwd(pempwd),
          clusterfile(clusterfile),
          clusterpwd(clusterpwd),
          cafile(cafile),
          crlfile(crlfile),
          cipherConfig(cipherConfig),
          disabledProtocols(disabledProtocols),
          weakCertificateValidation(weakCertificateValidation),
          allowInvalidCertificates(allowInvalidCertificates),
          allowInvalidHostnames(allowInvalidHostnames),
          fipsMode(fipsMode){};

    std::string pemfile;
    std::string pempwd;
    std::string clusterfile;
    std::string clusterpwd;
    std::string cafile;
    std::string crlfile;
    std::string cipherConfig;
    std::vector<client::Options::TLSProtocol> disabledProtocols;
    bool weakCertificateValidation;
    bool allowInvalidCertificates;
    bool allowInvalidHostnames;
    bool fipsMode;
};

class SSLManager : public SSLManagerInterface {
public:
    explicit SSLManager(const Params& params, bool isServer);

    virtual ~SSLManager();

    virtual SSLConnection* connect(Socket* socket);

    virtual SSLConnection* accept(Socket* socket, const char* initialBytes, int len);

    virtual std::string parseAndValidatePeerCertificate(const SSLConnection* conn,
                                                        const std::string& remoteHost);

    virtual void cleanupThreadLocals();

    virtual std::string getServerSubjectName() {
        return _serverSubjectName;
    }

    virtual std::string getClientSubjectName() {
        return _clientSubjectName;
    }

    virtual std::string getSSLErrorMessage(int code);

    virtual int SSL_read(SSLConnection* conn, void* buf, int num);

    virtual int SSL_write(SSLConnection* conn, const void* buf, int num);

    virtual unsigned long ERR_get_error();

    virtual char* ERR_error_string(unsigned long e, char* buf);

    virtual int SSL_get_error(const SSLConnection* conn, int ret);

    virtual int SSL_shutdown(SSLConnection* conn);

    virtual void SSL_free(SSLConnection* conn);

private:
    SSL_CTX* _serverContext;  // SSL context for incoming connections
    SSL_CTX* _clientContext;  // SSL context for outgoing connections
    std::string _password;
    bool _validateCertificates;
    bool _weakValidation;
    bool _allowInvalidCertificates;
    bool _allowInvalidHostnames;
    std::string _serverSubjectName;
    std::string _clientSubjectName;

    /**
     * creates an SSL object to be used for this file descriptor.
     * caller must SSL_free it.
     */
    SSL* _secure(SSL_CTX* context, int fd);

    /**
     * Given an error code from an SSL-type IO function, logs an
     * appropriate message and throws a SocketException
     */
    MONGO_COMPILER_NORETURN void _handleSSLError(int code, int ret);

    /*
     * Init the SSL context using parameters provided in params.
     */
    bool _initSSLContext(SSL_CTX** context, const Params& params);

    /*
     * Parse the x509 subject name from the PEM keyfile and store it
     */
    bool _setSubjectName(const std::string& keyFile, std::string& subjectName);

    /** @return true if was successful, otherwise false */
    bool _setupPEM(SSL_CTX* context, const std::string& keyFile, const std::string& password);

    /*
     * Set up an SSL context for certificate validation by loading a CA
     */
    bool _setupCA(SSL_CTX* context, const std::string& caFile);

    /*
     * Import a certificate revocation list into an SSL context
     * for use with validating certificates
     */
    bool _setupCRL(SSL_CTX* context, const std::string& crlFile);

    /*
     * Activate FIPS 140-2 mode, if the server started with a command line
     * parameter.
     */
    void _setupFIPS();

    /*
     * sub function for checking the result of an SSL operation
     */
    bool _doneWithSSLOp(SSLConnection* conn, int status);

    /*
     * Send and receive network data
     */
    void _flushNetworkBIO(SSLConnection* conn);

    /*
     * match a remote host name to an x.509 host name
     */
    bool _hostNameMatch(const char* nameToMatch, const char* certHostName);

    /**
     * Callbacks for SSL functions
     */
    static int password_cb(char* buf, int num, int rwflag, void* userdata);
    static int verify_cb(int ok, X509_STORE_CTX* ctx);
};

}  // namespace

// Global variable indicating if this is a server or a client instance
const bool isSSLServer = false;

MONGO_INITIALIZER(SSLManager)(InitializerContext* context) {
    const client::Options& options = client::Options::current();

    boost::lock_guard<boost::mutex> lck(sslManagerMtx);
    if (options.SSLEnabled()) {
        const Params params(options.SSLPEMKeyFile(),
                            options.SSLPEMKeyPassword(),
                            std::string(),  // server only parameter
                            std::string(),  // server only parameter
                            options.SSLDisabledTLSProtocols(),
                            options.SSLCAFile(),
                            options.SSLCRLFile(),
                            options.SSLCipherConfig(),
                            false,  // server only parameter
                            options.SSLAllowInvalidCertificates(),
                            options.SSLAllowInvalidHostnames(),
                            options.FIPSMode());
        theSSLManager = new SSLManager(params, isSSLServer);
    }
    return Status::OK();
}

SSLManagerInterface* getSSLManager() {
    boost::lock_guard<boost::mutex> lck(sslManagerMtx);
    if (theSSLManager)
        return theSSLManager;
    return NULL;
}

std::string getCertificateSubjectName(X509* cert) {
    std::string result;

    BIO* out = BIO_new(BIO_s_mem());
    uassert(16884, "unable to allocate BIO memory", NULL != out);
    ON_BLOCK_EXIT(BIO_free, out);

    if (X509_NAME_print_ex(out, X509_get_subject_name(cert), 0, XN_FLAG_RFC2253) >= 0) {
        if (BIO_number_written(out) > 0) {
            result.resize(BIO_number_written(out));
            BIO_read(out, &result[0], result.size());
        }
    } else {
        log() << "failed to convert subject name to RFC2253 format" << endl;
    }

    return result;
}

SSLConnection::SSLConnection(SSL_CTX* context, Socket* sock, const char* initialBytes, int len)
    : socket(sock) {
    // This just ensures that SSL multithreading support is set up for this thread,
    // if it's not already.
    SSLThreadInfo::get();

    ssl = SSL_new(context);

    std::string sslErr =
        NULL != getSSLManager() ? getSSLManager()->getSSLErrorMessage(ERR_get_error()) : "";
    massert(15861, "Error creating new SSL object " + sslErr, ssl);

    BIO_new_bio_pair(&internalBIO, BUFFER_SIZE, &networkBIO, BUFFER_SIZE);
    SSL_set_bio(ssl, internalBIO, internalBIO);

    if (len > 0) {
        int toBIO = BIO_write(networkBIO, initialBytes, len);
        if (toBIO != len) {
            LOG(3) << "Failed to write initial network data to the SSL BIO layer";
            throw SocketException(SocketException::RECV_ERROR, socket->remoteString());
        }
    }
}

SSLConnection::~SSLConnection() {
    if (ssl) {  // The internalBIO is automatically freed as part of SSL_free
        SSL_free(ssl);
    }
    if (networkBIO) {
        BIO_free(networkBIO);
    }
}

SSLManagerInterface::~SSLManagerInterface() {}

SSLManager::SSLManager(const Params& params, bool isServer)
    : _validateCertificates(false),
      _weakValidation(params.weakCertificateValidation),
      _allowInvalidCertificates(params.allowInvalidCertificates),
      _allowInvalidHostnames(params.allowInvalidHostnames) {
    SSL_library_init();
    SSL_load_error_strings();
    ERR_load_crypto_strings();

    if (params.fipsMode) {
        _setupFIPS();
    }

    // Add all digests and ciphers to OpenSSL's internal table
    // so that encryption/decryption is backwards compatible
    OpenSSL_add_all_algorithms();

    // Setup OpenSSL multithreading callbacks
    CRYPTO_set_id_callback(_ssl_id_callback);
    CRYPTO_set_locking_callback(_ssl_locking_callback);

    SSLThreadInfo::init();
    SSLThreadInfo::get();

    if (!_initSSLContext(&_clientContext, params)) {
        uasserted(16768, "ssl initialization problem");
    }

    // SSL client specific initialization
    if (!isServer) {
        _serverContext = NULL;

        if (!params.pemfile.empty()) {
            if (!_setSubjectName(params.pemfile, _clientSubjectName)) {
                uasserted(16941, "ssl initialization problem");
            }
        }
    }
    // SSL server specific initialization
    if (isServer) {
        if (!_initSSLContext(&_serverContext, params)) {
            uasserted(16562, "ssl initialization problem");
        }

        if (!_setSubjectName(params.pemfile, _serverSubjectName)) {
            uasserted(16942, "ssl initialization problem");
        }
        // use the cluster certificate for outgoing connections if specified
        if (!params.clusterfile.empty()) {
            if (!_setSubjectName(params.clusterfile, _clientSubjectName)) {
                uasserted(16943, "ssl initialization problem");
            }
        } else {
            if (!_setSubjectName(params.pemfile, _clientSubjectName)) {
                uasserted(16944, "ssl initialization problem");
            }
        }
    }
}

SSLManager::~SSLManager() {
    CRYPTO_set_id_callback(0);
    ERR_free_strings();
    EVP_cleanup();

    if (NULL != _serverContext) {
        SSL_CTX_free(_serverContext);
    }
    if (NULL != _clientContext) {
        SSL_CTX_free(_clientContext);
    }
}

int SSLManager::password_cb(char* buf, int num, int rwflag, void* userdata) {
    // Unless OpenSSL misbehaves, num should always be positive
    fassert(17314, num > 0);
    SSLManager* sm = static_cast<SSLManager*>(userdata);
    const size_t copied = sm->_password.copy(buf, num - 1);
    buf[copied] = '\0';
    return copied;
}

int SSLManager::verify_cb(int ok, X509_STORE_CTX* ctx) {
    return 1;  // always succeed; we will catch the error in our get_verify_result() call
}

int SSLManager::SSL_read(SSLConnection* conn, void* buf, int num) {
    int status;
    do {
        status = ::SSL_read(conn->ssl, buf, num);
    } while (!_doneWithSSLOp(conn, status));

    if (status <= 0)
        _handleSSLError(SSL_get_error(conn, status), status);
    return status;
}

int SSLManager::SSL_write(SSLConnection* conn, const void* buf, int num) {
    int status;
    do {
        status = ::SSL_write(conn->ssl, buf, num);
    } while (!_doneWithSSLOp(conn, status));

    if (status <= 0)
        _handleSSLError(SSL_get_error(conn, status), status);
    return status;
}

unsigned long SSLManager::ERR_get_error() {
    return ::ERR_get_error();
}

char* SSLManager::ERR_error_string(unsigned long e, char* buf) {
    return ::ERR_error_string(e, buf);
}

int SSLManager::SSL_get_error(const SSLConnection* conn, int ret) {
    return ::SSL_get_error(conn->ssl, ret);
}

int SSLManager::SSL_shutdown(SSLConnection* conn) {
    int status;
    do {
        status = ::SSL_shutdown(conn->ssl);
    } while (!_doneWithSSLOp(conn, status));

    if (status < 0)
        _handleSSLError(SSL_get_error(conn, status), status);
    return status;
}

void SSLManager::SSL_free(SSLConnection* conn) {
    return ::SSL_free(conn->ssl);
}

void SSLManager::_setupFIPS() {
// Turn on FIPS mode if requested.
// OPENSSL_FIPS must be defined by the OpenSSL headers, plus MONGO_SSL_FIPS
// must be defined via a MongoDB build flag.
#if defined(MONGO_HAVE_FIPS_MODE_SET)
    int status = FIPS_mode_set(1);
    if (!status) {
        severe() << "can't activate FIPS mode: " << getSSLErrorMessage(ERR_get_error()) << endl;
        fassertFailed(16703);
    }
    log() << "FIPS 140-2 mode activated" << endl;
#else
    severe() << "this version of mongodb was not compiled with FIPS support";
    fassertFailed(17089);
#endif
}

bool SSLManager::_initSSLContext(SSL_CTX** context, const Params& params) {
    *context = SSL_CTX_new(SSLv23_method());
    massert(15864,
            mongoutils::str::stream()
                << "can't create SSL Context: " << getSSLErrorMessage(ERR_get_error()),
            context);

    // SSL_OP_ALL - Activate all bug workaround options, to support buggy client SSL's.
    // SSL_OP_NO_SSLv2 - Disable SSL v2 support
    // SSL_OP_NO_SSLv3 - Disable SSL v3 support
    long supportedProtocols = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;

    // Set the supported TLS protocols. Allow --disabledProtocols to disable selected ciphers.
    if (!params.disabledProtocols.empty()) {
        for (std::vector<client::Options::TLSProtocol>::const_iterator it =
                 params.disabledProtocols.begin();
             it != params.disabledProtocols.end();
             ++it) {
            if (*it == client::Options::kTLS1_0) {
                supportedProtocols |= SSL_OP_NO_TLSv1;
            } else if (*it == client::Options::kTLS1_1) {
                supportedProtocols |= SSL_OP_NO_TLSv1_1;
            } else if (*it == client::Options::kTLS1_2) {
                supportedProtocols |= SSL_OP_NO_TLSv1_2;
            }
        }
    }
    SSL_CTX_set_options(*context, supportedProtocols);

    // HIGH - Enable strong ciphers
    // !EXPORT - Disable export ciphers (40/56 bit)
    // !aNULL - Disable anonymous auth ciphers
    // @STRENGTH - Sort ciphers based on strength
    std::string cipherConfig = "HIGH:!EXPORT:!aNULL@STRENGTH";

    // Allow the cipher configuration string to be overriden by --sslCipherConfig
    if (!params.cipherConfig.empty()) {
        cipherConfig = params.cipherConfig;
    }

    massert(28615,
            mongoutils::str::stream()
                << "can't set supported cipher suites: " << getSSLErrorMessage(ERR_get_error()),
            SSL_CTX_set_cipher_list(*context, cipherConfig.c_str()));

    // If renegotiation is needed, don't return from recv() or send() until it's successful.
    // Note: this is for blocking sockets only.
    SSL_CTX_set_mode(*context, SSL_MODE_AUTO_RETRY);

    massert(28607,
            mongoutils::str::stream()
                << "can't store ssl session id context: " << getSSLErrorMessage(ERR_get_error()),
            SSL_CTX_set_session_id_context(*context,
                                           static_cast<unsigned char*>(static_cast<void*>(context)),
                                           sizeof(*context)));

    // Use the clusterfile for internal outgoing SSL connections if specified
    if (context == &_clientContext && !params.clusterfile.empty()) {
        EVP_set_pw_prompt("Enter cluster certificate passphrase");
        if (!_setupPEM(*context, params.clusterfile, params.clusterpwd)) {
            return false;
        }
    }
    // Use the pemfile for everything else
    else if (!params.pemfile.empty()) {
        EVP_set_pw_prompt("Enter PEM passphrase");
        if (!_setupPEM(*context, params.pemfile, params.pempwd)) {
            return false;
        }
    }

    if (!params.cafile.empty()) {
        // Set up certificate validation with a certificate authority
        if (!_setupCA(*context, params.cafile)) {
            return false;
        }
    }

    if (!params.crlfile.empty()) {
        if (!_setupCRL(*context, params.crlfile)) {
            return false;
        }
    }

    return true;
}

bool SSLManager::_setSubjectName(const std::string& keyFile, std::string& subjectName) {
    // Read the certificate subject name and store it
    BIO* in = BIO_new(BIO_s_file_internal());
    if (NULL == in) {
        error() << "failed to allocate BIO object: " << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }
    ON_BLOCK_EXIT(BIO_free, in);

    if (BIO_read_filename(in, keyFile.c_str()) <= 0) {
        error() << "cannot read key file when setting subject name: " << keyFile << ' '
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }

    X509* x509 = PEM_read_bio_X509(in, NULL, &SSLManager::password_cb, this);
    if (NULL == x509) {
        error() << "cannot retrieve certificate from keyfile: " << keyFile << ' '
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }
    ON_BLOCK_EXIT(X509_free, x509);
    subjectName = getCertificateSubjectName(x509);

    return true;
}

bool SSLManager::_setupPEM(SSL_CTX* context,
                           const std::string& keyFile,
                           const std::string& password) {
    _password = password;

    if (SSL_CTX_use_certificate_chain_file(context, keyFile.c_str()) != 1) {
        error() << "cannot read certificate file: " << keyFile << ' '
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }

    // If password is empty, use default OpenSSL callback, which uses the terminal
    // to securely request the password interactively from the user.
    if (!password.empty()) {
        SSL_CTX_set_default_passwd_cb_userdata(context, this);
        SSL_CTX_set_default_passwd_cb(context, &SSLManager::password_cb);
    }

    if (SSL_CTX_use_PrivateKey_file(context, keyFile.c_str(), SSL_FILETYPE_PEM) != 1) {
        error() << "cannot read PEM key file: " << keyFile << ' '
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }

    // Verify that the certificate and the key go together.
    if (SSL_CTX_check_private_key(context) != 1) {
        error() << "SSL certificate validation: " << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }

    return true;
}

bool SSLManager::_setupCA(SSL_CTX* context, const std::string& caFile) {
    // Set the list of CAs sent to clients
    STACK_OF(X509_NAME)* certNames = SSL_load_client_CA_file(caFile.c_str());
    if (certNames == NULL) {
        error() << "cannot read certificate authority file: " << caFile << " "
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }
    SSL_CTX_set_client_CA_list(context, certNames);

    // Load trusted CA
    if (SSL_CTX_load_verify_locations(context, caFile.c_str(), NULL) != 1) {
        error() << "cannot read certificate authority file: " << caFile << " "
                << getSSLErrorMessage(ERR_get_error()) << endl;
        return false;
    }
    // Set SSL to require peer (client) certificate verification
    // if a certificate is presented
    SSL_CTX_set_verify(context, SSL_VERIFY_PEER, &SSLManager::verify_cb);
    _validateCertificates = true;
    return true;
}

bool SSLManager::_setupCRL(SSL_CTX* context, const std::string& crlFile) {
    X509_STORE* store = SSL_CTX_get_cert_store(context);
    fassert(16583, store);

    X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
    X509_LOOKUP* lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
    fassert(16584, lookup);

    int status = X509_load_crl_file(lookup, crlFile.c_str(), X509_FILETYPE_PEM);
    if (status == 0) {
        error() << "cannot read CRL file: " << crlFile << ' ' << getSSLErrorMessage(ERR_get_error())
                << endl;
        return false;
    }
    log() << "ssl imported " << status << " revoked certificate" << ((status == 1) ? "" : "s")
          << " from the revocation list." << endl;
    return true;
}

/*
* The interface layer between network and BIO-pair. The BIO-pair buffers
* the data to/from the TLS layer.
*/
void SSLManager::_flushNetworkBIO(SSLConnection* conn) {
    char buffer[BUFFER_SIZE];
    int wantWrite;

    /*
    * Write the complete contents of the buffer. Leaving the buffer
    * unflushed could cause a deadlock.
    */
    while ((wantWrite = BIO_ctrl_pending(conn->networkBIO)) > 0) {
        if (wantWrite > BUFFER_SIZE) {
            wantWrite = BUFFER_SIZE;
        }
        int fromBIO = BIO_read(conn->networkBIO, buffer, wantWrite);

        int writePos = 0;
        do {
            int numWrite = fromBIO - writePos;
            numWrite = send(conn->socket->rawFD(), buffer + writePos, numWrite, portSendFlags);
            if (numWrite < 0) {
                conn->socket->handleSendError(numWrite, "");
            }
            writePos += numWrite;
        } while (writePos < fromBIO);
    }

    int wantRead;
    while ((wantRead = BIO_ctrl_get_read_request(conn->networkBIO)) > 0) {
        if (wantRead > BUFFER_SIZE) {
            wantRead = BUFFER_SIZE;
        }

        int numRead = recv(conn->socket->rawFD(), buffer, wantRead, portRecvFlags);
        if (numRead <= 0) {
            conn->socket->handleRecvError(numRead, wantRead);
            continue;
        }

        int toBIO = BIO_write(conn->networkBIO, buffer, numRead);
        if (toBIO != numRead) {
            LOG(3) << "Failed to write network data to the SSL BIO layer";
            throw SocketException(SocketException::RECV_ERROR, conn->socket->remoteString());
        }
    }
}

bool SSLManager::_doneWithSSLOp(SSLConnection* conn, int status) {
    int sslErr = SSL_get_error(conn, status);
    switch (sslErr) {
        case SSL_ERROR_NONE:
            _flushNetworkBIO(conn);  // success, flush network BIO before leaving
            return true;
        case SSL_ERROR_WANT_WRITE:
        case SSL_ERROR_WANT_READ:
            _flushNetworkBIO(conn);  // not ready, flush network BIO and try again
            return false;
        default:
            return true;
    }
}

SSLConnection* SSLManager::connect(Socket* socket) {
    std::auto_ptr<SSLConnection> sslConn(new SSLConnection(_clientContext, socket, NULL, 0));

    int ret;
    do {
        ret = ::SSL_connect(sslConn->ssl);
    } while (!_doneWithSSLOp(sslConn.get(), ret));

    if (ret != 1)
        _handleSSLError(SSL_get_error(sslConn.get(), ret), ret);

    return sslConn.release();
}

SSLConnection* SSLManager::accept(Socket* socket, const char* initialBytes, int len) {
    std::auto_ptr<SSLConnection> sslConn(
        new SSLConnection(_serverContext, socket, initialBytes, len));

    int ret;
    do {
        ret = ::SSL_accept(sslConn->ssl);
    } while (!_doneWithSSLOp(sslConn.get(), ret));

    if (ret != 1)
        _handleSSLError(SSL_get_error(sslConn.get(), ret), ret);

    return sslConn.release();
}

// TODO SERVER-11601 Use NFC Unicode canonicalization
bool SSLManager::_hostNameMatch(const char* nameToMatch, const char* certHostName) {
    if (strlen(certHostName) < 2) {
        return false;
    }

    // match wildcard DNS names
    if (certHostName[0] == '*' && certHostName[1] == '.') {
        // allow name.example.com if the cert is *.example.com, '*' does not match '.'
        const char* subName = strchr(nameToMatch, '.');
        return subName && !strcasecmp(certHostName + 1, subName);
    } else {
        return !strcasecmp(nameToMatch, certHostName);
    }
}

std::string SSLManager::parseAndValidatePeerCertificate(const SSLConnection* conn,
                                                        const std::string& remoteHost) {
    // only set if a CA cert has been provided
    if (!_validateCertificates)
        return "";

    X509* peerCert = SSL_get_peer_certificate(conn->ssl);

    if (NULL == peerCert) {  // no certificate presented by peer
        if (_weakValidation) {
            warning() << "no SSL certificate provided by peer" << endl;
        } else {
            error() << "no SSL certificate provided by peer; connection rejected" << endl;
            throw SocketException(SocketException::CONNECT_ERROR, "");
        }
        return "";
    }
    ON_BLOCK_EXIT(X509_free, peerCert);

    long result = SSL_get_verify_result(conn->ssl);

    if (result != X509_V_OK) {
        if (_allowInvalidCertificates) {
            warning() << "SSL peer certificate validation failed:"
                      << X509_verify_cert_error_string(result);
        } else {
            error() << "SSL peer certificate validation failed:"
                    << X509_verify_cert_error_string(result);
            throw SocketException(SocketException::CONNECT_ERROR, "");
        }
    }

    // TODO: check optional cipher restriction, using cert.
    std::string peerSubjectName = getCertificateSubjectName(peerCert);

    // If this is an SSL client context (on a MongoDB server or client)
    // perform hostname validation of the remote server
    if (remoteHost.empty()) {
        return peerSubjectName;
    }

    // Try to match using the Subject Alternate Name, if it exists.
    // RFC-2818 requires the Subject Alternate Name to be used if present.
    // Otherwise, the most specific Common Name field in the subject field
    // must be used.

    bool sanMatch = false;
    bool cnMatch = false;

    STACK_OF(GENERAL_NAME)* sanNames = static_cast<STACK_OF(GENERAL_NAME)*>(
        X509_get_ext_d2i(peerCert, NID_subject_alt_name, NULL, NULL));

    if (sanNames != NULL) {
        int sanNamesList = sk_GENERAL_NAME_num(sanNames);
        for (int i = 0; i < sanNamesList; i++) {
            const GENERAL_NAME* currentName = sk_GENERAL_NAME_value(sanNames, i);
            if (currentName && currentName->type == GEN_DNS) {
                char* dnsName = reinterpret_cast<char*>(ASN1_STRING_data(currentName->d.dNSName));
                if (_hostNameMatch(remoteHost.c_str(), dnsName)) {
                    sanMatch = true;
                    break;
                }
            }
        }
        sk_GENERAL_NAME_pop_free(sanNames, GENERAL_NAME_free);
    } else {
        // If Subject Alternate Name (SAN) didn't exist, check Common Name (CN).
        int cnBegin = peerSubjectName.find("CN=") + 3;
        int cnEnd = peerSubjectName.find(",", cnBegin);
        std::string commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin);

        if (_hostNameMatch(remoteHost.c_str(), commonName.c_str())) {
            cnMatch = true;
        }
    }

    if (!sanMatch && !cnMatch) {
        if (_allowInvalidCertificates || _allowInvalidHostnames) {
            warning() << "The server certificate does not match the host name " << remoteHost;
        } else {
            error() << "The server certificate does not match the host name " << remoteHost;
            throw SocketException(SocketException::CONNECT_ERROR, "");
        }
    }

    return peerSubjectName;
}

void SSLManager::cleanupThreadLocals() {
    ERR_remove_state(0);
}

std::string SSLManager::getSSLErrorMessage(int code) {
    // 120 from the SSL documentation for ERR_error_string
    static const size_t msglen = 120;

    char msg[msglen];
    ERR_error_string_n(code, msg, msglen);
    return msg;
}

void SSLManager::_handleSSLError(int code, int ret) {
    int err = ERR_get_error();

    switch (code) {
        case SSL_ERROR_WANT_READ:
        case SSL_ERROR_WANT_WRITE:
            // should not happen because we turned on AUTO_RETRY
            // However, it turns out this CAN happen during a connect, if the other side
            // accepts the socket connection but fails to do the SSL handshake in a timely
            // manner.
            error() << "SSL: " << code << ", possibly timed out during connect";
            break;

        case SSL_ERROR_ZERO_RETURN:
            // TODO: Check if we can avoid throwing an exception for this condition
            LOG(3) << "SSL network connection closed";
            break;
        case SSL_ERROR_SYSCALL:
            // If ERR_get_error returned 0, the error queue is empty
            // check the return value of the actual SSL operation
            if (err != 0) {
                error() << "SSL: " << getSSLErrorMessage(err);
            } else if (ret == 0) {
                error() << "Unexpected EOF encountered during SSL communication";
            } else {
                error() << "The SSL BIO reported an I/O error " << errnoWithDescription();
            }
            break;
        case SSL_ERROR_SSL: {
            error() << "SSL: " << getSSLErrorMessage(err);
            break;
        }

        default:
            error() << "unrecognized SSL error";
            break;
    }
    throw SocketException(SocketException::CONNECT_ERROR, "");
}
#endif  // #ifdef MONGO_SSL
}

MONGO_INITIALIZER_FUNCTION_ASSURE_FILE(util_net_ssl_manager)
