#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

/* SSLeay stuff */
#include <openssl/rsa.h>       
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#include "cse.h"
#include "proxy.h"

static int ssl_open(connection_t *conn, int fd);
static int ssl_read(connection_t *conn, char *buf, int len);
static int ssl_write(connection_t *conn, char *buf, int len);
static int ssl_close(connection_t *conn);
static void ssl_free(connection_t *conn);

struct connection_ops_t ssl_ops = {
  ssl_open,
  ssl_read,
  ssl_write,
  ssl_close,
  ssl_free,
};


static int
ssl_open(connection_t *conn, int fd)
{
  int result;
  SSL_CTX *ctx = conn->context;
  SSL *ssl;
  SSL_CIPHER *cipher;
  int algbits;

  ssl = conn->sock;
  if (! ssl) {
    pthread_mutex_lock(conn->ssl_lock);
    ssl = SSL_new(ctx);
    pthread_mutex_unlock(conn->ssl_lock);
    conn->sock = ssl;
  }

  if (! ssl) {
    close(fd);
    LOG(("can't allocate ssl\n"));
    return -1;
  }
  
  SSL_set_fd(ssl, fd);
  result = SSL_accept(ssl);

  /* fprintf(stderr, "ssl open fd:%d result:%d\n", fd, result); */

  if (result < 0) {
    close(fd);
    ERR(("can't accept ssl\n"));
    return -1;
  }
  
  conn->fd = fd;

  cipher = SSL_get_current_cipher(ssl);

  if (cipher) {
    conn->ssl_cipher = SSL_CIPHER_get_name(cipher);
    conn->ssl_bits = SSL_CIPHER_get_bits(cipher, &algbits);
  }

  return 0;
}

static int
ssl_read(connection_t *conn, char *buf, int len)
{
  SSL *ssl = conn->sock;
  int result;

  if (! ssl || conn->fd < 0)
    return -1;

  result = SSL_read(ssl, buf, len);

  return result;
}

static int
ssl_write(connection_t *conn, char *buf, int len)
{
  SSL *ssl = conn->sock;

  if (! ssl || conn->fd < 0)
    return -1;
  
  return SSL_write(ssl, buf, len);
}

static int
ssl_close(connection_t *conn)
{
  int fd = conn->fd;
  SSL *ssl = conn->sock;
  conn->sock = 0;
  
  conn->fd = -1;

  if (fd > 0 && ssl)
    SSL_shutdown(ssl);
  
  if (fd > 0)
    close(fd);

  if (ssl) {
    pthread_mutex_lock(conn->ssl_lock);
    SSL_free(ssl);
    pthread_mutex_unlock(conn->ssl_lock);
  }

  return 0;
}

static void
ssl_free(connection_t *conn)
{
  SSL *ssl = conn->sock;
  conn->sock = 0;

  if (ssl) {
    pthread_mutex_lock(conn->ssl_lock);
    SSL_free(ssl);
    pthread_mutex_unlock(conn->ssl_lock);
  }
}

static int
password_callback(char *buf, int size, int rwflag, void *userdata)
{
  sprintf(buf, userdata);

  return strlen(buf);
}

void *
ssl_create(registry_t *node)
{
  SSL_CTX *ctx;
  SSL_METHOD *meth;
  char *certificate_pem;
  char *key_pem;
  char *password;

  SSL_load_error_strings();
  SSLeay_add_ssl_algorithms();
  meth = SSLv23_server_method();
  ctx = SSL_CTX_new(meth);
  
  if (! ctx) {
    /* ERR_print_errors_fp(stderr); */
    printf("can't allocate context\n");
    exit(1);
  }

  certificate_pem = cse_find_value(node->first, "certificate-pem");
  key_pem = cse_find_value(node->first, "key-pem");

  if (! key_pem)
    key_pem = certificate_pem;
  if (! certificate_pem)
    certificate_pem = key_pem;

  if (! key_pem) {
    fprintf(stderr, "Can't find certificate-pem in SSL configuration\n");
    exit(1);
  }

  password = cse_find_value(node->first, "key-store-password");
  if (! password) {
    fprintf(stderr, "Can't find key-store-password in SSL configuration\n");
    exit(1);
  }

  SSL_CTX_set_default_passwd_cb(ctx, password_callback);
  SSL_CTX_set_default_passwd_cb_userdata(ctx, password);
  
  if (SSL_CTX_use_certificate_file(ctx, certificate_pem,
                                   SSL_FILETYPE_PEM) <= 0) {
    ERR_print_errors_fp(stderr);
    printf("Can't open certificate file %s\n", certificate_pem);
    exit(1);
  }
  
  if (SSL_CTX_use_PrivateKey_file(ctx, key_pem, SSL_FILETYPE_PEM) <= 0) {
    ERR_print_errors_fp(stderr);
    exit(1);
  }

  if (! SSL_CTX_check_private_key(ctx)) {
    fprintf(stderr, "Private key does not match the certificate public key\n");
    exit(1);
  }

  return ctx;
}

connection_ops_t *
ssl_get_ops()
{
  return &ssl_ops;
}
