/*
 * 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 "OpenSSLSocket.h"

#ifdef HAVE_OPENSSL
    #include <openssl/ssl.h>
    #include <openssl/tls1.h>
    #include <openssl/x509.h>
    #include <openssl/x509v3.h>
    #include <openssl/bio.h>
#endif

#include <decaf/net/SocketImpl.h>
#include <decaf/io/IOException.h>
#include <decaf/net/SocketException.h>
#include <decaf/lang/Boolean.h>
#include <decaf/lang/exceptions/NullPointerException.h>
#include <decaf/lang/exceptions/IndexOutOfBoundsException.h>
#include <decaf/internal/util/StringUtils.h>
#include <decaf/internal/net/SocketFileDescriptor.h>
#include <decaf/internal/net/ssl/openssl/OpenSSLParameters.h>
#include <decaf/internal/net/ssl/openssl/OpenSSLSocketException.h>
#include <decaf/internal/net/ssl/openssl/OpenSSLSocketInputStream.h>
#include <decaf/internal/net/ssl/openssl/OpenSSLSocketOutputStream.h>
#include <decaf/util/concurrent/Mutex.h>

using namespace decaf;
using namespace decaf::lang;
using namespace decaf::lang::exceptions;
using namespace decaf::io;
using namespace decaf::net;
using namespace decaf::net::ssl;
using namespace decaf::util::concurrent;
using namespace decaf::internal;
using namespace decaf::internal::util;
using namespace decaf::internal::net;
using namespace decaf::internal::net::ssl;
using namespace decaf::internal::net::ssl::openssl;

////////////////////////////////////////////////////////////////////////////////
namespace decaf {
namespace internal {
namespace net {
namespace ssl {
namespace openssl {

    class SocketData {
    public:

        bool handshakeStarted;
        bool handshakeCompleted;
        std::string commonName;

        Mutex handshakeLock;

    public:

        SocketData() : handshakeStarted(false),
                       handshakeCompleted(false),
                       commonName(),
                       handshakeLock() {
        }

        ~SocketData() {}

#ifdef HAVE_OPENSSL
        static int verifyCallback(int verified, X509_STORE_CTX* store DECAF_UNUSED) {
            if (!verified) {
                // Trap debug info here about why the Certificate failed to validate.
            }

            return verified;
        }
#endif

    };

}}}}}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::OpenSSLSocket(OpenSSLParameters* parameters) :
    SSLSocket(), data(new SocketData()), parameters(parameters), input(NULL), output(NULL) {

    if (parameters == NULL) {
        throw NullPointerException(__FILE__, __LINE__,
            "The OpenSSL Parameters object instance passed was NULL.");
    }
}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::OpenSSLSocket(OpenSSLParameters* parameters, const InetAddress* address, int port) :
    SSLSocket(address, port), data(new SocketData()), parameters(parameters), input(NULL), output(NULL) {

    if (parameters == NULL) {
        throw NullPointerException(__FILE__, __LINE__, "The OpenSSL Parameters object instance passed was NULL.");
    }
}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::OpenSSLSocket(OpenSSLParameters* parameters, const InetAddress* address, int port, const InetAddress* localAddress, int localPort) :
    SSLSocket(address, port, localAddress, localPort), data(new SocketData()), parameters(parameters), input(NULL), output(NULL) {

    if (parameters == NULL) {
        throw NullPointerException(__FILE__, __LINE__, "The OpenSSL Parameters object instance passed was NULL.");
    }
}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::OpenSSLSocket(OpenSSLParameters* parameters, const std::string& host, int port) :
    SSLSocket(host, port), data(new SocketData()), parameters(parameters), input(NULL), output(NULL) {

    if (parameters == NULL) {
        throw NullPointerException(__FILE__, __LINE__, "The OpenSSL Parameters object instance passed was NULL.");
    }
}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::OpenSSLSocket(OpenSSLParameters* parameters, const std::string& host, int port, const InetAddress* localAddress, int localPort) :
    SSLSocket(host, port, localAddress, localPort), data(new SocketData()), parameters(parameters), input(NULL), output(NULL) {

    if (parameters == NULL) {
        throw NullPointerException(__FILE__, __LINE__, "The OpenSSL Parameters object instance passed was NULL.");
    }
}

////////////////////////////////////////////////////////////////////////////////
OpenSSLSocket::~OpenSSLSocket() {
    try {

        SSLSocket::close();

#ifdef HAVE_OPENSSL
        if (this->parameters->getSSL()) {
            SSL_set_shutdown(this->parameters->getSSL(), SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN);
            SSL_shutdown(this->parameters->getSSL());
        }
#endif

        delete data;
        delete parameters;
        delete input;
        delete output;
    }
    DECAF_CATCH_NOTHROW(Exception)
    DECAF_CATCHALL_NOTHROW()}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::connect(const std::string& host, int port, int timeout) {

    try {

#ifdef HAVE_OPENSSL

        // Perform the actual Socket connection work
        SSLSocket::connect(host, port, timeout);

        // If we actually connected then we can connect the Socket to an OpenSSL
        // BIO filter so that we can use it in OpenSSL APIs.
        if (isConnected()) {

            BIO* bio = BIO_new(BIO_s_socket());
            if (!bio) {
                throw SocketException(__FILE__, __LINE__, "Failed to create SSL IO Bindings");
            }

            const SocketFileDescriptor* fd = dynamic_cast<const SocketFileDescriptor*>(this->impl->getFileDescriptor());

            if (fd == NULL) {
                throw SocketException(__FILE__, __LINE__, "Invalid File Descriptor returned from Socket");
            }

            BIO_set_fd(bio, (int )fd->getValue(), BIO_NOCLOSE);
            SSL_set_bio(this->parameters->getSSL(), bio, bio);

            // Later when startHandshake is called we will check for this common name
            // in the provided certificate
            this->data->commonName = host;
        }
#else
        throw SocketException( __FILE__, __LINE__, "Not Supported" );
#endif
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_RETHROW(IllegalArgumentException)
    DECAF_CATCH_EXCEPTION_CONVERT(Exception, IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::close() {

    try {

        if (isClosed()) {
            return;
        }

        SSLSocket::close();

        if (this->input != NULL) {
            this->input->close();
        }
        if (this->output != NULL) {
            this->output->close();
        }
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_EXCEPTION_CONVERT(Exception, IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
decaf::io::InputStream* OpenSSLSocket::getInputStream() {

    checkClosed();

    try {
        if (this->input == NULL) {
            this->input = new OpenSSLSocketInputStream(this);
        }

        return this->input;
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_EXCEPTION_CONVERT(Exception, IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
decaf::io::OutputStream* OpenSSLSocket::getOutputStream() {

    checkClosed();

    try {
        if (this->output == NULL) {
            this->output = new OpenSSLSocketOutputStream(this);
        }

        return this->output;
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_EXCEPTION_CONVERT(Exception, IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::shutdownInput() {
    throw SocketException(__FILE__, __LINE__, "Not supported for SSL Sockets");
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::shutdownOutput() {
    throw SocketException(__FILE__, __LINE__, "Not supported for SSL Sockets");
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setOOBInline(bool value DECAF_UNUSED) {
    throw SocketException(__FILE__, __LINE__, "Not supported for SSL Sockets");
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::sendUrgentData(int data DECAF_UNUSED) {
    throw SocketException(__FILE__, __LINE__, "Not supported for SSL Sockets");
}

////////////////////////////////////////////////////////////////////////////////
decaf::net::ssl::SSLParameters OpenSSLSocket::getSSLParameters() const {

    SSLParameters params(this->getEnabledCipherSuites(), this->getEnabledProtocols());

    params.setServerNames(this->parameters->getServerNames());
    params.setNeedClientAuth(this->parameters->getNeedClientAuth());
    params.setWantClientAuth(this->parameters->getWantClientAuth());

    return params;
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setSSLParameters(const decaf::net::ssl::SSLParameters& value) {
    this->parameters->setEnabledCipherSuites(value.getCipherSuites());
    this->parameters->setEnabledProtocols(value.getProtocols());
    this->parameters->setServerNames(value.getServerNames());
}

////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> OpenSSLSocket::getSupportedCipherSuites() const {
    return this->parameters->getSupportedCipherSuites();
}

////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> OpenSSLSocket::getSupportedProtocols() const {
    return this->parameters->getSupportedProtocols();
}

////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> OpenSSLSocket::getEnabledCipherSuites() const {
    return this->parameters->getEnabledCipherSuites();
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setEnabledCipherSuites(const std::vector<std::string>& suites) {
    this->parameters->setEnabledCipherSuites(suites);
}

////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> OpenSSLSocket::getEnabledProtocols() const {
    return this->parameters->getEnabledProtocols();
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setEnabledProtocols(const std::vector<std::string>& protocols) {
    this->parameters->setEnabledProtocols(protocols);
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::startHandshake() {

    if (!this->isConnected()) {
        throw IOException(__FILE__, __LINE__, "Socket is not connected.");
    }

    if (this->isClosed()) {
        throw IOException(__FILE__, __LINE__, "Socket already closed.");
    }

    try {

#ifdef HAVE_OPENSSL
        synchronized( &(this->data->handshakeLock ) ) {

            if (this->data->handshakeStarted) {
                return;
            }

            this->data->handshakeStarted = true;

            bool peerVerifyDisabled = Boolean::parseBoolean(System::getProperty("decaf.net.ssl.disablePeerVerification", "false"));

            if (this->parameters->getUseClientMode()) {

                // Since we are a client we want to enforce peer verification, we set a
                // callback so we can collect data on why a verify failed for debugging.
                if (!peerVerifyDisabled) {
                    SSL_set_verify(this->parameters->getSSL(), SSL_VERIFY_PEER, SocketData::verifyCallback);
                } else {
                    SSL_set_verify(this->parameters->getSSL(), SSL_VERIFY_NONE, NULL);
                }

                std::vector<std::string> serverNames = this->parameters->getServerNames();
                if (!serverNames.empty()) {
                    std::string serverName = serverNames.at(0);
                    SSL_set_tlsext_host_name(this->parameters->getSSL(), serverName.c_str());
                }

                int result = SSL_connect(this->parameters->getSSL());

                // Checks the error status, when things go right we still perform a deeper
                // check on the provided certificate to ensure that it matches the host name
                // that we connected to, this prevents someone from using any certificate
                // signed by a signing authority that we trust.
                switch (SSL_get_error(this->parameters->getSSL(), result)) {
                case SSL_ERROR_NONE:
                    if (!peerVerifyDisabled) {
                        verifyServerCert(this->data->commonName);
                    }
                    break;
                case SSL_ERROR_SSL:
                case SSL_ERROR_ZERO_RETURN:
                case SSL_ERROR_SYSCALL:
                    SSLSocket::close();
                    throw OpenSSLSocketException(__FILE__, __LINE__);
                }

            } else { // We are in Server Mode.

                int mode = SSL_VERIFY_NONE;

                if (!peerVerifyDisabled) {

                    if (this->parameters->getWantClientAuth()) {
                        mode = SSL_VERIFY_PEER;
                    }

                    if (this->parameters->getNeedClientAuth()) {
                        mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
                    }
                }

                // Since we are a server we want to enforce peer verification, we set a
                // callback so we can collect data on why a verify failed for debugging.
                SSL_set_verify(this->parameters->getSSL(), mode, SocketData::verifyCallback);

                int result = SSL_accept(this->parameters->getSSL());

                if (result != SSL_ERROR_NONE) {
                    SSLSocket::close();
                    throw OpenSSLSocketException(__FILE__, __LINE__);
                }
            }

            this->data->handshakeCompleted = true;
        }
#else
        throw IOException( __FILE__, __LINE__, "SSL Not Supported." );
#endif
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setUseClientMode(bool value) {

    synchronized( &( this->data->handshakeLock ) ) {
        if (this->data->handshakeStarted) {
            throw IllegalArgumentException(__FILE__, __LINE__,
                "Handshake has already been started cannot change mode.");
        }

        this->parameters->setUseClientMode(value);
    }
}

////////////////////////////////////////////////////////////////////////////////
bool OpenSSLSocket::getUseClientMode() const {
    return this->parameters->getUseClientMode();
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setNeedClientAuth(bool value) {
    this->parameters->setNeedClientAuth(value);
}

////////////////////////////////////////////////////////////////////////////////
bool OpenSSLSocket::getNeedClientAuth() const {
    return this->parameters->getNeedClientAuth();
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::setWantClientAuth(bool value) {
    this->parameters->setWantClientAuth(value);
}

////////////////////////////////////////////////////////////////////////////////
bool OpenSSLSocket::getWantClientAuth() const {
    return this->parameters->getWantClientAuth();
}

////////////////////////////////////////////////////////////////////////////////
int OpenSSLSocket::read(unsigned char* buffer, int size, int offset, int length) {

    try {
        if (this->isClosed()) {
            throw IOException(__FILE__, __LINE__, "The Stream has been closed");
        }

        if (this->isInputShutdown() == true) {
            return -1;
        }

        if (length == 0) {
            return 0;
        }

        if (buffer == NULL) {
            throw NullPointerException(__FILE__, __LINE__, "Buffer passed is Null");
        }

        if (size < 0) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "size parameter out of Bounds: %d.", size);
        }

        if (offset > size || offset < 0) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "offset parameter out of Bounds: %d.", offset);
        }

        if (length < 0 || length > size - offset) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "length parameter out of Bounds: %d.", length);
        }

#ifdef HAVE_OPENSSL

        if (!this->data->handshakeCompleted) {
            this->startHandshake();
        }

        // Read data from the socket.
        int result = SSL_read(this->parameters->getSSL(), buffer + offset, length);

        switch (SSL_get_error(this->parameters->getSSL(), result)) {
        case SSL_ERROR_NONE:
            return result;
        case SSL_ERROR_ZERO_RETURN:
            if (!isClosed()) {
                this->shutdownInput();
                return -1;
            }
        default:
            throw OpenSSLSocketException(__FILE__, __LINE__);
        }
#else
        throw SocketException( __FILE__, __LINE__, "Not Supported" );
#endif
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_RETHROW(NullPointerException)
    DECAF_CATCH_RETHROW(IndexOutOfBoundsException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::write(const unsigned char* buffer, int size, int offset, int length) {

    try {

        if (length == 0) {
            return;
        }

        if (buffer == NULL) {
            throw NullPointerException(__FILE__, __LINE__,
                "TcpSocketOutputStream::write - passed buffer is null");
        }

        if (isClosed()) {
            throw IOException(__FILE__, __LINE__,
                "TcpSocketOutputStream::write - This Stream has been closed.");
        }

        if (size < 0) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "size parameter out of Bounds: %d.", size);
        }

        if (offset > size || offset < 0) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "offset parameter out of Bounds: %d.", offset);
        }

        if (length < 0 || length > size - offset) {
            throw IndexOutOfBoundsException(__FILE__, __LINE__,
                "length parameter out of Bounds: %d.", length);
        }

#ifdef HAVE_OPENSSL

        if (!this->data->handshakeCompleted) {
            this->startHandshake();
        }

        int remaining = length;

        while (remaining > 0 && !isClosed()) {

            int written = SSL_write(this->parameters->getSSL(), buffer + offset, remaining);

            switch (SSL_get_error(this->parameters->getSSL(), written)) {
            case SSL_ERROR_NONE:
                offset += written;
                remaining -= written;
                break;
            case SSL_ERROR_ZERO_RETURN:
                throw SocketException(__FILE__, __LINE__, "The connection was broken unexpectedly.");
            default:
                throw OpenSSLSocketException(__FILE__, __LINE__);
            }
        }
#else
        throw SocketException( __FILE__, __LINE__, "Not Supported" );
#endif
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCH_RETHROW(NullPointerException)
    DECAF_CATCH_RETHROW(IndexOutOfBoundsException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
int OpenSSLSocket::available() {

    try {

#ifdef HAVE_OPENSSL
        if (!isClosed()) {
            return SSL_pending(this->parameters->getSSL());
        }
#else
        throw SocketException( __FILE__, __LINE__, "Not Supported" );
#endif

        return -1;
    }
    DECAF_CATCH_RETHROW(IOException)
    DECAF_CATCHALL_THROW(IOException)
}

////////////////////////////////////////////////////////////////////////////////
void OpenSSLSocket::verifyServerCert(const std::string& serverName) {

#ifdef HAVE_OPENSSL
    X509* cert = SSL_get_peer_certificate(this->parameters->getSSL());

    if (cert == NULL) {
        this->close();
        throw OpenSSLSocketException(__FILE__, __LINE__, "No server certificate for verify for host: %s", serverName.c_str());
    }

    class Finalizer {
    private:

        Finalizer(const Finalizer&);
        Finalizer& operator=(const Finalizer&);

    private:

        X509* cert;

    public:

        Finalizer(X509* cert) : cert(cert) {}
        ~Finalizer() {
            if (cert != NULL) {
                X509_free(cert);
            }
        }
    };

    // Store the Certificate to be cleaned up when the method returns
    Finalizer final(cert);

    // We check the extensions first since newer x509v3 Certificates are recommended
    // to store the FQDN in the dsnName field of the subjectAltName extension.  If we
    // don't find it there then we can check the commonName field which is where older
    // Certificates placed the FQDN.
    int extensions = X509_get_ext_count(cert);

    for (int ix = 0; ix < extensions; ix++) {

        X509_EXTENSION* extension = X509_get_ext(cert, ix);
        const char* extensionName = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(extension)));

        if (StringUtils::compare("subjectAltName", extensionName) == 0) {
            X509V3_EXT_METHOD* method = (X509V3_EXT_METHOD*) X509V3_EXT_get(extension);
            if (method == NULL) {
                break;
            }

            bool found = false;
            const unsigned char* data = ASN1_STRING_data(X509_EXTENSION_get_data(extension));
            long length = ASN1_STRING_length(X509_EXTENSION_get_data(extension));
            void* ext_data;

            if (method->it) {
                ext_data = ASN1_item_d2i(NULL, &data, length, ASN1_ITEM_ptr(method->it));
            } else {
                ext_data = method->d2i(NULL, &data, length);
            }
            STACK_OF(CONF_VALUE)* confValue = method->i2v(method, ext_data, NULL);

            CONF_VALUE* value = NULL;

            for (int iy = 0; iy < sk_CONF_VALUE_num( confValue ); iy++) {
                value = sk_CONF_VALUE_value(confValue, iy);
                if ((StringUtils::compare(value->name, "DNS") == 0) && StringUtils::compare(value->value, serverName.c_str()) == 0) {
                    found = true;
                    break;
                }
            }

            sk_CONF_VALUE_pop_free(confValue, X509V3_conf_free);
            if (method->it) {
                ASN1_item_free((ASN1_VALUE*)ext_data, ASN1_ITEM_ptr(method->it));
            } else {
                method->ext_free(ext_data);
            }

            if (found) {
                return;
            }
        }
    }

    X509_NAME* subject = X509_get_subject_name(cert);
    X509_NAME_ENTRY *entry;
    int lastpos = -1;

    if (subject != NULL) {
       for (;;) {
           lastpos = X509_NAME_get_index_by_NID(subject, NID_commonName, lastpos);
           if (lastpos == -1) {
               break;
           }
           entry = X509_NAME_get_entry(subject, lastpos);
           const char * entryText = (const char *) ASN1_STRING_data(X509_NAME_ENTRY_get_data(entry));
            if (StringUtils::compare(entryText , serverName.c_str()) == 0) {
                return;
            }
       }
    }

    // We got here so no match to serverName in the Certificate
    throw OpenSSLSocketException(__FILE__, __LINE__,
        "Server Certificate Name doesn't match the URI Host Name value.");
#endif
}
