/*
 * cyassl.c
 *
 * Cyassl <http://www.cyassl.org> support for curl.  Cyassl is small
 * footprint SSL stack, primarily meant for embedded systems and such.
 *
 * Almost everything is adopted from cyassl/examples/client/client.c
 * and other existing SSL implementations within curl.
 */

#include "setup.h"

#ifdef USE_CYASSL

/*
 * cyassl abuse alert: always keep NON_BLOCKING 1 -- everything else
 * (NON_BLOCKING undefined and CYASSL_CALLBACKS) is untested.
 */
#define NON_BLOCKING 1

#define CURL_CYASSL_DEBUG 1

#include <unistd.h>
#include <fcntl.h>
#include <openssl/ssl.h>
#include "urldata.h"
#include "sendf.h"
#include "cyassl.h"

static inline int tcp_set_nonblocking(int* sockfd)
{
  int flags = 0;

  flags = fcntl(*sockfd, F_GETFL, 0);
  return fcntl(*sockfd, F_SETFL, flags | O_NONBLOCK);
}

static int NonBlockingSSL_Connect(SSL* ssl)
{
  int ret, error;

  ret = SSL_connect(ssl);

  error = SSL_get_error(ssl, 0);

  while (ret != SSL_SUCCESS && (error == SSL_ERROR_WANT_READ ||
                                error == SSL_ERROR_WANT_WRITE)) {

#if (CURL_CYASSL_DEBUG == 1)
    if (error == SSL_ERROR_WANT_READ)
      printf("... client would read block\n");
    else
      printf("... client would write block\n");
#endif

    /* this ought to be good enough for more data to show up?. */
    usleep(300);

    ret = SSL_connect(ssl);

    error = SSL_get_error(ssl, 0);
  }

  if (ret != SSL_SUCCESS)
    return -1;

  return ret;
}

int Curl_cyassl_init(void)
{
#if (CURL_CYASSL_DEBUG == 1)
  CyaSSL_Debugging_ON();
#endif

  return 1;
}

int Curl_cyassl_cleanup(void)
{
#if (CURL_CYASSL_DEBUG == 1)
  printf(stderr, "Curl_cyassl_cleanup()\n");
#endif

  return 1;
}

static int do_file_type(const char *type)
{
  if(!type || !type[0])
    return SSL_FILETYPE_PEM;
  if(curl_strequal(type, "PEM"))
    return SSL_FILETYPE_PEM;
  if(curl_strequal(type, "DER"))
    return SSL_FILETYPE_ASN1;
  return -1;
}

/*
 * Mostly copied from ssluse.c -- what's unimplemented in cyassl has
 * simply been removed.
 */
static int cert_stuff(struct connectdata *conn,
                      SSL_CTX* ctx,
                      char *cert_file,
                      const char *cert_type,
                      char *key_file,
                      const char *key_type)
{
  struct SessionHandle *data = conn->data;
  int file_type;

  if(cert_file) {

    if(data->set.str[STRING_KEY_PASSWD]) {
      /*
       * SSL_CTX_set_default_passwd_cb_userdata() and
       * SSL_CTX_set_default_passwd_cb(ctx, passwd_callback) are not
       * available yet.  We'll just fail.
       */
      failf(conn->data, "cyassl can't handle key passwords yet.\n");
      return 0;
    }

    file_type = do_file_type(cert_type);

    switch(file_type) {
    case SSL_FILETYPE_PEM:
      if(SSL_CTX_use_certificate_chain_file(ctx,
                                            cert_file) != SSL_SUCCESS) {
        failf(data, "unable to use client certificate\n");
        return 0;
      }
      break;
    case SSL_FILETYPE_ASN1:
      /* SSL_CTX_use_certificate_file() works with either PEM or ASN1, but
         we use the case above for PEM so this can only be performed with
         ASN1 files. */
      if(SSL_CTX_use_certificate_file(ctx,
                                      cert_file,
                                      file_type) != SSL_SUCCESS) {
        failf(data, "unable to use client certificate\n");
        return 0;
      }
      break;

    default:
      failf(data, "Unknown/unimplemented certificate type (%s)\n", cert_type);
      return 0;
    }

    /*
     * X509_get_pubkey(), EVP_PKEY_copy_parameters(), EVP_PKEY_free(),
     * SSL_CTX_check_private_key() etc are not implemented in cyassl.
     * Just pretend that we've done something, for now.
     */
  }

  if (key_file) {
    file_type = do_file_type(key_type);

    if (SSL_CTX_use_PrivateKey_file(ctx,
                                    key_file,
                                    file_type) != SSL_SUCCESS) {
      failf(data, "can't load client key file");
      return 0;
    }
  }

  /*
   * Apparently there's no way to deal with STRING_KEY_PASSWD.  In any
   * case I could not find any.
   */

  return 1;
}

CURLcode Curl_cyassl_connect(struct connectdata *conn, int sockindex)
{
  curl_socket_t       sockfd = 0;
  struct SessionHandle* data = 0;
  SSL_METHOD*         method = 0;

#if (CURL_CYASSL_DEBUG == 1)
  infof(conn->data, "Curl_cyassl_connect()\n");
#endif

  sockfd = conn->sock[sockindex];
  data   = conn->data;

#ifndef NO_TLS
  infof(data, "TLSv1_client_method\n");
  method  = TLSv1_client_method();
#else
  infof(data, "SSLv3_client_method\n");
  method  = SSLv3_client_method();
#endif

  conn->ssl[sockindex].ctx = SSL_CTX_new(method);

  /* SSL_get_certificate() not available in cyassl. */

  if(data->set.str[STRING_CERT]) {
    if(!cert_stuff(conn,
                   conn->ssl[sockindex].ctx,
                   data->set.str[STRING_CERT],
                   data->set.str[STRING_CERT_TYPE],
                   data->set.str[STRING_KEY],
                   data->set.str[STRING_KEY_TYPE])) {
      return CURLE_SSL_CERTPROBLEM;
    }
  }

  if(data->set.str[STRING_SSL_CIPHER_LIST]) {
    if(!SSL_CTX_set_cipher_list(conn->ssl[sockindex].ctx,
                                data->set.str[STRING_SSL_CIPHER_LIST])) {
      failf(data, "failed setting cipher list");
      return CURLE_SSL_CIPHER;
    }
  }

  infof(data, "data->set.ssl.verifypeer=%d\n", data->set.ssl.verifypeer);

  if (data->set.str[STRING_SSL_CAFILE] || data->set.str[STRING_SSL_CAPATH]) {
    if (SSL_CTX_load_verify_locations(
          conn->ssl[sockindex].ctx,
          data->set.str[STRING_SSL_CAFILE],
          data->set.str[STRING_SSL_CAPATH]) != SSL_SUCCESS) {
      if(data->set.ssl.verifypeer) {
        /* Fail if we insist on successfully verifying the server. */
        failf(data,"error setting certificate verify locations:\n"
              "  CAfile: %s\n  CApath: %s\n",
              data->set.str[STRING_SSL_CAFILE]?
              data->set.str[STRING_SSL_CAFILE]: "none",
              data->set.str[STRING_SSL_CAPATH]?
              data->set.str[STRING_SSL_CAPATH] : "none");
        return CURLE_SSL_CACERT_BADFILE;
      }
      else {
        /* Just continue with a warning if no strict certificate
           verification is required. */
        infof(data, "error setting certificate verify locations,"
              " continuing anyway:\n");
      }
    }
    else {
      /* Everything is fine. */
      infof(data, "successfully set certificate verify locations:\n");
    }
    infof(data,
          "  CAfile: %s\n"
          "  CApath: %s\n",
          data->set.str[STRING_SSL_CAFILE] ? data->set.str[STRING_SSL_CAFILE]:
          "none",
          data->set.str[STRING_SSL_CAPATH] ? data->set.str[STRING_SSL_CAPATH]:
          "none");
  }

  /*
   * The last argument is a verify callback, which isn't called per
   * the current implementation anyway.
   *
   * SSL_get_verify_result() isn't implemented either, so I do not
   * have a clear idea how to proceed here.  Just use "-k" switch to
   * cul for now.
   */
  SSL_CTX_set_verify(conn->ssl[sockindex].ctx,
                     data->set.ssl.verifypeer ? SSL_VERIFY_PEER : SSL_VERIFY_NONE,
                     0);

  conn->ssl[sockindex].handle = SSL_new(conn->ssl[sockindex].ctx);

  SSL_set_fd(conn->ssl[sockindex].handle, sockfd);

  tcp_set_nonblocking(&sockfd);

  if (SSL_SUCCESS == NonBlockingSSL_Connect(conn->ssl[sockindex].handle)) {
    infof(data, "SSL connection complete.\n");
    conn->ssl[sockindex].state = ssl_connection_complete;
  } else {
    failf(data, "Connect error (%d)\n", CURLE_SSL_CONNECT_ERROR);
    return CURLE_SSL_CONNECT_ERROR;
  }

  return CURLE_OK;
}

void Curl_cyassl_close_all(struct SessionHandle *data)
{
#if (CURL_CYASSL_DEBUG == 1)
  infof(data, "Curl_cyassl_close_all()\n");
#endif

  /*
   * ERR_remove_state() requires cyassl built with OPENSSL_EXTRA.  It
   * doesn't do anything as of cyassl-0.9.9. Leaving as is, it might
   * serve as a reminder that something ought to be done when these
   * are available.
   */
  /* ERR_remove_state(0); */
  (void)data;
}

/*
 * Straight from Curl_ossl_close().
 */
void Curl_cyassl_close(struct connectdata *conn, int sockindex)
{
  struct ssl_connect_data *connssl = &conn->ssl[sockindex];

#if (CURL_CYASSL_DEBUG == 1)
  infof(conn->data, "Curl_cyassl_close()\n");
#endif

  if(connssl->handle) {
    (void)SSL_shutdown(connssl->handle);

    /*
     * SSL_set_connect_state() is not available in cyassl; but why is
     * it called in Curl_ossl_close() at the time of closing?
     */
    /* SSL_set_connect_state(connssl->handle); */

    SSL_free(connssl->handle);
    connssl->handle = NULL;
  }

  if(connssl->ctx) {
    SSL_CTX_free (connssl->ctx);
    connssl->ctx = NULL;
  }

  close(conn->sock[sockindex]);
}

ssize_t Curl_cyassl_send(struct connectdata *conn, int sockindex,
                         const void *mem, size_t len)
{
  int nwrote = 0;
  char error_buffer[120];
  unsigned long sslerror;

#if (CURL_CYASSL_DEBUG == 1)
  infof(conn->data, "Curl_cyassl_send()\n");
#endif

  nwrote = SSL_write(conn->ssl[sockindex].handle, mem, len);

#if (CURL_CYASSL_DEBUG == 1)
  infof(conn->data, "SSL_write(ssl=%p, mem=%p, len=%d) = %d\n",
         (void *)conn->ssl[sockindex].handle, mem, len, nwrote);
#endif

  if (nwrote < 0) {
    int err = SSL_get_error(conn->ssl[sockindex].handle, nwrote);

    switch (err) {
    case SSL_ERROR_WANT_READ:
    case SSL_ERROR_WANT_WRITE:
      /* call us again */
      return 0;
    default:
      /* In order to be able to use ERR_get_error(), cyassl should be
       * compiled with SSL_EXTRAS.  Currently (as of cyassl 0.9.9) it
       * simply returns 0 anyhow.
       */
      /* sslerror = ERR_get_error(); */
      failf(conn->data, "SSL_write() error: %s (%d)",
            ERR_error_string(sslerror, error_buffer), err);
      return -1;
    }
  }

  return nwrote;
}

ssize_t Curl_cyassl_recv(struct connectdata *conn, int sockindex,
                         char *buf, size_t bufsiz, bool *wouldblock)
{
  int nread = 0;
  char error_buffer[120];
  unsigned long sslerror;

  nread = SSL_read(conn->ssl[sockindex].handle, buf, bufsiz);

#if (CURL_CYASSL_DEBUG == 1)
  infof(conn->data, "Curl_cyassl_recv(), nread=%d\n", nread);
#endif

  if (nread < 0) {
    int err = SSL_get_error(conn->ssl[sockindex].handle, (int)nread);

    switch (err) {
    case SSL_ERROR_WANT_READ:
    case SSL_ERROR_WANT_WRITE:
      /* there's data pending, re-invoke SSL_read() */
      *wouldblock = TRUE;
      return -1; /* basically EWOULDBLOCK */
    default:
      /* See comment about ERR_get_error() in Curl_cyassl_send()
       * above. */
      /* sslerror = ERR_get_error(); */
      failf(conn->data, "SSL_read() error: %s (%d)",
            ERR_error_string(sslerror, error_buffer),
            err);
      /* I'm not sure this is the right thing to do.. */
      return -1;
    }
  }

  return nread;
}

size_t Curl_cyassl_version(char *buffer, size_t size)
{
  /* cyassl doesn't seem to provide a way to query its version
   */
  return snprintf(buffer, size, "cyassl/unknown");
}

int Curl_cyassl_shutdown(struct connectdata *conn, int sockindex)
{
  return 0;
}

void Curl_cyassl_session_free(void *ptr)
{
  /* there's no SSL_SESSION_free in CyaSSL. */
}

#endif /* USE_CYASSL */



