Logo Search packages:      
Sourcecode: kmess version File versions

tcpconnectionbase.cpp

/***************************************************************************
                          tcpconnectionbase.cpp -  description
                             -------------------
    begin                : Tue Sep 03 2006
    copyright            : (C) 2005 by Diederik van der Boor
    email                : vdboor --at-- codingdomain.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "tcpconnectionbase.h"

#include "../kmessdebug.h"

#include <qcstring.h>
#include <kextsock.h>
#include <ksockaddr.h>
#include <kssl.h>
#include <klocale.h>

#ifdef KMESSDEBUG_TCPCONNECTION
  #define KMESSDEBUG_TCPCONNECTION_GENERAL
#endif


/**
 * @brief The constructor
 * @param  parent  The Qt object parent.
 * @param  name    The Qt object name, for debugging signals.
 */
00038 TcpConnectionBase::TcpConnectionBase( QObject *parent, const char *name)
  : QObject(parent, name)
  , connectTimeout_(10)
  , socket_(0)
  , ssl_(0)
  , timeout_(false)
{
  connect( &connectionTimer_, SIGNAL(               timeout() ),
           this,              SLOT  ( slotConnectionTimeout() ));
}



/**
 * @brief The destructor
 *
 * Closes the sockets and clears the buffer.
 */
00056 TcpConnectionBase::~TcpConnectionBase()
{
  // Close and clean up
  if( socket_ != 0 )
  {
    closeConnection();
    delete socket_;
    socket_ = 0;
  }

  readBuffer_.truncate(0);

#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
  kdDebug() << "DESTROYED TcpConnectionBase" << endl;
#endif
}



/**
 * @brief Close the connection.
 */
00078 void TcpConnectionBase::closeConnection()
{
  // Internal function to disconnect, useful for derived classes.
  closeSockets();
}



/**
 * @brief Close the sockets.
 *
 * This is a protected function called internally from closeConnection().
 * This allows derived classes to make a distinction between internal disconnects and external closeConnection() requests.
 *
 * @return Returns whether the socket had to be closed; e.g. when the socket was already closed it returns false.
 */
00094 bool TcpConnectionBase::closeSockets()
{
  // Reset globals
  connectionTimer_.stop();

  // Close buffers
  readBuffer_.truncate(0);

  // Close and delete the socket
  if( socket_ != 0 )
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: closing sockets" << endl;
#endif

    if( ssl_ != 0 )
    {
      ssl_->close();
      //ssl_->reInitialize();
      delete ssl_;
      ssl_ = 0;
    }

    socket_->flush();
    socket_->closeNow();  // also calls cancelAsyncConnect()
    delete socket_;
    socket_ = 0;

#ifdef KMESS_NETWORK_WINDOW
    KMESS_NET_CLOSE(this);
#endif

    return true;
  }
  else
  {
    return false;
  }
}



/**
 * @brief Return the number of bytes which are already received in the local buffer
 *
 * @return The number of bytes in the buffer, or a negative number in case of an error.
 */
00141 int TcpConnectionBase::getBytesAvailable() const
{
  if(KMESS_NULL(socket_)) return -2;  // same as KExtendedSocket API, means "invalid state"

  if( ssl_ != 0 )
  {
    return readBuffer_.size();
  }
  else
  {
    return socket_->bytesAvailable();
  }
}



/**
 * @brief Return the connect timeout in seconds.
 * @return The connection timeout.
 */
00161 int TcpConnectionBase::getConnectTimeout() const
{
  return connectTimeout_;
}



/**
 * @brief Get the remote ip the socket is connected with.
 * @returns The remote IP.
 */
00172 QString TcpConnectionBase::getRemoteIp() const
{
  if(socket_ == 0)
  {
    return QString::null;
  }
  else if( socket_->peerAddress() == 0 )
  {
    return QString::null;
  }
  else
  {
    return socket_->peerAddress()->nodeName();
  }
}



/**
 * @brief Get the remote port the socket is connected with.
 * @returns The remote port.
 */
00194 QString TcpConnectionBase::getRemotePort() const
{
  if(socket_ == 0)
  {
    return QString::null;
  }
  else if( socket_->peerAddress() == 0 )
  {
    return QString::null;
  }
  else
  {
    return socket_->peerAddress()->serviceName();
  }
}



/**
 * @brief Get a description of the socket error.
 *
 * This is only useful in slotConnectionFailed().
 *
 * @return The error description.
 */
00219 QString TcpConnectionBase::getSocketError() const
{
  if(socket_ == 0)
  {
    return QString::null;
  }
  else if( timeout_ )
  {
    return i18n("Connection timeout");
  }
  else
  {
    // The return value of systemError() is the "errno" value from errno.h at the
    // time the socket system-call failed. This function hides the low-level stuff
    // like "errno" (see man:errno(3)) and perror() (see man:perror(3))
    return KExtendedSocket::strError( socket_->status(), socket_->systemError() );
  }
}



/**
 * @brief Return true if a connection is active.
 * @returns True when connected, false when disconnected.
 */
00244 bool TcpConnectionBase::isConnected() const
{
  return (socket_ != 0 && socket_->socketStatus() == KExtendedSocket::connected);
}



/**
 * @brief Connect to a remote host.
 * @param  host   IP-address or hostname to connect to.
 * @param  port   TCP-port to connect to.
 * @param  ssl    Whether to use SSL or not.
 * @param  async  Use asychonous connections, or not. If false, the application blocks until the connection is made or failed.
 */
00258 bool TcpConnectionBase::openConnection(const QString &host, const int port, bool ssl, bool async)
{
#ifdef KMESSTEST
  ASSERT( socket_ == 0 );
  ASSERT( ssl_    == 0 );
  ASSERT( port    != 0 );
  ASSERT( ! host.isEmpty() );
#endif

  // Test for sources of unexplainable errors.
  if( host.isEmpty() || port == 0 )
  {
    kdWarning() << "TcpConnectionBase::openConnection: No ipAddress or port given!" << endl;
    return false;
  }

#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
  kdDebug() << "TcpConnectionBase: Connecting to " << host << " port " << port << " (ssl=" << ssl << ")." << endl;
#endif

  // Configure the socket for asynchronous operation
  // Note that a buffered socket only emits the readyRead() signal when data is really received.
  // The socket is made non-blocking, so the connectionSuccess() signal tells us when the connection is made.
  // Otherwise KMess would freeze until a connection is made or fails.
  socket_ = new KExtendedSocket(host, port, ( ssl ? KExtendedSocket::streamSocket : KExtendedSocket::bufferedSocket ) );  // not buffering because KSSL/OpenSSL accesses the socket directly.
  socket_->enableRead(true);
  socket_->enableWrite(false);
  socket_->setBlockingMode(false);  // avoid blocking until connect() returns
  socket_->setTimeout( connectTimeout_ );
  connect( socket_, SIGNAL( readyRead()           ), this, SLOT( slotSocketReadyRead()  ));
  connect( socket_, SIGNAL( readyWrite()          ), this, SLOT( slotSocketReadyWrite() ));
  connect( socket_, SIGNAL( connectionFailed(int) ), this, SLOT( slotSocketFailed(int)  ));
  connect( socket_, SIGNAL( connectionSuccess()   ), this, SLOT( slotSocketConnected()  ));

  // Update state variables
  timeout_      = false;
  connectingTo_ = host + ":" + QString::number(port);

  // Connect the socket
  int connectResult = (async ? socket_->startAsyncConnect() : socket_->connect());

  // Connect the socket
  if(connectResult != 0)
  {
    kdWarning() << "TcpConnectionBase: Couldn't connect new socket to " << connectingTo_ << endl;
    delete socket_;
    socket_ = 0;
    return false;
  }

  // Start timer, as the asyncConnect doesn't seam to honor the timeout value.
  connectionTimer_.start( connectTimeout_ * 1000, true );  // because KExtendedSocket uses seconds, not miliseconds

  // Initialize SSL
  if( ssl )
  {
    ssl_ = new KSSL();
  }

  // The code continues with slotSocketConnected() or slotSocketFailed()
  return true;
}



/**
 * @brief Generate data/size values from a QByteArray buffer.
 *
 * This is used by peekBlock() and readBlock().
 * The returned bufferStart and bufferSize values respect the allocated size of the buffer,
 * even if maxSize / bufferOffset exceed the buffer size. An error will be displayed at the
 * console when this occurs.
 *
 * @param  buffer        The buffer to fill.
 * @param  maxSize       The maximum number of bytes to fill.
 * @param  bufferOffset  The starting position where bytes should be written.
 * @param  bufferStart   Output value, the 'data' pointer to use.
 * @param  blockSize     Output value, the 'size' value to use.
 * @return  Whether the output values are set or not.
 */
00338 bool TcpConnectionBase::parseBuffer( QByteArray *buffer, const int maxSize, const int bufferOffset, char **bufferStart, int *blockSize )
{
  // Determine the maximum number of bytes we can write to the buffer
  int bufferSpace = buffer->size() - bufferOffset;

  // API usage check.
  // Warn if certain sizes get a unusual value
  if(buffer->size() == 0)
  {
    kdWarning() << "TcpConnectionBase::readBlock: buffer is not initialized!" << endl;
    return false;
  }
  else if(bufferSpace < 0)
  {
    kdWarning() << "TcpConnectionBase::readBlock: offset of " << bufferOffset << " bytes"
                << " exceeds the buffer size of " << buffer->size() << " bytes!" << endl;
  }
  else if(bufferSpace < maxSize)
  {
    kdWarning() << "TcpConnectionBase::readBlock: maximum allowed read size"
                << " exceeds the buffer size of " << buffer->size() << " bytes!" << endl;
  }

  // Fill the buffer from the give offset,
  // size argument is limited to what the buffer can/may sustain.
  *bufferStart = buffer->data() + bufferOffset;
  *blockSize   = (maxSize == 0 ? bufferSpace : QMIN(bufferSpace, maxSize));
  return true;
}



/**
 * @brief Peek data from the socket.
 *
 * The contents in the buffer is overwritten with the socket data.
 *
 * Unlike readBlock(), the data is not removed from the buffer.
 * It can be peeked or read later. This method is useful to
 * search in incoming data for an expected end marking (e.g. newline).
 *
 * @param  buffer  Buffer to fill with the data.
 * @param  size    Size of the buffer; the maximum number of bytes which can be written to it.
 * @return  The number of bytes which could be written to the buffer. A negative value is returned when an error occured.
 */
00383 int TcpConnectionBase::peekBlock( char *buffer, int size )
{
#ifdef KMESSTEST
  ASSERT( size > 0 );
#endif

  // Avoid crashes
  if(KMESS_NULL(socket_)) return -1;
  if(KMESS_NULL(buffer))  return -1;

  // Fill the buffer
  if( ssl_ != 0 )
  {
    // KMessBuffer::left warns if the size exceeds the buffer, peek manually.
    // data is already stored into a buffer because SSL socket is unbuffered.
    uint realBytes = QMIN( readBuffer_.size(), (uint) size );
    if( realBytes == 0 )
    {
      return 0;
    }
    else
    {
      memcpy( buffer, readBuffer_.data(), realBytes );
      return realBytes;
    }
  }
  else
  {
    return socket_->peekBlock( buffer, size );
  }
}



/**
 * @brief Peek data from the socket.
 *
 * The contents of the QByteArray is overwritten with the socket data.
 * The value of QByteArray::size() is used as buffer size, maxSize can only be
 * used to limit the number of bytes that should be written, not extend it.
 *
 * This is a convenience function which uses a QByteArray as buffer to avoid buffer overflows.
 * 
 * @param  buffer        The buffer to fill with socket data.
 * @param  maxSize       The maximum number of bytes that should be written to the buffer, in case the buffer is larger.
 * @param  bufferOffset  The offset in the buffer, the byte position where the new data should be written to.
 * @return  The number of bytes which could be written to the buffer. A negative value is returned when an error occured.
 */
00431 int TcpConnectionBase::peekBlock( QByteArray *buffer, const int maxSize, const int bufferOffset )
{
  // Avoid crashes
  if(KMESS_NULL(socket_)) return -1;
  if(KMESS_NULL(buffer))  return -1;

  char *bufferStart = 0;
  int   blockSize   = 0;

  if( ! parseBuffer( buffer, maxSize, bufferOffset, &bufferStart, &blockSize  ) )
  {
    return -1;
  }

  return peekBlock( bufferStart, blockSize );
}



/**
 * @brief Return all data available in the read buffer.
 * @returns All data in the buffer.
 */
00454 QByteArray TcpConnectionBase::readAll()
{
  if( ssl_ != 0 )
  {
    QByteArray data = readBuffer_;  // creates explicitly shared copy.
    data.detach();                  // force deep copy, avoid sharing the same data.
    readBuffer_.truncate(0);
    return data;
  }
  else
  {
    return socket_->readAll();
  }
}



/**
 * @brief Read data from the socket.
 *
 * In constrast to the peekBlock() function, this function
 * also removes the returned data from the read buffer.
 *
 * @param  buffer  Buffer to fill with the data.
 * @param  size    Size of the buffer; the maximum number of bytes which can be written to it.
 * @return  The number of bytes which could be written to the buffer. A negative value is returned when an error occured.
 */
00481 int TcpConnectionBase::readBlock( char *buffer, int size )
{
#ifdef KMESSTEST
  ASSERT( size > 0 );
#endif

  // Avoid crashes
  if(KMESS_NULL(socket_)) return -1;
  if(KMESS_NULL(buffer))  return -1;

  // Fill the buffer
  if( ssl_ != 0 )
  {
#ifdef KMESSDEBUG_TCPCONNECTION
    kdDebug() << "TcpConnectionBase: read buffer has " << readBuffer_.size() << " bytes available, "
              << "requesting " << size << " bytes." << endl;
#endif
    // data is already stored into a buffer because SSL socket is unbuffered.
    int bytesRead = readBuffer_.readBlock( buffer, size );
    return bytesRead;
  }
  else
  {
    return socket_->readBlock( buffer, size );
  }
}



/**
 * @brief Read data from the socket.
 *
 * The contents of the QByteArray is overwritten with the socket data.
 * The value of QByteArray::size() is used as buffer size, maxSize can only be
 * used to limit the number of bytes that should be written, not extend it.
 *
 * This is a convenience function which uses a QByteArray as buffer to avoid buffer overflows.
 *
 * @param  buffer        The buffer to fill with socket data.
 * @param  maxSize       The maximum number of bytes that should be written to the buffer, in case the buffer is larger.
 * @param  bufferOffset  The offset in the buffer, the byte position where the new data should be written to.
 * @return  The number of bytes which could be written to the buffer. A negative value is returned when an error occured.
 */
00524 int TcpConnectionBase::readBlock( QByteArray *buffer, const int maxSize, const int bufferOffset )
{
  // Avoid crashes
  if(KMESS_NULL(socket_)) return -1;
  if(KMESS_NULL(buffer))  return -1;

  char *bufferStart = 0;
  int   blockSize   = 0;

  if( ! parseBuffer( buffer, maxSize, bufferOffset, &bufferStart, &blockSize ) )
  {
    return -1;
  }

  int bytesRead = readBlock( bufferStart, blockSize );
  return bytesRead;
}



/**
 * @brief Set the connect timeout
 * @param  seconds  The new timeout value.
 */
00548 void TcpConnectionBase::setConnectTimeout( int seconds )
{
  connectTimeout_ = seconds;
}



/**
 * @brief Called when an error occured while connecting.
 *
 * Either a connection timeout occured, or the remote host refused the connection.
 */
00560 void TcpConnectionBase::slotConnectionFailed()
{
  // This method is added for derived classes to overload.

  QString sysError = getSocketError();  // avoids poking sys_errlist[] manually

  if( ! sysError.isNull() )
  {
    kdWarning() << "TcpConnectionBase: Failed to connect to " << connectingTo_ << ", system error: " << sysError << "." << endl;
  }
  else
  {
    kdWarning() << "TcpConnectionBase: Failed to connect to " << connectingTo_ << "!" << endl;
  }
}



/**
 * @brief Called when timeout occured while connecting to a remote host.
 *
 * This method stops the timeout detection, closes the sockets
 * and calls slotConnectionFailed().
 */
00584 void TcpConnectionBase::slotConnectionTimeout()
{
  if( isConnected() )
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase::slotConnectionTimeout: Timer fired but connection already established, ignore." << endl;
#endif
    return;
  }


#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
  kdDebug() << "TcpConnectionBase: Timer fired before connection was established." << endl;
#endif

  // Delete sockets, no connectionClosed events.
  closeSockets();

  // Set state for getSocketError();
  timeout_ = true;

  // Inform derived class
  connectionTimer_.stop();
  slotConnectionFailed();
}



/**
 * @brief Called when the socket connected.
 *
 * This method stops the timeout detection, initializes the buffers
 * and calls slotConnectionEstablished().
 */
00618 void TcpConnectionBase::slotSocketConnected()
{
  // This is called when the socket in openConnection() was connected.
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
  kdDebug() << "TcpConnectionBase: Connected with host" << endl;
#endif

  // Connect SSL
  if( ssl_ != 0 )
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: Connecting SSL" << endl;
#endif
    int sslStatus = ssl_->connect( socket_->fd() );
    if( sslStatus != 1 )
    {
      kdWarning() << "TcpConnectionBase::slotSocketConnected: SSL Connection failed "
                  << "(endpoint=" << connectingTo_ << " sslerror=" << sslStatus << ")" << endl;
      return;
    }
  }

  // Update state
  connectionTimer_.stop();
  readBuffer_.truncate(0);

  // Inform derived class
  slotConnectionEstablished();
}



/**
 * @brief Called when the connection could not be made.
 *
 * This method stops the timeout detection
 * and calls slotConnectionFailed().
 *
 * @param  error  The system error code.
 */
00658 void TcpConnectionBase::slotSocketFailed(int error)
{
  // This is called when the socket in openConnection() failed.
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
  kdDebug() << "TcpConnectionBase: Failed to connect to host: " << getSocketError() << endl;
#endif
#ifdef KMESSTEST
  ASSERT( socket_->systemError() == error );
#endif

  // Inform derived class
  connectionTimer_.stop();
  slotConnectionFailed();
}



/**
 * @brief Called when new data is available to read.
 *
 * This method calls slotDataReceived().
 *
 * For SSL-based sockets, this method buffers the SSL data before notifying the derived class.
 * While KExtendedSocket implements read buffers, KSSL does not.
 * For SSL negotiation messages, it's possible KSSL consumes all data.
 * The slotDataReceived() method won't be called in that situation.
 */
00685 void TcpConnectionBase::slotSocketReadyRead()
{
  if( KMESS_NULL(socket_) ) return;

  // When SSL is used, the socket is unbuffered.
  // Read all data from the socket into the local buffer.
  if( ssl_ != 0 )
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: socket is ready, ssl has " << ssl_->pending() << " bytes pending for immediate read." << endl;
#endif

    // Attempt to read all data available.
    char buffer[512];
    int bytesRead = 0;
    int totalBytesRead = false;
    do
    {
      bytesRead = ssl_->read(buffer, 512);
      if( bytesRead < 0 )
      {
        kdWarning() << className() << ": SSL read returned " << bytesRead << endl;
        return;
      }
      else if( bytesRead > 0 )
      {
        // Move pointer to end to write, move
        readBuffer_.add( buffer, bytesRead );
        totalBytesRead += bytesRead;
      }
    }
    while( bytesRead == 512 || ssl_->pending() > 0 );

    // Notify subclass when data is available.
    if( totalBytesRead > 0 )
    {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
      kdDebug() << "TcpConnectionBase: Read " << totalBytesRead << " bytes from the SSL layer." << endl;
#endif
      slotDataReceived();
    }
  }
  else
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: Socket has " << socket_->bytesAvailable() << " bytes available." << endl;
#endif
    slotDataReceived();
  }
}



/**
 * @brief Called when the socket can write new data.
 *
 * This is currently a STUB for an event-based writing methods.
 */
00743 void TcpConnectionBase::slotSocketReadyWrite()
{
  // STUB.
  // Flush the write buffer, call enableWrite(false) when the buffer is empty.
}



/**
 * @brief Stop the connection timeout detection.
 */
00754 void TcpConnectionBase::stopTimeoutDetection()
{
  connectionTimer_.stop();
}



/**
 * @brief  Write data to the socket
 * @param  block  The data to write.
 * @param  size   The size of the data.
 * @return Whether the bytes could be written to the socket.
 */
00767 bool TcpConnectionBase::writeBlock( const char *block, const int size )
{
  if(KMESS_NULL(socket_)) return false;

  int noBytesWritten;

  if( ssl_ != 0 )
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: Writing " << size << " bytes to the SSL layer." << endl;
#endif

    noBytesWritten = ssl_->write( block, size );
  }
  else
  {
#ifdef KMESSDEBUG_TCPCONNECTION_GENERAL
    kdDebug() << "TcpConnectionBase: Writing " << size << " bytes to the socket buffer." << endl;
#endif

    noBytesWritten = socket_->writeBlock( block, size );
  }

#ifdef KMESS_NETWORK_WINDOW
  QByteArray wrapper;
  wrapper.setRawData( block, size );
  KMESS_NET_SENT(this, wrapper);
  wrapper.resetRawData( block, size );
#endif

  // Return true if the data was indeed written.
  return ( noBytesWritten == size );
}



/**
 * @brief Write data to the socket.
 *
 * This is a convience function.
 *
 * @param  block  The data to write
 * @return Whether the bytes could be written to the socket.
 */
00811 bool TcpConnectionBase::writeBlock( const QByteArray &block )
{
  return writeBlock( block.data(), block.size() );
}


#include "tcpconnectionbase.moc"

Generated by  Doxygen 1.6.0   Back to index