/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/* Cherokee
 *
 * Authors:
 *      Alvaro Lopez Ortega <alvaro@alobbs.com>
 *
 * Copyright (C) 2001-2011 Alvaro Lopez Ortega
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/* IN CASE THIS PLUG-IN IS COMPILED WITH OPENSSL:
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects for
 * all of the code used other than OpenSSL.  If you modify file(s)
 * with this exception, you may extend this exception to your version
 * of the file(s), but you are not obligated to do so.  If you do not
 * wish to do so, delete this exception statement from your version.
 * If you delete this exception statement from all source files in the
 * program, then also delete it here.
 */

#include "common-internal.h"
#include "cryptor_libssl.h"
#include "virtual_server.h"
#include "socket.h"
#include "util.h"
#include "server-protected.h"

#define ENTRIES "crypto,ssl"

static DH *dh_param_512  = NULL;
static DH *dh_param_1024 = NULL;
static DH *dh_param_2048 = NULL;
static DH *dh_param_4096 = NULL;

#include "cryptor_libssl_dh_512.c"
#include "cryptor_libssl_dh_1024.c"
#include "cryptor_libssl_dh_2048.c"
#include "cryptor_libssl_dh_4096.c"

#define CLEAR_LIBSSL_ERRORS						\
	do {								\
		unsigned long openssl_error;				\
		while ((openssl_error = ERR_get_error())) {		\
			TRACE(ENTRIES, "Ignoring libssl error: %s\n",	\
			      ERR_error_string(openssl_error, NULL));	\
		}							\
	} while(0)


static ret_t
_free (cherokee_cryptor_libssl_t *cryp)
{
	/* DH Parameters
	 */
	if (dh_param_512  != NULL) {
		DH_free (dh_param_512);
		dh_param_512 = NULL;
	}

	if (dh_param_1024 != NULL) {
		DH_free (dh_param_1024);
		dh_param_1024 = NULL;
	}

	if (dh_param_2048 != NULL) {
		DH_free (dh_param_2048);
		dh_param_2048 = NULL;
	}

	if (dh_param_4096 != NULL) {
		DH_free (dh_param_4096);
		dh_param_4096 = NULL;
	}

	/* Free loaded error strings
	 */
	ERR_free_strings();

	/* Free all ciphers and digests
	 */
	EVP_cleanup();

	cherokee_cryptor_free_base (CRYPTOR(cryp));
	return ret_ok;
}

static ret_t
try_read_dh_param(cherokee_config_node_t  *conf,
		  DH                     **dh,
		  int                      bitsize)
{
	ret_t              ret;
	cherokee_buffer_t *buf;
	FILE              *paramfile = NULL;
	cherokee_buffer_t  confentry = CHEROKEE_BUF_INIT;

	cherokee_buffer_add_va (&confentry, "dh_param%d", bitsize);

	/* Read the configuration parameter
	 */
	ret = cherokee_config_node_read (conf, confentry.buf, &buf);
	if (ret != ret_ok) {
		ret = ret_ok;
		goto out;
	}

	/* Read the file
	 */
	paramfile = fopen (buf->buf, "r");
	if (paramfile == NULL) {
		TRACE(ENTRIES, "Cannot open file %s\n", buf->buf);
		ret = ret_file_not_found;
		goto out;
	}

	/* Process the content
	 */
	*dh = PEM_read_DHparams (paramfile, NULL, NULL, NULL);
	if (*dh == NULL) {
		TRACE(ENTRIES, "Failed to load PEM %s\n", buf->buf);
		ret = ret_error;
		goto out;
	}

	ret = ret_ok;

out:
	/* Clean up
	 */
	if (paramfile != NULL) {
		fclose (paramfile);
	}

	cherokee_buffer_mrproper (&confentry);
	return ret;
}

static ret_t
_configure (cherokee_cryptor_t     *cryp,
	    cherokee_config_node_t *conf,
	    cherokee_server_t      *srv)
{
	ret_t ret;

	UNUSED(cryp);
	UNUSED(srv);

	ret = try_read_dh_param (conf, &dh_param_512, 512);
	if (ret != ret_ok)
		return ret;

	ret = try_read_dh_param (conf, &dh_param_1024, 1024);
	if (ret != ret_ok)
		return ret;

	ret = try_read_dh_param (conf, &dh_param_2048, 2048);
	if (ret != ret_ok)
		return ret;

	ret = try_read_dh_param (conf, &dh_param_4096, 4096);
	if (ret != ret_ok)
		return ret;

	return ret_ok;
}

static ret_t
_vserver_free (cherokee_cryptor_vserver_libssl_t *cryp_vsrv)
{
	if (cryp_vsrv->context != NULL) {
		SSL_CTX_free (cryp_vsrv->context);
		cryp_vsrv->context = NULL;
	}

	free (cryp_vsrv);
	return ret_ok;
}

ret_t
cherokee_cryptor_libssl_find_vserver (SSL *ssl,
				      cherokee_server_t     *srv,
				      cherokee_buffer_t     *servername,
				      cherokee_connection_t *conn)
{
	ret_t                      ret;
	cherokee_virtual_server_t *vsrv = NULL;
	SSL_CTX                   *ctx;

	/* Try to match the connection to a server
	 */

	ret = cherokee_server_get_vserver(srv, servername, conn, &vsrv);
	if ((ret != ret_ok) || (vsrv == NULL)) {
		LOG_ERROR (CHEROKEE_ERROR_SSL_SRV_MATCH, servername->buf);
		return ret_error;
	}

	TRACE (ENTRIES, "Setting new TLS context. Virtual host='%s'\n",
	       vsrv->name.buf);

	/* Check whether the Virtual Server supports TLS
	 */
	if ((vsrv->cryptor == NULL) ||
	    (CRYPTOR_VSRV_SSL(vsrv->cryptor)->context == NULL))
	{
		TRACE (ENTRIES, "Virtual server '%s' does not support SSL\n", servername->buf);
		return ret_error;
	}

	/* Set the new SSL context
	 */
	ctx = SSL_set_SSL_CTX (ssl, CRYPTOR_VSRV_SSL(vsrv->cryptor)->context);
	if (ctx != CRYPTOR_VSRV_SSL(vsrv->cryptor)->context) {
		LOG_ERROR (CHEROKEE_ERROR_SSL_CHANGE_CTX, servername->buf);
	}

	/* SSL_set_SSL_CTX() only change certificates. We need to
	 * changes more options by hand.
	 */
	SSL_set_options(ssl, SSL_CTX_get_options(ssl->ctx));

	if ((SSL_get_verify_mode(ssl) == SSL_VERIFY_NONE) ||
	    (SSL_num_renegotiations(ssl) == 0)) {

		SSL_set_verify(ssl, SSL_CTX_get_verify_mode(ssl->ctx),
		               SSL_CTX_get_verify_callback(ssl->ctx));
	}

	return ret_ok;
}

#ifndef OPENSSL_NO_TLSEXT
static int
openssl_sni_servername_cb (SSL *ssl, int *ad, void *arg)
{
	ret_t                      ret;
	int                        re;
	const char                *servername;
	cherokee_connection_t     *conn;
	cherokee_buffer_t          tmp;
	cherokee_server_t         *srv       = SRV(arg);
	cherokee_virtual_server_t *vsrv      = NULL;

	UNUSED(ad);

	/* Get the pointer to the socket
	 */
	conn = SSL_get_app_data (ssl);
	if (unlikely (conn == NULL)) {
		LOG_ERROR (CHEROKEE_ERROR_SSL_SOCKET, ssl);
		return SSL_TLSEXT_ERR_ALERT_FATAL;
	}

	cherokee_buffer_init(&tmp);
	cherokee_buffer_ensure_size(&tmp, 40);

	/* Read the SNI server name
	 */
	servername = SSL_get_servername (ssl, TLSEXT_NAMETYPE_host_name);
	if (servername == NULL) {
		/* Set the server name to the IP address if we couldn't get the host name via SNI
		 */
		cherokee_socket_ntop (&conn->socket, tmp.buf, tmp.size);
		TRACE (ENTRIES, "No SNI: Did not provide a server name, using IP='%s' as servername.\n", tmp.buf);
	} else {
		cherokee_buffer_add (&tmp, servername, strlen(servername));
		TRACE (ENTRIES, "SNI: Switching to servername='%s'\n", servername);
	}

	/* Look up and change the vserver
	 */
	ret = cherokee_cryptor_libssl_find_vserver(ssl, srv, &tmp, conn);
	if (ret != ret_ok) {
		re = SSL_TLSEXT_ERR_NOACK;
	}
	else {
		re = SSL_TLSEXT_ERR_OK;
	}

	cherokee_buffer_mrproper (&tmp);
	return re;
}
#endif

static DH *
tmp_dh_cb (SSL *ssl, int export, int keylen)
{
	UNUSED(ssl);
	UNUSED(export);

	switch (keylen) {
	case 512:
		if (dh_param_512 == NULL) {
			dh_param_512 = get_dh512();
		}
		return dh_param_512;

	case 1024:
		if (dh_param_1024 == NULL) {
			dh_param_1024 = get_dh1024();
		}
		return dh_param_1024;

	case 2048:
		if (dh_param_2048 == NULL) {
			dh_param_2048 = get_dh2048();
		}
		return dh_param_2048;

	case 4096:
		if (dh_param_4096 == NULL) {
			dh_param_4096 = get_dh4096();
		}
		return dh_param_4096;
	}
	return NULL;
}


static ret_t
_vserver_new (cherokee_cryptor_t          *cryp,
	      cherokee_virtual_server_t   *vsrv,
	      cherokee_cryptor_vserver_t **cryp_vsrv)
{
	ret_t       ret;
	int         rc;
	const char *error;
	long        options;
	int         verify_mode = SSL_VERIFY_NONE;

	CHEROKEE_NEW_STRUCT (n, cryptor_vserver_libssl);

	UNUSED(cryp);

	/* Init
	 */
	ret = cherokee_cryptor_vserver_init_base (CRYPTOR_VSRV(n));
	if (ret != ret_ok) {
		free (n);
		return ret;
	}

	CRYPTOR_VSRV(n)->free = (cryptor_vsrv_func_free_t) _vserver_free;

	/* Init the OpenSSL context
	 */
	n->context = SSL_CTX_new (SSLv23_server_method());
	if (n->context == NULL) {
		LOG_ERROR_S(CHEROKEE_ERROR_SSL_ALLOCATE_CTX);
		goto error;
	}

	/* Callback to be used when a DH parameters are required
	 */
	SSL_CTX_set_tmp_dh_callback (n->context, tmp_dh_cb);

	/* Set the SSL context options:
	 */
	options  = SSL_OP_ALL;
	options |= SSL_OP_SINGLE_DH_USE;

#ifdef SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS
	options |= SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS;
#endif

	if (! cryp->allow_SSLv2) {
		options |= SSL_OP_NO_SSLv2;
	}

	SSL_CTX_set_options (n->context, options);

	/* Set cipher list that vserver will accept.
	 */
	if (! cherokee_buffer_is_empty (&vsrv->ciphers)) {
		rc = SSL_CTX_set_cipher_list (n->context, vsrv->ciphers.buf);
		if (rc != 1) {
			OPENSSL_LAST_ERROR(error);
			LOG_ERROR(CHEROKEE_ERROR_SSL_CIPHER,
				  vsrv->ciphers.buf, error);
			goto error;
		}
	}

	CLEAR_LIBSSL_ERRORS;

	/* Certificate
	 */
	TRACE(ENTRIES, "Vserver '%s'. Reading certificate file '%s'\n",
	      vsrv->name.buf, vsrv->server_cert.buf);

	rc = SSL_CTX_use_certificate_chain_file (n->context, vsrv->server_cert.buf);
	if (rc != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR(CHEROKEE_ERROR_SSL_CERTIFICATE,
			  vsrv->server_cert.buf, error);
		goto error;
	}

	/* Private key
	 */
	TRACE(ENTRIES, "Vserver '%s'. Reading key file '%s'\n",
	      vsrv->name.buf, vsrv->server_key.buf);

	rc = SSL_CTX_use_PrivateKey_file (n->context, vsrv->server_key.buf, SSL_FILETYPE_PEM);
	if (rc != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR(CHEROKEE_ERROR_SSL_KEY, vsrv->server_key.buf, error);
		goto error;
	}

	/* Check private key
	 */
	rc = SSL_CTX_check_private_key (n->context);
	if (rc != 1) {
		LOG_ERROR_S(CHEROKEE_ERROR_SSL_KEY_MATCH);
		goto error;
	}

	if (! cherokee_buffer_is_empty (&vsrv->req_client_certs)) {
		STACK_OF(X509_NAME) *X509_clients;

		verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
		if (cherokee_buffer_cmp_str (&vsrv->req_client_certs, "required") == 0) {
			verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
		}

		/* Trusted CA certificates
		*/
		if (! cherokee_buffer_is_empty (&vsrv->certs_ca)) {
			rc = SSL_CTX_load_verify_locations (n->context, vsrv->certs_ca.buf, NULL);
			if (rc != 1) {
				OPENSSL_LAST_ERROR(error);
				LOG_CRITICAL(CHEROKEE_ERROR_SSL_CA_READ,
					     vsrv->certs_ca.buf, error);
				goto error;
			}

			X509_clients = SSL_load_client_CA_file (vsrv->certs_ca.buf);
			if (X509_clients == NULL) {
				OPENSSL_LAST_ERROR(error);
				LOG_CRITICAL (CHEROKEE_ERROR_SSL_CA_LOAD,
					      vsrv->certs_ca.buf, error);
				goto error;
			}

			CLEAR_LIBSSL_ERRORS;

			SSL_CTX_set_client_CA_list (n->context, X509_clients);
			TRACE (ENTRIES, "Setting client CA list: %s on '%s'\n", vsrv->certs_ca.buf, vsrv->name.buf);
		} else {
			verify_mode = SSL_VERIFY_NONE;
		}
	}

	SSL_CTX_set_verify (n->context, verify_mode, NULL);
	SSL_CTX_set_verify_depth (n->context, vsrv->verify_depth);

	SSL_CTX_set_read_ahead (n->context, 1);
	SSL_CTX_set_mode (n->context,
			  SSL_CTX_get_mode(n->context) | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);

	/* Set the SSL context cache
	 */
	rc = SSL_CTX_set_session_id_context (n->context,
					     (unsigned char *) vsrv->name.buf,
					     (unsigned int)    vsrv->name.len);
	if (rc != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_SESSION_ID, vsrv->name.buf, error);
	}

	SSL_CTX_set_session_cache_mode (n->context, SSL_SESS_CACHE_SERVER);


#ifndef OPENSSL_NO_TLSEXT
	/* Enable SNI
	 */
	rc = SSL_CTX_set_tlsext_servername_callback (n->context, openssl_sni_servername_cb);
	if (rc != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_WARNING (CHEROKEE_ERROR_SSL_SNI, vsrv->name.buf, error);
	} else {
		rc = SSL_CTX_set_tlsext_servername_arg (n->context, VSERVER_SRV(vsrv));
		if (rc != 1) {
			OPENSSL_LAST_ERROR(error);
			LOG_WARNING (CHEROKEE_ERROR_SSL_SNI, vsrv->name.buf, error);
		}
	}
#endif /* OPENSSL_NO_TLSEXT */

	*cryp_vsrv = CRYPTOR_VSRV(n);
	return ret_ok;

error:
	if (n->context != NULL) {
		SSL_CTX_free (n->context);
		n->context = NULL;
	}

	free (n);
	return ret_error;
}


static ret_t
socket_initialize (cherokee_cryptor_socket_libssl_t *cryp,
		   cherokee_socket_t                *socket,
		   cherokee_virtual_server_t        *vserver,
		   cherokee_connection_t            *conn)
{
	int                                re;
	const char                        *error;
	cherokee_cryptor_vserver_libssl_t *vsrv_crytor = CRYPTOR_VSRV_SSL(vserver->cryptor);

#ifdef OPENSSL_NO_TLSEXT
	cherokee_buffer_t                  servername = CHEROKEE_BUF_INIT;
#endif

	/* Set the virtual server object reference
	 */
	CRYPTOR_SOCKET(cryp)->vserver_ref = vserver;

	/* Check whether the virtual server supports SSL
	 */
	if (vserver->cryptor == NULL) {
		return ret_not_found;
	}

	if (vsrv_crytor->context == NULL) {
		return ret_not_found;
	}

	/* New session
	 */
	cryp->session = SSL_new (vsrv_crytor->context);
	if (cryp->session == NULL) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_CONNECTION, error);
		return ret_error;
	}

	/* Sets ssl to work in server mode
	 */
	SSL_set_accept_state (cryp->session);

	/* Set the socket file descriptor
	 */
	re = SSL_set_fd (cryp->session, socket->socket);
	if (re != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_FD,
			   socket->socket, error);
		return ret_error;
	}

#ifndef OPENSSL_NO_TLSEXT
	SSL_set_app_data (cryp->session, conn);
#else
	/* Attempt to determine the vserver without SNI.
	 */
	cherokee_buffer_ensure_size(&servername, 40);
	cherokee_socket_ntop (&conn->socket, servername.buf, servername.size);
	cherokee_cryptor_libssl_find_vserver (cryp->session, CONN_SRV(conn), &servername, conn);
	cherokee_buffer_mrproper (&servername);
#endif

	return ret_ok;
}


static ret_t
_socket_init_tls (cherokee_cryptor_socket_libssl_t *cryp,
		  cherokee_socket_t                *sock,
		  cherokee_virtual_server_t        *vsrv,
		  cherokee_connection_t            *conn,
		  cherokee_socket_status_t         *blocking)
{
	int   re;
	ret_t ret;

	/* Initialize
	 */
	if (CRYPTOR_SOCKET(cryp)->initialized == false) {
		ret = socket_initialize (cryp, sock, vsrv, conn);
		if (ret != ret_ok) {
			return ret_error;
		}

		CRYPTOR_SOCKET(cryp)->initialized = true;
	}

	/* TLS Handshake
	 */
	CLEAR_LIBSSL_ERRORS;

	re = SSL_do_handshake (cryp->session);
	if (re == 0) {
		/* The TLS/SSL handshake was not successful but was
		 * shut down controlled and by the specifications of
		 * the TLS/SSL protocol.
		 */
		return ret_eof;

	} else if (re <= 0) {
		int         err;
		const char *error;
		int         err_sys = errno;

		err = SSL_get_error (cryp->session, re);
		switch (err) {
		case SSL_ERROR_WANT_READ:
			*blocking = socket_reading;
			return ret_eagain;

		case SSL_ERROR_WANT_WRITE:
		case SSL_ERROR_WANT_CONNECT:
		case SSL_ERROR_WANT_ACCEPT:
			*blocking = socket_writing;
			return ret_eagain;

		case SSL_ERROR_SYSCALL:
			if (err_sys == EAGAIN) {
				return ret_eagain;
			}
			return ret_error;

		case SSL_ERROR_SSL:
		case SSL_ERROR_ZERO_RETURN:
			return ret_error;
		default:
			OPENSSL_LAST_ERROR(error);
			LOG_ERROR (CHEROKEE_ERROR_SSL_INIT, error);
			return ret_error;
		}
	}

	/* Report the connection details
	 */
#ifdef TRACE_ENABLED
	{
		CHEROKEE_TEMP (buf,256);
		SSL_CIPHER *cipher = SSL_get_current_cipher (cryp->session);

		if (cipher) {
			SSL_CIPHER_description (cipher, &buf[0], buf_size-1);

			TRACE (ENTRIES, "SSL: %s, %sREUSED, Ciphers: %s",
			       SSL_get_version(cryp->session),
			       SSL_session_reused(cryp->session)? "" : "Not ", &buf[0]);
		}
	}
#endif

	/* Disable Ciphers renegotiation (CVE-2009-3555)
	 */
	if (cryp->session->s3) {
		cryp->session->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS;
	}

	return ret_ok;
}

static ret_t
_socket_shutdown (cherokee_cryptor_socket_libssl_t *cryp)
{
	int re;
	int ssl_error;

	if (unlikely (cryp->session == NULL)) {
		return ret_ok;
	}

	/* Send a 'close_notify' SSL message
	 */
	errno = 0;
	CLEAR_LIBSSL_ERRORS;

	re = SSL_shutdown (cryp->session);
	if (re == 1) {
		/* The shutdown was successfully completed. */
		return ret_ok;

	} else if (re == 0) {
		/* Shutdown in progress */
		int ssl_err = SSL_get_error(cryp->session, re);

		/* According to OpenSSL bug #611, something bad might
		 * have happened during the call to SSL_shutdown()..
		 */
		switch (ssl_err) {
		case SSL_ERROR_ZERO_RETURN:
			return ret_ok;
		case SSL_ERROR_SYSCALL: {
			unsigned long e = ERR_get_error();

			if (e == 0) {
				/* EOF occurred in violation of protocol */
				return ret_eof;

			} else if (e == -1) {
				/* Underlying BIO reported an I/O error */
				switch (errno) {
				case EINTR:
				case EAGAIN:
					return ret_eagain;
				case EIO:
				case EPIPE:
				case ECONNRESET:
					return ret_eof;
				default:
					return ret_error;
				}
			}
			return ret_error;
		}
		default:
			return ret_error;
		}

	} else if (re < 0) {
		ssl_error = SSL_get_error (cryp->session, re);
		switch (ssl_error) {
		case SSL_ERROR_ZERO_RETURN:
			return ret_ok;

		case SSL_ERROR_WANT_READ:
			return ret_eagain;
		case SSL_ERROR_WANT_WRITE:
			return ret_eagain;

		case SSL_ERROR_SYSCALL:
			CLEAR_LIBSSL_ERRORS;

			switch (errno) {
			case 0:
				return ret_ok;
			case EINTR:
			case EAGAIN:
				return ret_eagain;
			default:
				return ret_error;
			}
			break;

		default:
			return ret_error;
		}
	}

	SHOULDNT_HAPPEN;
	return ret_error;
}

static ret_t
_socket_write (cherokee_cryptor_socket_libssl_t *cryp,
	       char                             *buf,
	       int                               buf_len,
	       size_t                           *pcnt_written)
{
	int     re;
	ssize_t len;
	int     error;

	/* 'Truco del almendruco': This is a method to bypass the
	 * limitations of the libssl sockets regard to SSL_write.
	 *
	 * A buffer being sent cannot be modified. Since Cherokee's
	 * core does that by default (it removes the chunk of info
	 * already sent), we have to trick it. This piece of code
	 * reports EAGAIN errors until the complete buffer is sent.
	 */

	/* Stage 1: Keep track of the buffer
	 */
	if (cryp->writing.buf != buf) {
		TRACE (ENTRIES, "SSL-Write. Sets new buffer: %p (len %d)\n", buf, buf_len);

		cryp->writing.buf     = buf;
		cryp->writing.buf_len = buf_len;
		cryp->writing.written = 0;
	}

	/* Proceed to write
	 */
	CLEAR_LIBSSL_ERRORS;

	len = SSL_write (cryp->session, buf, buf_len);
	if (likely (len > 0) ) {

		/* Stage 2: Lie, if required
		 */
		cryp->writing.written += len;

		if (cryp->writing.written >= buf_len) {
			TRACE (ENTRIES, "SSL-Write. Buffer sent: %p (total len %d)\n", buf, buf_len);
			*pcnt_written = buf_len;
			return ret_ok;
		}

		TRACE (ENTRIES",lie", "SSL-Write lies, (package %d, written %d, total %d): eagain\n", len, cryp->writing.written, buf_len);
		return ret_eagain;
	}

	if (len == 0) {
		TRACE (ENTRIES",write", "write got %s\n", "EOF");
		return ret_eof;
	}

	/* len < 0 */
	error = errno;

	re = SSL_get_error (cryp->session, len);
	switch (re) {
	case SSL_ERROR_WANT_READ:
	case SSL_ERROR_WANT_WRITE:
		TRACE (ENTRIES",write", "write len=%d (written=0), EAGAIN\n", buf_len);
		return ret_eagain;

	case SSL_ERROR_SYSCALL:
		switch (error) {
		case EAGAIN:
			TRACE (ENTRIES",write", "write len=%d (written=0), EAGAIN\n", buf_len);
			return ret_eagain;
#ifdef ENOTCONN
		case ENOTCONN:
#endif
		case EPIPE:
		case ECONNRESET:
			TRACE (ENTRIES",write", "write len=%d (written=0), EOF\n", buf_len);
			return ret_eof;
		default:
			LOG_ERRNO_S (error, cherokee_err_error,
				     CHEROKEE_ERROR_SSL_SW_DEFAULT);
		}

		TRACE (ENTRIES",write", "write len=%d (written=0), ERROR: %s\n", buf_len, ERR_error_string(re, NULL));
		return ret_error;

	case SSL_ERROR_SSL:
		TRACE (ENTRIES",write", "write len=%d (written=0), ERROR: %s\n", buf_len, ERR_error_string(re, NULL));
		return ret_error;
	}

	LOG_ERROR (CHEROKEE_ERROR_SSL_SW_ERROR,
		   SSL_get_fd(cryp->session), (int)len, ERR_error_string(re, NULL));

	TRACE (ENTRIES",write", "write len=%d (written=0), ERROR: %s\n", buf_len, ERR_error_string(re, NULL));
	return ret_error;
}


static ret_t
_socket_read (cherokee_cryptor_socket_libssl_t *cryp,
	      char                             *buf,
	      int                               buf_size,
	      size_t                           *pcnt_read)
{
	int     re;
	int     error;
	ssize_t len;

	CLEAR_LIBSSL_ERRORS;

	len = SSL_read (cryp->session, buf, buf_size);
	if (likely (len > 0)) {
		*pcnt_read = len;
		if (SSL_pending (cryp->session))
			return ret_eagain;
		return ret_ok;
	}

	if (len == 0) {
		return ret_eof;
	}

	/* len < 0 */
	error = errno;
	re = SSL_get_error (cryp->session, len);
	switch (re) {
	case SSL_ERROR_WANT_READ:
	case SSL_ERROR_WANT_WRITE:
		return ret_eagain;
	case SSL_ERROR_ZERO_RETURN:
		return ret_eof;
	case SSL_ERROR_SSL:
		return ret_error;
	case SSL_ERROR_SYSCALL:
		switch (error) {
		case EAGAIN:
			return ret_eagain;
		case EPIPE:
		case ECONNRESET:
			return ret_eof;
		default:
			LOG_ERRNO_S (error, cherokee_err_error,
				     CHEROKEE_ERROR_SSL_SR_DEFAULT);
		}
		return ret_error;
	}

	LOG_ERROR (CHEROKEE_ERROR_SSL_SR_ERROR,
		   SSL_get_fd(cryp->session), (int)len, ERR_error_string(re, NULL));
	return ret_error;
}

static int
_socket_pending (cherokee_cryptor_socket_libssl_t *cryp)
{
	return (SSL_pending (cryp->session) > 0);
}

static ret_t
_socket_clean (cherokee_cryptor_socket_libssl_t *cryp_socket)
{
	cherokee_cryptor_socket_clean_base (CRYPTOR_SOCKET(cryp_socket));

	if (cryp_socket->session != NULL) {
		SSL_free (cryp_socket->session);
		cryp_socket->session = NULL;
	}

	if (cryp_socket->ssl_ctx != NULL) {
		SSL_CTX_free (cryp_socket->ssl_ctx);
		cryp_socket->ssl_ctx = NULL;
	}

	return ret_ok;
}

static ret_t
_socket_free (cherokee_cryptor_socket_libssl_t *cryp_socket)
{
	_socket_clean (cryp_socket);

	free (cryp_socket);
	return ret_ok;
}

static ret_t
_socket_new (cherokee_cryptor_libssl_t         *cryp,
	     cherokee_cryptor_socket_libssl_t **cryp_socket)
{
	ret_t ret;
	CHEROKEE_NEW_STRUCT (n, cryptor_socket_libssl);

	UNUSED(cryp);

	ret = cherokee_cryptor_socket_init_base (CRYPTOR_SOCKET(n));
	if (unlikely (ret != ret_ok))
		return ret;

	/* Socket properties */
	n->session = NULL;
	n->ssl_ctx = NULL;

	n->writing.buf     = NULL;
	n->writing.buf_len = -1;
	n->writing.written = -1;

	/* Virtual methods */
	CRYPTOR_SOCKET(n)->free     = (cryptor_socket_func_free_t) _socket_free;
	CRYPTOR_SOCKET(n)->clean    = (cryptor_socket_func_clean_t) _socket_clean;
	CRYPTOR_SOCKET(n)->init_tls = (cryptor_socket_func_init_tls_t) _socket_init_tls;
	CRYPTOR_SOCKET(n)->shutdown = (cryptor_socket_func_shutdown_t) _socket_shutdown;
	CRYPTOR_SOCKET(n)->read     = (cryptor_socket_func_read_t) _socket_read;
	CRYPTOR_SOCKET(n)->write    = (cryptor_socket_func_write_t) _socket_write;
	CRYPTOR_SOCKET(n)->pending  = (cryptor_socket_func_pending_t) _socket_pending;

	*cryp_socket = n;
	return ret_ok;
}

static ret_t
_client_init_tls (cherokee_cryptor_client_libssl_t *cryp,
		  cherokee_buffer_t                *host,
		  cherokee_socket_t                *socket)
{
	int         re;
	const char *error;

	/* New context
	 */
	cryp->ssl_ctx = SSL_CTX_new (SSLv23_client_method());
	if (cryp->ssl_ctx == NULL) {
		OPENSSL_LAST_ERROR(error);
		LOG_CRITICAL (CHEROKEE_ERROR_SSL_CREATE_CTX, error);
		return ret_error;
	}

	/* CA verifications
	re = cherokee_buffer_is_empty (&cryp->vserver_ref->certs_ca);
	if (! re) {
		re = SSL_CTX_load_verify_locations (socket->ssl_ctx,
						    socket->vserver_ref->certs_ca.buf, NULL);
		if (! re) {
			OPENSSL_LAST_ERROR(error);
			LOG_ERROR (CHEROKEE_ERROR_SSL_CTX_LOAD,
			           socket->vserver_ref->certs_ca.buf, error);
			return ret_error;
		}

		re = SSL_CTX_set_default_verify_paths (socket->ssl_ctx);
		if (! re) {
			OPENSSL_LAST_ERROR(error);
			LOG_ERROR (CHEROKEE_ERROR_SSL_CTX_SET, error);
			return ret_error;
		}
	}
	 */

	SSL_CTX_set_verify (cryp->ssl_ctx, SSL_VERIFY_NONE, NULL);

	/* New session
	 */
	cryp->session = SSL_new (cryp->ssl_ctx);
	if (cryp->session == NULL) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_CONNECTION, error);
		return ret_error;
	}

	/* Set the socket file descriptor
	 */
	re = SSL_set_fd (cryp->session, socket->socket);
	if (re != 1) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_FD, socket->socket, error);
		return ret_error;
	}

	SSL_set_connect_state (cryp->session);

#ifndef OPENSSL_NO_TLSEXT
	if ((host != NULL) &&
	    (! cherokee_buffer_is_empty (host)))
	{
		re = SSL_set_tlsext_host_name (cryp->session, host->buf);
		if (re <= 0) {
			OPENSSL_LAST_ERROR(error);
			LOG_ERROR (CHEROKEE_ERROR_SSL_SNI_SRV, error);
			return ret_error;
		}
	}
#endif

	re = SSL_connect (cryp->session);
	if (re <= 0) {
		OPENSSL_LAST_ERROR(error);
		LOG_ERROR (CHEROKEE_ERROR_SSL_CONNECT, error);
		return ret_error;
	}

	return ret_ok;
}

static ret_t
_client_free (cherokee_cryptor_client_libssl_t *cryp)
{
	if (cryp->session != NULL) {
		SSL_free (cryp->session);
		cryp->session = NULL;
	}

	if (cryp->ssl_ctx != NULL) {
		SSL_CTX_free (cryp->ssl_ctx);
		cryp->ssl_ctx = NULL;
	}

	free (cryp);
	return ret_ok;
}

static ret_t
_client_new (cherokee_cryptor_t         *cryp,
	     cherokee_cryptor_client_t **cryp_client)
{
	ret_t ret;
	CHEROKEE_NEW_STRUCT (n, cryptor_client_libssl);

	UNUSED(cryp);

	ret = cherokee_cryptor_socket_init_base (CRYPTOR_SOCKET(n));
	if (unlikely (ret != ret_ok))
		return ret;

	/* Client properties */
	n->session = NULL;
	n->ssl_ctx = NULL;

	/* Socket */
	CRYPTOR_SOCKET(n)->shutdown = (cryptor_socket_func_shutdown_t) _socket_shutdown;
	CRYPTOR_SOCKET(n)->read     = (cryptor_socket_func_read_t) _socket_read;
	CRYPTOR_SOCKET(n)->write    = (cryptor_socket_func_write_t) _socket_write;
	CRYPTOR_SOCKET(n)->pending  = (cryptor_socket_func_pending_t) _socket_pending;
	CRYPTOR_SOCKET(n)->clean    = (cryptor_socket_func_clean_t) _socket_clean;

	/* Client */
	CRYPTOR_SOCKET(n)->free     = (cryptor_socket_func_free_t) _client_free;
	CRYPTOR_SOCKET(n)->init_tls = (cryptor_client_func_init_t) _client_init_tls;

	*cryp_client = CRYPTOR_CLIENT(n);
	return ret_ok;
}


PLUGIN_INFO_INIT (libssl,
		  cherokee_cryptor,
		  cherokee_cryptor_libssl_new,
		  NULL);


ret_t
cherokee_cryptor_libssl_new (cherokee_cryptor_libssl_t **cryp)
{
	ret_t ret;
	CHEROKEE_NEW_STRUCT (n, cryptor_libssl);

	/* Init
	 */
	ret = cherokee_cryptor_init_base (CRYPTOR(n), PLUGIN_INFO_PTR(libssl));
	if (ret != ret_ok)
		return ret;

	MODULE(n)->free         = (module_func_free_t) _free;
	CRYPTOR(n)->configure   = (cryptor_func_configure_t) _configure;
	CRYPTOR(n)->vserver_new = (cryptor_func_vserver_new_t) _vserver_new;
	CRYPTOR(n)->socket_new  = (cryptor_func_socket_new_t) _socket_new;
	CRYPTOR(n)->client_new  = (cryptor_func_client_new_t) _client_new;

	*cryp = n;
	return ret_ok;
}



/* Private low-level functions for OpenSSL
 */
static pthread_mutex_t *locks;
static size_t           locks_num;

static unsigned long
__get_thread_id (void)
{
	return (unsigned long) pthread_self();
}


static void
__lock_thread (int mode, int n, const char *file, int line)
{
	UNUSED (file);
	UNUSED (line);

	if (mode & CRYPTO_LOCK) {
		CHEROKEE_MUTEX_LOCK (&locks[n]);
	} else {
		CHEROKEE_MUTEX_UNLOCK (&locks[n]);
	}
}


/* Plug-in initialization
 */

cherokee_boolean_t PLUGIN_IS_INIT(libssl) = false;

void
PLUGIN_INIT_NAME(libssl) (cherokee_plugin_loader_t *loader)
{
#if HAVE_OPENSSL_ENGINE_H
	ENGINE *e;
#endif
	UNUSED(loader);

	/* Do not initialize the library twice
	 */
	PLUGIN_INIT_ONCE_CHECK (libssl);

	/* Init OpenSSL
	 */
	OPENSSL_config (NULL);
	SSL_library_init();
	SSL_load_error_strings();
	OpenSSL_add_all_algorithms();

	/* Ensure PRNG has been seeded with enough data
	 */
	if (RAND_status() == 0) {
		LOG_WARNING_S (CHEROKEE_ERROR_SSL_NO_ENTROPY);
	}

	/* Init concurrency related stuff
	 */
	if ((CRYPTO_get_id_callback()      == NULL) &&
	    (CRYPTO_get_locking_callback() == NULL))
	{
		cuint_t n;

		CRYPTO_set_id_callback (__get_thread_id);
		CRYPTO_set_locking_callback (__lock_thread);

		locks_num = CRYPTO_num_locks();
		locks     = malloc (locks_num * sizeof(*locks));

		for (n = 0; n < locks_num; n++) {
			CHEROKEE_MUTEX_INIT (&locks[n], NULL);
		}
	}


# if HAVE_OPENSSL_ENGINE_H
#  if OPENSSL_VERSION_NUMBER >= 0x00907000L
        ENGINE_load_builtin_engines();
	OpenSSL_add_all_algorithms();
#  endif
        e = ENGINE_by_id("pkcs11");
        while (e != NULL) {
                if(! ENGINE_init(e)) {
                        ENGINE_free (e);
                        LOG_CRITICAL_S (CHEROKEE_ERROR_SSL_PKCS11);
			break;
                }

                if(! ENGINE_set_default(e, ENGINE_METHOD_ALL)) {
                        ENGINE_free (e);
                        LOG_CRITICAL_S (CHEROKEE_ERROR_SSL_DEFAULTS);
			break;
                }

                ENGINE_finish(e);
                ENGINE_free(e);
		break;
        }
#endif
}
