/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2006 by the KFTPGrabber developers
 * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  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 Steet, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * 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 "ssl.h"

#include <ksocketdevice.h>
#include <kmdcodec.h>
#include <ksslx509v3.h>

#include <openssl/ssl.h>
#include <openssl/x509.h>

#include <unistd.h>

namespace KFTPEngine {

class Ssl::Private {
public:
    Private()
      : ssl(0), sslCtx(0), bio(0)
    {
    }
    
    bool initialized;
    
    SSL *ssl;
    SSL_CTX *sslCtx;
    BIO *bio;
    X509 *certificate;
};

Ssl::Ssl(KNetwork::KStreamSocket *socket)
  : d(new Ssl::Private()),
    m_socket(socket)
{
  d->ssl = 0;
  d->sslCtx = 0;
  d->bio = 0;
  d->certificate = 0;
  d->initialized = false;
  
  initialize();
}

Ssl::~Ssl()
{
  close();
  delete d;
}

void Ssl::initialize()
{
  if (!d->ssl) {
    SSL_library_init();
    
    d->sslCtx = SSL_CTX_new(SSLv23_client_method());
    d->ssl = SSL_new(d->sslCtx);
    
    SSL_CTX_set_options(d->sslCtx, SSL_OP_ALL);
    
    // Initialize the socket BIO
    d->bio = BIO_new_socket(m_socket->socketDevice()->socket(), BIO_NOCLOSE);
    SSL_set_bio(d->ssl, d->bio, d->bio);
  }
  
  d->initialized = true;
}

bool Ssl::connect()
{
  if (!d->initialized)
    return false;
  
retry_connect:
  int ret = SSL_connect(d->ssl);
  if (ret == 1) {
    // Connection established
    setConnectionInfo();
    return true;
  } else {
    int err = SSL_get_error(d->ssl, ret);
    
    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
retry_poll:
      bool input;
      m_socket->socketDevice()->poll(&input, 0, 0, 0);
      
      if (input)
        goto retry_connect;
      else {
        ::usleep(20000);
        goto retry_poll;
      }
    } else {
      return false;
    }
  }
  
  return true;
}

bool Ssl::setClientCertificate(KSSLPKCS12 *pkcs)
{
  if (!pkcs || !pkcs->getCertificate())
    return false;
  
  int ret;
  X509 *x;
  EVP_PKEY *k = pkcs->getPrivateKey();
  QCString cert = QCString(pkcs->getCertificate()->toString().ascii());
  
  QByteArray qba, qbb = cert.copy();
  KCodecs::base64Decode(qbb, qba);
#if OPENSSL_VERSION_NUMBER > 0x009070afL
  const unsigned char *qbap = reinterpret_cast<unsigned char *>(qba.data());
#else
  unsigned char *qbap = reinterpret_cast<unsigned char *>(qba.data());
#endif
  x = d2i_X509(NULL, &qbap, qba.size());
  
  if (!x || !k)
    return false;
  
  if (!pkcs->getCertificate()->x509V3Extensions().certTypeSSLClient())
    return false;
  
  ret = SSL_CTX_use_certificate(d->sslCtx, x);
  if (ret <= 0)
    return false;
  
  ret = SSL_CTX_use_PrivateKey(d->sslCtx, k);
  if (ret <= 0)
    return false;
  
  return true;
}

void Ssl::setConnectionInfo()
{
  SSL_CIPHER *cipher;
  char buffer[1024];
  
  buffer[0] = 0;
  cipher = SSL_get_current_cipher(d->ssl);
  
  if (!cipher)
    return;
  
  m_connectionInfo.m_cipherUsedBits = SSL_CIPHER_get_bits(cipher, &(m_connectionInfo.m_cipherBits));
  m_connectionInfo.m_cipherVersion = SSL_CIPHER_get_version(cipher);
  m_connectionInfo.m_cipherName = SSL_CIPHER_get_name(cipher);
  m_connectionInfo.m_cipherDescription = SSL_CIPHER_description(cipher, buffer, 1023);
}

SslConnectionInfo &Ssl::connectionInfo()
{
  return m_connectionInfo;
}

void Ssl::close()
{
  if (!d->initialized)
    return;
  
  if (d->certificate) {
    X509_free(d->certificate);
    d->certificate = 0;
  }
  
  if (d->ssl) {
    SSL_shutdown(d->ssl);
    SSL_free(d->ssl);
    SSL_CTX_free(d->sslCtx);
    
    d->ssl = 0;
    d->sslCtx = 0;
    d->bio = 0;
  }
}

int Ssl::read(void *buffer, int bytes)
{
  if (!d->initialized)
    return -1;

  int ret = SSL_read(d->ssl, buffer, bytes);
  
  if (ret <= 0) {
    int err = SSL_get_error(d->ssl, ret);
    
    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
      return 0;
    else
      return -1;
  }
  
  return ret;
}
    
int Ssl::write(void *buffer, int bytes)
{
  if (!d->initialized)
    return -1;
  
retry_write:
  int ret = SSL_write(d->ssl, buffer, bytes);
  
  if (ret <= 0) {
    int err = SSL_get_error(d->ssl, ret);
    
    if (err == SSL_ERROR_WANT_READ) {
retry_poll:
      bool input;
      m_socket->socketDevice()->poll(&input, 0, 0, 0);
      
      if (input)
        goto retry_write;
      else {
        ::usleep(20000);
        goto retry_poll;
      }
    } else if (err == SSL_ERROR_WANT_WRITE) {
      return -1;
    } else {
      return -1;
    }
  }
  
  return ret;
}

}
