/*
 * This file is part of the Sofia-SIP package
 *
 * Copyright (C) 2005 Nokia Corporation.
 *
 * Contact: Pekka Pessi <pekka.pessi@nokia.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */

/**@CFILE tport_tls.c
 * @brief TLS interface
 *
 * @author Mikko Haataja <ext-Mikko.A.Haataja@nokia.com>
 * @author Pekka Pessi <ext-Pekka.Pessi@nokia.com>
 * @author Jarod Neuner <janeuner@networkharbor.com>
 *
 */

#include "config.h"

#define OPENSSL_NO_KRB5 oh-no
#define SU_WAKEUP_ARG_T  struct tport_s

#include <sofia-sip/su_types.h>
#include <sofia-sip/su.h>
#include <sofia-sip/su_alloc.h>
#include <sofia-sip/su_wait.h>
#include <sofia-sip/su_string.h>

#include <openssl/lhash.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/bio.h>
#include <openssl/opensslv.h>

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#if HAVE_FUNC
#elif HAVE_FUNCTION
#define __func__ __FUNCTION__
#else
static char const __func__[] = "tport_tls";
#endif

#if HAVE_SIGPIPE
#include <signal.h>
#endif

#if SU_HAVE_PTHREADS

#include <pthread.h>

#if __sun
#undef PTHREAD_ONCE_INIT
#define PTHREAD_ONCE_INIT {{ 0, 0, 0, PTHREAD_ONCE_NOTDONE }}
#endif

static pthread_once_t once = PTHREAD_ONCE_INIT;
#define ONCE_INIT(f) pthread_once(&once, f)

#else

static int once;
#define ONCE_INIT(f) (!once ? (once = 1), f() : (void)0)

#endif

#include "tport_tls.h"

char const tls_version[] = OPENSSL_VERSION_TEXT;
static int tls_ex_data_idx = -1; /* see SSL_get_ex_new_index(3ssl) */

static void
tls_init_once(void)
{
  SSL_library_init();
  SSL_load_error_strings();
  tls_ex_data_idx = SSL_get_ex_new_index(0, "sofia-sip private data", NULL, NULL, NULL);
}

enum  { tls_master = 0, tls_slave = 1};

struct tls_s {
  su_home_t home[1];
  SSL_CTX *ctx;
  SSL *con;
  BIO *bio_con;
  unsigned int type:1,
               accept:1,
               verify_incoming:1,
               verify_outgoing:1,
	       verify_subj_in:1,
	       verify_subj_out:1,
	       verify_date:1,
               x509_verified:1;

  /* Receiving */
  int read_events;
  void *read_buffer;
  size_t read_buffer_len;

  /* Sending */
  int   write_events;
  void *write_buffer;
  size_t write_buffer_len;

  /* Host names */
  su_strlst_t *subjects;
};

enum { tls_buffer_size = 16384 };

/** Log TLS error(s).
 *
 * Log the TLS error specified by the error code @a e and all the errors in
 * the queue. The error code @a e implies no error, and it is not logged.
 */
static
void tls_log_errors(unsigned level, char const *s, unsigned long e)
{
  if (e == 0)
    e = ERR_get_error();

  if (!tport_log->log_init)
    su_log_init(tport_log);

  if (s == NULL) s = "tls";

  for (; e != 0; e = ERR_get_error()) {
    if (level <= tport_log->log_level) {
      const char *error = ERR_lib_error_string(e);
      const char *func = ERR_func_error_string(e);
      const char *reason = ERR_reason_error_string(e);

      su_llog(tport_log, level, "%s: %08lx:%s:%s:%s\n",
	      s, e, error, func, reason);
    }
  }
}

/*
 * This callback hands back the password to be used during decryption.
 *
 * buf      : the function will write the password into this buffer
 * size     : the size of "buf"
 * rwflag   : indicates whether the callback is being used for reading/
 *            decryption (0) or writing/encryption (1)
 * userdata : pointer tls_issues_t where the passphrase is stored
 */
static int passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
	if (rwflag == 0) { // reading/decryption
		tls_issues_t *tlsi = (tls_issues_t *)userdata;

		strncpy(buf, tlsi->passphrase, size);
		buf[size - 1] = '\0';

		return strlen(tlsi->passphrase);
	}
	return 0;
}

static
tls_t *tls_create(int type)
{
  tls_t *tls = su_home_new(sizeof(*tls));

  if (tls)
    tls->type = type == tls_master ? tls_master : tls_slave;

  return tls;
}


static
void tls_set_default(tls_issues_t *i)
{
  i->verify_depth = i->verify_depth == 0 ? 2 : i->verify_depth;
  i->cert = i->cert ? i->cert : "agent.pem";
  i->key = i->key ? i->key : i->cert;
  i->randFile = i->randFile ? i->randFile : "tls_seed.dat";
  i->CAfile = i->CAfile ? i->CAfile : "cafile.pem";
  i->ciphers = i->ciphers ? i->ciphers : "!eNULL:!aNULL:!EXP:!LOW:!MD5:ALL:@STRENGTH";
  /* Default SIP cipher */
  /* "RSA-WITH-AES-128-CBC-SHA"; */
  /* RFC-2543-compatibility ciphersuite */
  /* TLS_RSA_WITH_3DES_EDE_CBC_SHA; */
}


static
int tls_verify_cb(int ok, X509_STORE_CTX *store)
{
  if (!ok)
  {
    char data[256];

    X509 *cert = X509_STORE_CTX_get_current_cert(store);
    int  depth = X509_STORE_CTX_get_error_depth(store);
    int  err = X509_STORE_CTX_get_error(store);
    int  sslidx = SSL_get_ex_data_X509_STORE_CTX_idx();
    SSL  *ssl = X509_STORE_CTX_get_ex_data(store, sslidx);
    tls_t *tls = SSL_get_ex_data(ssl, tls_ex_data_idx);

    assert(tls);

#define TLS_VERIFY_CB_CLEAR_ERROR(OK,ERR,STORE) \
                   do {\
                     OK = 1;\
		     ERR = X509_V_OK;\
		     X509_STORE_CTX_set_error(STORE,ERR);\
		   } while (0)

    if (tls->accept && !tls->verify_incoming)
      TLS_VERIFY_CB_CLEAR_ERROR(ok, err, store);
    else if (!tls->accept && !tls->verify_outgoing)
      TLS_VERIFY_CB_CLEAR_ERROR(ok, err, store);
    else switch (err) {
      case X509_V_ERR_CERT_NOT_YET_VALID:
      case X509_V_ERR_CERT_HAS_EXPIRED:
      case X509_V_ERR_CRL_NOT_YET_VALID:
      case X509_V_ERR_CRL_HAS_EXPIRED:
        if (!tls->verify_date)
	  TLS_VERIFY_CB_CLEAR_ERROR(ok, err, store);

      default:
        break;
    }

    if (!ok) {
      SU_DEBUG_3(("-Error with certificate at depth: %i\n", depth));
      X509_NAME_oneline(X509_get_issuer_name(cert), data, 256);
      SU_DEBUG_3(("  issuer   = %s\n", data));
      X509_NAME_oneline(X509_get_subject_name(cert), data, 256);
      SU_DEBUG_3(("  subject  = %s\n", data));
      SU_DEBUG_3(("  err %i:%s\n", err, X509_verify_cert_error_string(err)));
    }

  }

  return ok;
}

void tls_init(void) {
  ONCE_INIT(tls_init_once);
}

#ifndef OPENSSL_NO_EC
static
int tls_init_ecdh_curve(tls_t *tls)
{
  int nid;
  EC_KEY *ecdh;
  if (!(nid = OBJ_sn2nid("prime256v1"))) {
    tls_log_errors(1, "Couldn't find specified curve", 0);
    errno = EIO;
    return -1;
  }
  if (!(ecdh = EC_KEY_new_by_curve_name(nid))) {
    tls_log_errors(1, "Couldn't create specified curve", 0);
    errno = EIO;
    return -1;
  }
  SSL_CTX_set_options(tls->ctx, SSL_OP_SINGLE_ECDH_USE);
  SSL_CTX_set_tmp_ecdh(tls->ctx, ecdh);
  EC_KEY_free(ecdh);
  return 0;
}
#endif

static
int tls_init_context(tls_t *tls, tls_issues_t const *ti)
{
  int verify;
  static int random_loaded;

  ONCE_INIT(tls_init_once);

  if (!random_loaded) {
    random_loaded = 1;

    if (ti->randFile &&
	!RAND_load_file(ti->randFile, 1024 * 1024)) {
      if (ti->configured > 1) {
	SU_DEBUG_3(("%s: cannot open randFile %s\n",
		   "tls_init_context", ti->randFile));
	tls_log_errors(3, "tls_init_context", 0);
      }
      /* errno = EIO; */
      /* return -1; */
    }
  }

#if HAVE_SIGPIPE
  /* Avoid possible SIGPIPE when sending close_notify */
  signal(SIGPIPE, SIG_IGN);
#endif

  if (tls->ctx == NULL)
    if (!(tls->ctx = SSL_CTX_new((SSL_METHOD*)SSLv23_method()))) {
      tls_log_errors(1, "SSL_CTX_new() failed", 0);
      errno = EIO;
      return -1;
    }
  if (!(ti->version & TPTLS_VERSION_SSLv2))
    SSL_CTX_set_options(tls->ctx, SSL_OP_NO_SSLv2);
  if (!(ti->version & TPTLS_VERSION_SSLv3))
    SSL_CTX_set_options(tls->ctx, SSL_OP_NO_SSLv3);
  if (!(ti->version & TPTLS_VERSION_TLSv1))
    SSL_CTX_set_options(tls->ctx, SSL_OP_NO_TLSv1);
  if (!(ti->version & TPTLS_VERSION_TLSv1_1))
    SSL_CTX_set_options(tls->ctx, SSL_OP_NO_TLSv1_1);
  if (!(ti->version & TPTLS_VERSION_TLSv1_2))
    SSL_CTX_set_options(tls->ctx, SSL_OP_NO_TLSv1_2);
  SSL_CTX_sess_set_remove_cb(tls->ctx, NULL);
  SSL_CTX_set_timeout(tls->ctx, ti->timeout);
  /* CRIME (CVE-2012-4929) mitigation */
  SSL_CTX_set_options(tls->ctx, SSL_OP_NO_COMPRESSION);

  /* Set callback if we have a passphrase */
  if (ti->passphrase != NULL) {
    SSL_CTX_set_default_passwd_cb(tls->ctx, passwd_cb);
    SSL_CTX_set_default_passwd_cb_userdata(tls->ctx, (void *)ti);
  }

  if (!SSL_CTX_use_certificate_file(tls->ctx,
				    ti->cert,
				    SSL_FILETYPE_PEM)) {
    if (ti->configured > 0) {
      SU_DEBUG_1(("%s: invalid local certificate: %s\n",
		 "tls_init_context", ti->cert));
      tls_log_errors(3, "tls_init_context", 0);
#if require_client_certificate
      errno = EIO;
      return -1;
#endif
    }
  }

  if (!SSL_CTX_use_PrivateKey_file(tls->ctx,
                                   ti->key,
                                   SSL_FILETYPE_PEM)) {
    if (ti->configured > 0) {
      SU_DEBUG_1(("%s: invalid private key: %s\n",
		 "tls_init_context", ti->key));
      tls_log_errors(3, "tls_init_context(key)", 0);
#if require_client_certificate
      errno = EIO;
      return -1;
#endif
    }
  }

  if (!SSL_CTX_check_private_key(tls->ctx)) {
    if (ti->configured > 0) {
      SU_DEBUG_1(("%s: private key does not match the certificate public key\n",
		  "tls_init_context"));
    }
#if require_client_certificate
    errno = EIO;
    return -1;
#endif
#ifndef OPENSSL_NO_DH
  } else {
    BIO *bio = BIO_new_file(ti->key, "r");
    if (bio != NULL) {
      DH *dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL);
      if (dh != NULL) {
        if (!SSL_CTX_set_tmp_dh(tls->ctx, dh)) {
          SU_DEBUG_1(("%s: invalid DH parameters (PFS) because %s: %s\n",
                      "tls_init_context",
                      ERR_reason_error_string(ERR_get_error()),
                      ti->key));
        } else {
          long options = SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_SINGLE_DH_USE;
          options = SSL_CTX_set_options(tls->ctx, options);
          SU_DEBUG_3(("%s\n", "tls: initialized DHE"));
        }
        DH_free(dh);
      }
      BIO_free(bio);
    }
#endif
  }

  if (!SSL_CTX_load_verify_locations(tls->ctx,
                                     ti->CAfile,
                                     ti->CApath)) {
    SU_DEBUG_1(("%s: error loading CA list: %s\n",
		 "tls_init_context", ti->CAfile));
    if (ti->configured > 0)
      tls_log_errors(3, "tls_init_context(CA)", 0);
    errno = EIO;
    return -1;
  }

  /* corresponds to (enum tport_tls_verify_policy) */
  tls->verify_incoming = (ti->policy & 0x1) ? 1 : 0;
  tls->verify_outgoing = (ti->policy & 0x2) ? 1 : 0;
  tls->verify_subj_in  = (ti->policy & 0x4) ? tls->verify_incoming : 0;
  tls->verify_subj_out = (ti->policy & 0x8) ? tls->verify_outgoing : 0;
  tls->verify_date     = (ti->verify_date)  ? 1 : 0;

  if (tls->verify_incoming)
    verify = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
  else
    verify = SSL_VERIFY_NONE;

  SSL_CTX_set_verify_depth(tls->ctx, ti->verify_depth);
  SSL_CTX_set_verify(tls->ctx, verify, tls_verify_cb);
#ifndef OPENSSL_NO_EC
  if (tls_init_ecdh_curve(tls) == 0) {
    SU_DEBUG_3(("%s\n", "tls: initialized ECDH"));
  } else {
    SU_DEBUG_3(("%s\n", "tls: failed to initialize ECDH"));
  }
#endif
  if (!SSL_CTX_set_cipher_list(tls->ctx, ti->ciphers)) {
    SU_DEBUG_1(("%s: error setting cipher list\n", "tls_init_context"));
    tls_log_errors(3, "tls_init_context", 0);
    errno = EIO;
    return -1;
  }

  return 0;
}

void tls_free(tls_t *tls)
{
  if (!tls)
    return;

  if (tls->con != NULL) {
	SSL_shutdown(tls->con);
	SSL_free(tls->con), tls->con = NULL;
  }

  if (tls->ctx != NULL && tls->type != tls_slave) {
    SSL_CTX_free(tls->ctx);
  }

  su_home_unref(tls->home);
}

int tls_get_socket(tls_t *tls)
{
  int sock = -1;

  if (tls != NULL && tls->bio_con != NULL)
    BIO_get_fd(tls->bio_con, &sock);

  return sock;
}

tls_t *tls_init_master(tls_issues_t *ti)
{
  /* Default id in case RAND fails */
  unsigned char sessionId[32] = "sofia/tls";
  tls_t *tls;

#if HAVE_SIGPIPE
  signal(SIGPIPE, SIG_IGN);  /* Ignore spurios SIGPIPE from OpenSSL */
#endif

  tls_set_default(ti);

  if (!(tls = tls_create(tls_master)))
    return NULL;

  if (tls_init_context(tls, ti) < 0) {
    int err = errno;
    tls_free(tls);
    errno = err;
    return NULL;
  }

  RAND_pseudo_bytes(sessionId, sizeof(sessionId));

  SSL_CTX_set_session_id_context(tls->ctx,
                                 (void*) sessionId,
				 sizeof(sessionId));

  if (ti->CAfile != NULL)
    SSL_CTX_set_client_CA_list(tls->ctx,
                               SSL_load_client_CA_file(ti->CAfile));

#if 0
  if (sock != -1) {
    tls->bio_con = BIO_new_socket(sock, BIO_NOCLOSE);

    if (tls->bio_con == NULL) {
      tls_log_errors(1, "tls_init_master", 0);
      tls_free(tls);
      errno = EIO;
      return NULL;
    }
  }
#endif

  return tls;
}

tls_t *tls_init_secondary(tls_t *master, int sock, int accept)
{
  tls_t *tls = tls_create(tls_slave);

  if (tls) {
    tls->ctx = master->ctx;
    tls->accept = accept ? 1 : 0;
    tls->verify_outgoing = master->verify_outgoing;
    tls->verify_incoming = master->verify_incoming;
    tls->verify_subj_out = master->verify_subj_out;
    tls->verify_subj_in  = master->verify_subj_in;
    tls->verify_date     = master->verify_date;
    tls->x509_verified   = master->x509_verified;

    if (!(tls->read_buffer = su_alloc(tls->home, tls_buffer_size)))
      su_home_unref(tls->home), tls = NULL;
  }
  if (!tls)
    return tls;

  assert(sock != -1);

  tls->bio_con = BIO_new_socket(sock, BIO_NOCLOSE);
  tls->con = SSL_new(tls->ctx);

  if (tls->con == NULL) {
    tls_log_errors(1, "tls_init_secondary", 0);
    tls_free(tls);
    errno = EIO;
    return NULL;
  }

  SSL_set_bio(tls->con, tls->bio_con, tls->bio_con);
  SSL_set_mode(tls->con, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
  SSL_set_ex_data(tls->con, tls_ex_data_idx, tls);

  su_setblocking(sock, 0);

  return tls;
}

su_inline
int tls_post_connection_check(tport_t *self, tls_t *tls)
{
  X509 *cert;
  const SSL_CIPHER *cipher;
  char cipher_description[256];
  int cipher_bits, alg_bits;
  int extcount;
  int i, j, error;

  if (!tls) return -1;

  if (!(cipher = SSL_get_current_cipher(tls->con))) {
    SU_DEBUG_7(("%s(%p): %s\n", __func__, (void*)self,
                "OpenSSL failed to return an SSL_CIPHER object to us."));
    return SSL_ERROR_SSL;
  }
  SU_DEBUG_9(("%s(%p): TLS cipher chosen (name): %s\n", __func__, (void*)self,
              SSL_CIPHER_get_name(cipher)));
  SU_DEBUG_9(("%s(%p): TLS cipher chosen (version): %s\n", __func__, (void*)self,
              SSL_CIPHER_get_version(cipher)));
  cipher_bits = SSL_CIPHER_get_bits(cipher, &alg_bits);
  SU_DEBUG_9(("%s(%p): TLS cipher chosen (bits/alg_bits): %d/%d\n", __func__, (void*)self,
              cipher_bits, alg_bits));
  SSL_CIPHER_description(cipher, cipher_description, sizeof(cipher_description));
  SU_DEBUG_9(("%s(%p): TLS cipher chosen (description): %s\n", __func__, (void*)self,
              cipher_description));

  cert = SSL_get_peer_certificate(tls->con);
  if (!cert) {
    SU_DEBUG_7(("%s(%p): Peer did not provide X.509 Certificate.\n", 
				__func__, (void *) self));
    if (self->tp_accepted && tls->verify_incoming)
      return X509_V_ERR_CERT_UNTRUSTED;
    else if (!self->tp_accepted && tls->verify_outgoing)
      return X509_V_ERR_CERT_UNTRUSTED;
    else 
      return X509_V_OK;
  }

  tls->subjects = su_strlst_create(tls->home);
  if (!tls->subjects)
    return X509_V_ERR_OUT_OF_MEM;

  extcount = X509_get_ext_count(cert);

  /* Find matching subjectAltName.DNS */
  for (i = 0; i < extcount; i++) {
    X509_EXTENSION *ext;
    char const *name;
#if OPENSSL_VERSION_NUMBER >  0x10000000L
    const X509V3_EXT_METHOD *vp;
#else
    X509V3_EXT_METHOD *vp;
#endif
    STACK_OF(CONF_VALUE) *values;
    CONF_VALUE *value;
    void *d2i;

    ext = X509_get_ext(cert, i);
    name = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));

    if (strcmp(name, "subjectAltName") != 0)
      continue;

    vp = X509V3_EXT_get(ext); if (!vp) continue;
    d2i = X509V3_EXT_d2i(ext);
    values = vp->i2v(vp, d2i, NULL);

    for (j = 0; j < sk_CONF_VALUE_num(values); j++) {
      value = sk_CONF_VALUE_value(values, j);
      if (strcmp(value->name, "DNS") == 0)
        su_strlst_dup_append(tls->subjects, value->value);
      if (strcmp(value->name, "IP") == 0)
        su_strlst_dup_append(tls->subjects, value->value);
      else if (strcmp(value->name, "URI") == 0)
        su_strlst_dup_append(tls->subjects, value->value);
    }
  }

  {
    X509_NAME *subject;
    char name[256];

    subject = X509_get_subject_name(cert);

    if (subject) {
      if (X509_NAME_get_text_by_NID(subject, NID_commonName,
				    name, sizeof name) > 0) {
	usize_t k, N = su_strlst_len(tls->subjects);
	name[(sizeof name) - 1] = '\0';

	for (k = 0; k < N; k++)
	  if (su_casematch(su_strlst_item(tls->subjects, k), name) == 0)
	    break;

	if (k >= N)
	  su_strlst_dup_append(tls->subjects, name);
      }
    }
  }

  X509_free(cert);

  error = SSL_get_verify_result(tls->con);

  if (cert && error == X509_V_OK)
    tls->x509_verified = 1;

  if (tport_log->log_level >= 7) {
    int i, len = su_strlst_len(tls->subjects);
    for (i=0; i < len; i++)
      SU_DEBUG_7(("%s(%p): Peer Certificate Subject %i: %s\n", \
				  __func__, (void *)self, i, su_strlst_item(tls->subjects, i)));
    if (i == 0)
      SU_DEBUG_7(("%s(%p): Peer Certificate provided no usable subjects.\n",
				  __func__, (void *)self));
  }

  /* Verify incoming connections */
  if (self->tp_accepted) {
    if (!tls->verify_incoming)
      return X509_V_OK;

    if (!tls->x509_verified)
      return error;

    if (tls->verify_subj_in) {
      su_strlst_t const *subjects = self->tp_pri->pri_primary->tp_subjects;
      int i, items;

      items = subjects ? su_strlst_len(subjects) : 0;
      if (items == 0)
        return X509_V_OK;

      for (i=0; i < items; i++) {
	if (tport_subject_search(su_strlst_item(subjects, i), tls->subjects))
	  return X509_V_OK;
      }
      SU_DEBUG_3(("%s(%p): Peer Subject Mismatch (incoming connection)\n", \
				  __func__, (void *)self));

      return X509_V_ERR_CERT_UNTRUSTED;
    }
  }
  /* Verify outgoing connections */
  else {
    char const *subject = self->tp_canon;
    if (!tls->verify_outgoing)
      return X509_V_OK;

    if (!tls->x509_verified || !subject)
      return error;

    if (tls->verify_subj_out) {
      if (tport_subject_search(subject, tls->subjects))
        return X509_V_OK; /* Subject match found in verified certificate chain */
      SU_DEBUG_3(("%s(%p): Peer Subject Mismatch (%s)\n", \
				  __func__, (void *)self, subject));

      return X509_V_ERR_CERT_UNTRUSTED;
    }
  }

  return error;
}

static
int tls_error(tls_t *tls, int ret, char const *who,
	      void *buf, int size)
{
  int events = 0;
  int err = SSL_get_error(tls->con, ret);

  switch (err) {
  case SSL_ERROR_WANT_WRITE:
    events = SU_WAIT_OUT;
    break;

  case SSL_ERROR_WANT_READ:
    events = SU_WAIT_IN;
    break;

  case SSL_ERROR_ZERO_RETURN:
    return 0;

  case SSL_ERROR_SYSCALL:
    if (SSL_get_shutdown(tls->con) & SSL_RECEIVED_SHUTDOWN)
      return 0;			/* EOS */
    if (errno == 0)
      return 0;			/* EOS */
    return -1;

  default:
    tls_log_errors(1, who, err);
    errno = EIO;
    return -1;
  }

  if (buf) {
    tls->write_events = events;
    tls->write_buffer = buf, tls->write_buffer_len = size;
  }
  else {
    tls->read_events = events;
  }

  errno = EAGAIN;
  return -1;
}

ssize_t tls_read(tls_t *tls)
{
  ssize_t ret;

  if (tls == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (0)
    SU_DEBUG_1(("tls_read(%p) called on %s (events %u)\n", (void *)tls,
	    tls->type ? "master" : "slave",
	    tls->read_events));

  if (tls->read_buffer_len)
    return (ssize_t)tls->read_buffer_len;

  tls->read_events = SU_WAIT_IN;

  ret = SSL_read(tls->con, tls->read_buffer, tls_buffer_size);
  if (ret <= 0)
    return tls_error(tls, ret, "tls_read: SSL_read", NULL, 0);

  return (ssize_t)(tls->read_buffer_len = ret);
}

void *tls_read_buffer(tls_t *tls, size_t N)
{
  assert(N == tls->read_buffer_len);
  tls->read_buffer_len = 0;
  return tls->read_buffer;
}

int tls_pending(tls_t const *tls)
{
  return tls && tls->con && SSL_pending(tls->con);
}

/** Check if data is available in TCP connection.
 *
 *
 *
 * @retval -1 upon an error
 * @retval 0 end-of-stream
 * @retval 1 nothing to read
 * @retval 2 there is data to read
 */
int tls_want_read(tls_t *tls, int events)
{
  if (tls && (events & tls->read_events)) {
    int ret = tls_read(tls);
    if (ret > 0)
      return 2;
    else if (ret == 0)
      return 0;
    else if (errno == EAGAIN)
      return 3;			/* ??? */
    else
      return -1;
  }

  return 1;
}

ssize_t tls_write(tls_t *tls, void *buf, size_t size)
{
  ssize_t ret;

  if (0)
    SU_DEBUG_1(("tls_write(%p, %p, "MOD_ZU") called on %s\n",
	    (void *)tls, buf, size,
	    tls && tls->type == tls_slave ? "master" : "slave"));

  if (tls == NULL || buf == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (tls->write_buffer) {
    assert(buf == tls->write_buffer);
    assert(size >= tls->write_buffer_len);
    assert(tls->write_events == 0);

    if (tls->write_events ||
	buf != tls->write_buffer ||
	size < tls->write_buffer_len) {
      errno = EIO;
      return -1;
    }

    ret = tls->write_buffer_len;

    tls->write_buffer = NULL;
    tls->write_buffer_len = 0;

    return ret;
  }

  if (size == 0)
    return 0;

  tls->write_events = 0;

  ret = SSL_write(tls->con, buf, size);
  if (ret < 0)
    return tls_error(tls, ret, "tls_write: SSL_write", buf, size);

  return ret;
}

int tls_want_write(tls_t *tls, int events)
{
  if (tls && (events & tls->write_events)) {
    int ret;
    void *buf = tls->write_buffer;
    size_t size = tls->write_buffer_len;

    tls->write_events = 0;

    /* remove buf */
    tls->write_buffer = NULL;
    tls->write_buffer_len = 0;

    ret = tls_write(tls, buf, size);

    if (ret >= 0)
      /* Restore buf */
      return tls->write_buffer = buf, tls->write_buffer_len = ret;
    else if (errno == EAGAIN)
      return 0;
    else
      return -1;
  }
  return 0;
}

int tls_events(tls_t const *tls, int mask)
{

  if (!tls)
    return mask;

  if (tls->type == tls_master)
    return mask;

  return
    (mask & ~(SU_WAIT_IN|SU_WAIT_OUT)) |
    ((mask & SU_WAIT_IN) ? tls->read_events : 0) |
    ((mask & SU_WAIT_OUT) ? tls->write_events : 0);
}

int tls_connect(su_root_magic_t *magic, su_wait_t *w, tport_t *self)
{
  tport_master_t *mr = self->tp_master;
  tport_tls_t *tlstp = (tport_tls_t *)self;
  tls_t *tls;
  int events = su_wait_events(w, self->tp_socket);
  int error;

  SU_DEBUG_7(("%s(%p): events%s%s%s%s\n", __func__, (void *)self,
              events & (SU_WAIT_CONNECT) ? " CONNECTING" : "",
              events & SU_WAIT_IN  ? " NEGOTIATING" : "",
              events & SU_WAIT_ERR ? " ERROR" : "",
              events & SU_WAIT_HUP ? " HANGUP" : ""));

#if HAVE_POLL
  assert(w->fd == self->tp_socket);
#endif

  if (events & SU_WAIT_ERR)
    tport_error_event(self);

  if (events & SU_WAIT_HUP && !self->tp_closed)
    tport_hup_event(self);

  if (self->tp_closed)
    return 0;

  error = su_soerror(self->tp_socket);
  if (error) {
    tport_error_report(self, error, NULL);
    return 0;
  }

  if ((tls = tlstp->tlstp_context) == NULL) {
    SU_DEBUG_3(("%s(%p): Error: no TLS context data for connected socket.\n",
                __func__, (void *)tlstp));
    tport_close(self);
    tport_set_secondary_timer(self);
    return 0;
  }

  if (self->tp_is_connected == 0) {
    int ret, status;

    ret = self->tp_accepted ? SSL_accept(tls->con) : SSL_connect(tls->con);
    status = SSL_get_error(tls->con, ret);

    switch (status) {
      case SSL_ERROR_WANT_READ:
        /* OpenSSL is waiting for the peer to send handshake data */
        self->tp_events = SU_WAIT_IN | SU_WAIT_ERR | SU_WAIT_HUP;
        su_root_eventmask(mr->mr_root, self->tp_index,
		          self->tp_socket, self->tp_events);
        return 0;

      case SSL_ERROR_WANT_WRITE:
        /* OpenSSL is waiting for the peer to receive handshake data */
        self->tp_events = SU_WAIT_IN | SU_WAIT_ERR | SU_WAIT_HUP | SU_WAIT_OUT;
        su_root_eventmask(mr->mr_root, self->tp_index,
                           self->tp_socket, self->tp_events);
        return 0;

      case SSL_ERROR_NONE:
        /* TLS Handshake complete */
	status = tls_post_connection_check(self, tls);
        if ( status == X509_V_OK ) {
          su_wait_t wait[1] = {SU_WAIT_INIT};
          tport_master_t *mr = self->tp_master;

          su_root_deregister(mr->mr_root, self->tp_index);
          self->tp_index = -1;
          self->tp_events = SU_WAIT_IN | SU_WAIT_ERR | SU_WAIT_HUP;

          if ((su_wait_create(wait, self->tp_socket, self->tp_events) == -1) ||
             ((self->tp_index = su_root_register(mr->mr_root, wait, tport_wakeup,
                                                           self, 0)) == -1)) {
            tport_close(self);
            tport_set_secondary_timer(self);
	    return 0;
          }

          tls->read_events = SU_WAIT_IN;
          tls->write_events = 0;
          self->tp_is_connected = 1;
	  self->tp_verified = tls->x509_verified;
	  self->tp_subjects = tls->subjects;

	  if (tport_has_queued(self))
            tport_send_event(self);
          else
            tport_set_secondary_timer(self);

          return 0;
	}
	break;

      default:
        {
	  char errbuf[64];
	  ERR_error_string_n(status, errbuf, 64);
          SU_DEBUG_3(("%s(%p): TLS setup failed (%s)\n",
					  __func__, (void *)self, errbuf));
        }
        break;
    }
  }

  /* TLS Handshake Failed or Peer Certificate did not Verify */
  tport_close(self);
  tport_set_secondary_timer(self);

  return 0;
}
