//
// openssl_operation.hpp
// ~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2005 Voipster / Indrek dot Juhani at voipster dot com
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#ifndef BOOST_ASIO_SSL_DETAIL_OPENSSL_OPERATION_HPP
#define BOOST_ASIO_SSL_DETAIL_OPENSSL_OPERATION_HPP

#if defined(_MSC_VER) && (_MSC_VER >= 1200)
# pragma once
#endif // defined(_MSC_VER) && (_MSC_VER >= 1200)

#include <boost/asio/detail/push_options.hpp>

#include <boost/asio/detail/push_options.hpp>
#include <boost/function.hpp>
#include <boost/assert.hpp>
#include <boost/bind.hpp>
#include <boost/asio/detail/pop_options.hpp>

#include <boost/asio/buffer.hpp>
#include <boost/asio/placeholders.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/detail/socket_ops.hpp>
#include <boost/asio/ssl/detail/openssl_types.hpp>

namespace boost {
namespace asio {
namespace ssl {
namespace detail {

typedef boost::function<int (::SSL*)> ssl_primitive_func; 
typedef boost::function<void (const boost::system::error_code&, int)>
  user_handler_func;

// Network send_/recv buffer implementation
//
//
class net_buffer
{
  static const int  NET_BUF_SIZE = 16*1024 + 256; // SSL record size + spare

  unsigned char buf_[NET_BUF_SIZE];
  unsigned char* data_start_;
  unsigned char* data_end_;

public:
  net_buffer()
  {
    data_start_ = data_end_ = buf_;
  }
  unsigned char* get_unused_start() { return data_end_; }
  unsigned char* get_data_start() { return data_start_; }
  size_t get_unused_len() { return (NET_BUF_SIZE - (data_end_ - buf_)); }    
  size_t get_data_len() { return (data_end_ - data_start_); }    
  void data_added(size_t count)
  { 
    data_end_ += count; 
    data_end_ = data_end_ > (buf_ + NET_BUF_SIZE)? 
      (buf_ + NET_BUF_SIZE):
      data_end_; 
  }
  void data_removed(size_t count) 
  { 
    data_start_ += count; 
    if (data_start_ >= data_end_) reset(); 
  }
  void reset() { data_start_ = buf_; data_end_ = buf_; }               
  bool has_data() { return (data_start_ < data_end_); }
}; // class net_buffer

//
// Operation class
//
//
template <typename Stream>
class openssl_operation
{
public:

  // Constructor for asynchronous operations
  openssl_operation(ssl_primitive_func primitive,
                    Stream& socket,
                    net_buffer& recv_buf,
                    SSL* session,
                    BIO* ssl_bio,
                    user_handler_func  handler,
                    boost::asio::io_service::strand& strand
                    )
    : primitive_(primitive)
    , user_handler_(handler)
    , strand_(&strand)
    , recv_buf_(recv_buf)
    , socket_(socket)
    , ssl_bio_(ssl_bio)
    , session_(session)
  {
    write_ = boost::bind(
      &openssl_operation::do_async_write, 
      this, boost::arg<1>(), boost::arg<2>()
    );
    read_ = boost::bind(
      &openssl_operation::do_async_read, 
      this
    );
    handler_= boost::bind(
      &openssl_operation::async_user_handler, 
      this, boost::arg<1>(), boost::arg<2>()
    );
  }

  // Constructor for synchronous operations
  openssl_operation(ssl_primitive_func primitive,
                    Stream& socket,
                    net_buffer& recv_buf,
                    SSL* session,
                    BIO* ssl_bio)
    : primitive_(primitive)
    , strand_(0)
    , recv_buf_(recv_buf)
    , socket_(socket)
    , ssl_bio_(ssl_bio)
    , session_(session)
  {      
    write_ = boost::bind(
      &openssl_operation::do_sync_write, 
      this, boost::arg<1>(), boost::arg<2>()
    );
    read_ = boost::bind(
      &openssl_operation::do_sync_read, 
      this
    );
    handler_ = boost::bind(
      &openssl_operation::sync_user_handler, 
      this, boost::arg<1>(), boost::arg<2>()
      );
  }

  // Start operation
  // In case of asynchronous it returns 0, in sync mode returns success code
  // or throws an error...
  int start()
  {
    int rc = primitive_( session_ );

    bool is_operation_done = (rc > 0);  
                // For connect/accept/shutdown, the operation
                // is done, when return code is 1
                // for write, it is done, when is retcode > 0
                // for read, is is done when retcode > 0

    int error_code =  !is_operation_done ?
          ::SSL_get_error( session_, rc ) :
          0;        
    int sys_error_code = ERR_get_error();

    if (error_code == SSL_ERROR_SSL)
      return handler_(boost::system::error_code(
            error_code, boost::asio::error::get_ssl_category()), rc);

    bool is_read_needed = (error_code == SSL_ERROR_WANT_READ);
    bool is_write_needed = (error_code == SSL_ERROR_WANT_WRITE ||
                              ::BIO_ctrl_pending( ssl_bio_ ));
    bool is_shut_down_received = 
      ((::SSL_get_shutdown( session_ ) & SSL_RECEIVED_SHUTDOWN) == 
          SSL_RECEIVED_SHUTDOWN);
    bool is_shut_down_sent = 
      ((::SSL_get_shutdown( session_ ) & SSL_SENT_SHUTDOWN) ==
            SSL_SENT_SHUTDOWN);

    if (is_shut_down_sent && is_shut_down_received
        && is_operation_done && !is_write_needed)
      // SSL connection is shut down cleanly
      return handler_(boost::system::error_code(), 1);

    if (is_shut_down_received && !is_operation_done)
      // Shutdown has been requested, while we were reading or writing...
      // abort our action...
      return handler_(boost::asio::error::shut_down, 0);

    if (!is_operation_done && !is_read_needed && !is_write_needed 
      && !is_shut_down_sent)
    {
      // The operation has failed... It is not completed and does 
      // not want network communication nor does want to send shutdown out...
      if (error_code == SSL_ERROR_SYSCALL)
      {
        return handler_(boost::system::error_code(
              sys_error_code, boost::asio::error::system_category), rc); 
      }
      else
      {
        return handler_(boost::system::error_code(
              error_code, boost::asio::error::get_ssl_category()), rc); 
      }
    }

    if (!is_operation_done && !is_write_needed)
    {
      // We may have left over data that we can pass to SSL immediately
      if (recv_buf_.get_data_len() > 0)
      {
        // Pass the buffered data to SSL
        int written = ::BIO_write
        ( 
          ssl_bio_, 
          recv_buf_.get_data_start(), 
          recv_buf_.get_data_len() 
        );

        if (written > 0)
        {
          recv_buf_.data_removed(written);
        }
        else if (written < 0)
        {
          if (!BIO_should_retry(ssl_bio_))
          {
            // Some serios error with BIO....
            return handler_(boost::asio::error::no_recovery, 0);
          }
        }

        return start();
      }
      else if (is_read_needed || (is_shut_down_sent && !is_shut_down_received))
      {
        return read_();
      }
    }

    // Continue with operation, flush any SSL data out to network...
    return write_(is_operation_done, rc); 
  }

// Private implementation
private:
  typedef boost::function<int (const boost::system::error_code&, int)>
    int_handler_func;
  typedef boost::function<int (bool, int)> write_func;
  typedef boost::function<int ()> read_func;

  ssl_primitive_func  primitive_;
  user_handler_func  user_handler_;
  boost::asio::io_service::strand* strand_;
  write_func  write_;
  read_func  read_;
  int_handler_func handler_;
    
  net_buffer send_buf_; // buffers for network IO

  // The recv buffer is owned by the stream, not the operation, since there can
  // be left over bytes after passing the data up to the application, and these
  // bytes need to be kept around for the next read operation issued by the
  // application.
  net_buffer& recv_buf_;

  Stream& socket_;
  BIO*    ssl_bio_;
  SSL*    session_;

  //
  int sync_user_handler(const boost::system::error_code& error, int rc)
  {
    if (!error)
      return rc;

    throw boost::system::system_error(error);
  }
    
  int async_user_handler(boost::system::error_code error, int rc)
  {
    if (rc < 0)
    {
      if (!error)
        error = boost::asio::error::no_recovery;
      rc = 0;
    }

    user_handler_(error, rc);
    return 0;
  }

  // Writes bytes asynchronously from SSL to NET
  int  do_async_write(bool is_operation_done, int rc) 
  {
    int len = ::BIO_ctrl_pending( ssl_bio_ );
    if ( len )
    { 
      // There is something to write into net, do it...
      len = (int)send_buf_.get_unused_len() > len? 
        len: 
        send_buf_.get_unused_len();
        
      if (len == 0)
      {
        // In case our send buffer is full, we have just to wait until 
        // previous send to complete...
        return 0;
      }

      // Read outgoing data from bio
      len = ::BIO_read( ssl_bio_, send_buf_.get_unused_start(), len); 
         
      if (len > 0)
      {
        unsigned char *data_start = send_buf_.get_unused_start();
        send_buf_.data_added(len);
 
        BOOST_ASSERT(strand_); 
        boost::asio::async_write
        ( 
          socket_, 
          boost::asio::buffer(data_start, len),
          strand_->wrap
          (
            boost::bind
            (
              &openssl_operation::async_write_handler, 
              this, 
              is_operation_done,
              rc, 
              boost::asio::placeholders::error, 
              boost::asio::placeholders::bytes_transferred
            )
          )
        );
                  
        return 0;
      }
      else if (!BIO_should_retry(ssl_bio_))
      {
        // Seems like fatal error
        // reading from SSL BIO has failed...
        handler_(boost::asio::error::no_recovery, 0);
        return 0;
      }
    }
    
    if (is_operation_done)
    {
      // Finish the operation, with success
      handler_(boost::system::error_code(), rc);
      return 0;
    }
    
    // OPeration is not done and writing to net has been made...
    // start operation again
    start();
          
    return 0;
  }

  void async_write_handler(bool is_operation_done, int rc, 
    const boost::system::error_code& error, size_t bytes_sent)
  {
    if (!error)
    {
      // Remove data from send buffer
      send_buf_.data_removed(bytes_sent);

      if (is_operation_done)
        handler_(boost::system::error_code(), rc);
      else
        // Since the operation was not completed, try it again...
        start();
    }
    else 
      handler_(error, rc);
  }

  int do_async_read()
  {
    // Wait for new data
    BOOST_ASSERT(strand_);
    socket_.async_read_some
    ( 
      boost::asio::buffer(recv_buf_.get_unused_start(),
        recv_buf_.get_unused_len()),
      strand_->wrap
      (
        boost::bind
        (
          &openssl_operation::async_read_handler, 
          this, 
          boost::asio::placeholders::error, 
          boost::asio::placeholders::bytes_transferred
        )
      )
    );
    return 0;
  }

  void async_read_handler(const boost::system::error_code& error,
      size_t bytes_recvd)
  {
    if (!error)
    {
      recv_buf_.data_added(bytes_recvd);

      // Pass the received data to SSL
      int written = ::BIO_write
      ( 
        ssl_bio_, 
        recv_buf_.get_data_start(), 
        recv_buf_.get_data_len() 
      );

      if (written > 0)
      {
        recv_buf_.data_removed(written);
      }
      else if (written < 0)
      {
        if (!BIO_should_retry(ssl_bio_))
        {
          // Some serios error with BIO....
          handler_(boost::asio::error::no_recovery, 0);
          return;
        }
      }

      // and try the SSL primitive again
      start();
    }
    else
    {
      // Error in network level...
      // SSL can't continue either...
      handler_(error, 0);
    }
  }

  // Syncronous functions...
  int do_sync_write(bool is_operation_done, int rc)
  {
    int len = ::BIO_ctrl_pending( ssl_bio_ );
    if ( len )
    { 
      // There is something to write into net, do it...
      len = (int)send_buf_.get_unused_len() > len? 
        len: 
        send_buf_.get_unused_len();
        
      // Read outgoing data from bio
      len = ::BIO_read( ssl_bio_, send_buf_.get_unused_start(), len); 
         
      if (len > 0)
      {
        size_t sent_len = boost::asio::write( 
                  socket_, 
                  boost::asio::buffer(send_buf_.get_unused_start(), len)
                  );

        send_buf_.data_added(len);
        send_buf_.data_removed(sent_len);
      }          
      else if (!BIO_should_retry(ssl_bio_))
      {
        // Seems like fatal error
        // reading from SSL BIO has failed...
        throw boost::system::system_error(boost::asio::error::no_recovery);
      }
    }
    
    if (is_operation_done)
      // Finish the operation, with success
      return rc;
                
    // Operation is not finished, start again.
    return start();
  }

  int do_sync_read()
  {
    size_t len = socket_.read_some
      ( 
        boost::asio::buffer(recv_buf_.get_unused_start(),
          recv_buf_.get_unused_len())
      );

    // Write data to ssl
    recv_buf_.data_added(len);

    // Pass the received data to SSL
    int written = ::BIO_write
    ( 
      ssl_bio_, 
      recv_buf_.get_data_start(), 
      recv_buf_.get_data_len() 
    );

    if (written > 0)
    {
      recv_buf_.data_removed(written);
    }
    else if (written < 0)
    {
      if (!BIO_should_retry(ssl_bio_))
      {
        // Some serios error with BIO....
        throw boost::system::system_error(boost::asio::error::no_recovery);
      }
    }

    // Try the operation again
    return start();
  }
}; // class openssl_operation

} // namespace detail
} // namespace ssl
} // namespace asio
} // namespace boost

#include <boost/asio/detail/pop_options.hpp>

#endif // BOOST_ASIO_SSL_DETAIL_OPENSSL_OPERATION_HPP
