/*************************************************************************** msnsockettcp.cpp - description ------------------- begin : Thu Jan 23 2003 copyright : (C) 2003 by Mike K. Bennett email : mkb137b@hotmail.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 "msnsockettcp.h" #include <QTimer> #include <QTcpSocket> #include <QHostAddress> #include <KLocale> #include "../utils/kmessshared.h" #include "../currentaccount.h" #include "../kmessapplication.h" #include "../kmessdebug.h" #ifdef KMESSDEBUG_CONNECTION // Area-specific debug statements #define KMESSDEBUG_CONNECTION_SOCKET_TCP #endif /** * @brief Login timer for DNS name resolution * * In milliseconds, the time to wait for the msn server name resolution to complete. */ #define LOGIN_TIMER_DNS_RESOLVE 10000 /** * @brief Login timer for actual server connection * * In milliseconds, the time to wait for the connection to be established. */ #define LOGIN_TIMER_CONNECTION 5000 #define MSN_TCP_HOST "messenger.hotmail.com" #define MSN_TCP_PORT 1863 /** * @brief The constructor * * Initializes the sockets, buffers and ping timer. */ 00060 MsnSocketTcp::MsnSocketTcp( ServerType serverType ) : MsnSocketBase( serverType ) , missedPings_( 0 ) , nextPayloadSize_( 0 ) , sendPings_( false ) { setObjectName( "MsnSocketTcp" ); // Create the socket socket_ = new QTcpSocket( this ); // TODO: socket timeout settings.. // Forward the socket's raw event signals for further interpretation connect( socket_, SIGNAL( connected() ), this, SLOT ( slotConnected() ) ); connect( socket_, SIGNAL( disconnected() ), this, SLOT ( slotDisconnected() ) ); // Connect the socket's signals to parse them internally connect( socket_, SIGNAL( hostFound() ), this, SLOT ( slotHostFound() ) ); connect( socket_, SIGNAL( readyRead() ), this, SLOT ( slotSocketDataReceived() ) ); connect( socket_, SIGNAL( error(QAbstractSocket::SocketError) ), this, SLOT ( slotSocketError(QAbstractSocket::SocketError) ) ); connect( socket_, SIGNAL( proxyAuthenticationRequired(const QNetworkProxy&,QAuthenticator*) ), this, SLOT ( proxyAuthenticate(const QNetworkProxy&,QAuthenticator*) ) ); // Connect the ping timer to the sendPing slot pingTimer_.stop(); connect( &pingTimer_, SIGNAL( timeout() ), this, SLOT ( slotSendPing() ) ); // Attach a timer to watch over the connection process connectionTimer_.stop(); connectionTimer_.setSingleShot( true ); connect( &connectionTimer_, SIGNAL( timeout() ), this, SLOT ( slotLoginFailed() ) ); #ifdef KMESSTEST KMESS_ASSERT( socket_ != 0 ); #endif } /** * @brief The destructor * * Closes the connection, and deletes the sockets. */ 00111 MsnSocketTcp::~MsnSocketTcp() { // Disconnect from the server on exit if( isConnected() ) { disconnectFromServer(); } // Delete the socket delete socket_; #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "DESTROYED"; #endif } /** * @brief Connect to the given server via the socket * * The connection attempt is asynchronous. * When the connection is made, connectionSuccess() is called, * on error connectionFailed() is called. * Connection timeouts are not detected here yet. * * @param server The server hostname or IP address. * @param port The port to connect to. */ 00140 void MsnSocketTcp::connectToServer( const QString& server, const quint16 port ) { quint16 desiredPort; QString desiredServer; if( server.isEmpty() && port == 0 ) { desiredServer = MSN_TCP_HOST; desiredPort = MSN_TCP_PORT; } else { desiredServer = server; desiredPort = port; } // Redirect to another server if started with --server if( static_cast<KMessApplication*>( kapp )->getUseTestServer() ) { desiredServer = static_cast<KMessApplication*>( kapp )->getTestServer(); } #ifdef KMESSTEST KMESS_ASSERT( desiredPort < 32768 ); #endif #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "Connecting to server at " << desiredServer << ":" << desiredPort << "."; kDebug() << "Socket state is " << socket_->state() << "."; #endif // Flush any pending data if the socket is closing a previous connection socket_->blockSignals( true ); socket_->flush(); // Check if we are disconnected (the socket is idle) if( socket_->state() != QAbstractSocket::UnconnectedState ) { kWarning() << "Socket is not disconnected. Already connected? The socket is in state" << socket_->state() << "."; } // Prepare state vars for the new connection connected_ = true; nextPayloadSize_ = 0; // Close any previous connection (without firing disconnection signals) socket_->close(); socket_->blockSignals( false ); // Start connecting socket_->connectToHost( desiredServer, desiredPort ); // Start the connection timer to watch over the server name resolution connectionTimer_.start( LOGIN_TIMER_DNS_RESOLVE ); } /** * @brief Disconnect from the server * * This function is called after closeConnection() * It empties all buffers, and resets the sockets. * * @param isTransfer When set, a disconnected() signal won't be fired. * This is used to handle transfers to another server * without noticing a disconnect. */ 00209 void MsnSocketTcp::disconnectFromServer( bool isTransfer ) { // Stop pinging setSendPings( false ); // Stop the connection timer in case the user closed the connection during login connectionTimer_.stop(); #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "Close the socket."; #endif // Disconnect the socket socket_->blockSignals( true ); socket_->disconnectFromHost(); // Just clean up if we are disconnected already if( ! connected_ ) { return; } connected_ = false; // Do not signal a disconnection when we are transferring to another gateway if( isTransfer ) { return; } #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "Emitting disconnection signal."; #endif emit disconnected(); } /** * @brief Return the local IP address of the socket. * * This is typically a LAN address, unless the system * is directly connected to the Internet. * * @returns The IP address the socket uses at the system. */ 00256 QString MsnSocketTcp::getLocalIp() const { QHostAddress address = socket_->localAddress(); return address.toString(); } /** * @brief Return whether or the socket is connected. * * @returns True when connected, false otherwise. */ 00270 bool MsnSocketTcp::isConnected() const { return socket_->isValid() && socket_->state() == QAbstractSocket::ConnectedState; } // Set whether we're sending pings or not (also resets ping timer) void MsnSocketTcp::setSendPings( bool sendPings ) { sendPings_ = sendPings; missedPings_ = 0; pingReceived_ = true; if ( sendPings_ ) { slotSendPing(); pingTimer_.stop(); pingTimer_.start( 30000 ); // 30s ping timer } else { pingTimer_.stop(); } } /** * @brief Called when the connection has been successfully opened */ 00303 void MsnSocketTcp::slotConnected() { // Stop the connection timer, we're done connectionTimer_.stop(); // Notify about the operation result emit connected(); } /** * @brief Called when the connection has been closed * * This method is not called when using disconnectFromServer(), because * its signals were blocked. */ 00320 void MsnSocketTcp::slotDisconnected() { // Stop the connection timer, in case the user had stopped the connection process connectionTimer_.stop(); // Notify about what's happening if( connected_ ) { emit disconnected(); } } /** * @brief Called when the server's IP address has been found * * This method is used to prolong the connection timer, when the DNS query for * the MSN Server has been successfully completed. */ 00340 void MsnSocketTcp::slotHostFound() { #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "Server host address found."; #endif // Restart the connection timer, to watch over the actual connection establishment. connectionTimer_.stop(); connectionTimer_.start( LOGIN_TIMER_CONNECTION ); } /** * @brief Called when the connection did not complete within a time limit * * This method is used to prolong the connection timer, when the DNS query for * the MSN Server has been successfully completed. */ 00359 void MsnSocketTcp::slotLoginFailed() { #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kDebug() << "Connection not successful within time limit."; #endif // Bounce back to the unified error handler to give the user a message emit error( i18n( "Connection time limit exceeded" ), MsnSocketBase::ERROR_CONNECTION_TIME_LIMIT ); } // Send a "ping" to the server void MsnSocketTcp::slotSendPing() { QString message; if ( pingReceived_ == false ) { missedPings_ ++; message += i18np( "1 ping lost", "%1 pings lost", missedPings_ ); emit statusMessage( message, false ); } else { emit statusMessage( i18n( "Connected" ), false ); } pingReceived_ = false; // Assume the connection was lost when 3 pings are missed. if ( missedPings_ == 3 ) { // Bounce back to the unified error handler to disconnect and give the user a message emit error( i18n( "The connection to the server was lost" ), MsnSocketBase::ERROR_DROP ); } writeData( "PNG\r\n" ); emit pingSent(); } /** * @brief Called when data is received from the server. * * This method parses the incoming raw data for the syntax used by the MSN protocol. * When a command is only partially received, it's buffered until the whole command is received by subsequent dataReceived() calls. * * Messages are then signalled back to MsnConnection which perform the parsing. * Only <code>PNG</code> commands are handled internally. */ 00414 void MsnSocketTcp::slotSocketDataReceived() { bool gotPayloadCommand = false; bool hasCompletePayload = false; QByteArray rawCommandLine; QByteArray payloadData; QStringList command; QString commandLine; while( true ) { // Read the next command if we're not waiting for a payload. if( nextPayloadSize_ == 0 ) { // Stop when socket didn't receive a full line yet (Qt does the buffering, thanks Qt4!) if( ! socket_->canReadLine() ) { break; } // Read the next line. rawCommandLine = socket_->readLine(); // Parse the command, split in separate fields commandLine = QString::fromUtf8( rawCommandLine.data(), rawCommandLine.size() ); command = commandLine.trimmed().split(" "); // See if it's a payload command or an error if( isErrorCommand( command[0] ) ) { // If the command contains a payload length parameter, also parse it gotPayloadCommand = ( command.size() > 2 ); } else { gotPayloadCommand = isPayloadCommand( command[0] ); } if( gotPayloadCommand ) { nextPayloadSize_ = command[ command.count() - 1 ].toInt(); // Last arg is payload length. nextPayloadCommand_ = command; } } // See if we can read the payload if( nextPayloadSize_ > 0 ) { // Check if the full payload is received. // Otherwise, just wait until "dataReceived" is called again. hasCompletePayload = ( socket_->bytesAvailable() >= nextPayloadSize_ ); if( ! hasCompletePayload ) { break; } // Full payload received. payloadData = socket_->read( nextPayloadSize_ ); // Copy state from previous loop if needed. if( ! gotPayloadCommand ) { gotPayloadCommand = true; command = nextPayloadCommand_; } // Reset state for next loop. nextPayloadSize_ = 0; } else { payloadData.clear(); } // Response from the internal ping timer. if( command[0] == "QNG" ) { pingReceived_ = true; missedPings_ = 0; } else // All other data { emit dataReceived( command, payloadData ); } } } /** * @brief Detect socket errors * * Closes the connection, and displays * the "could not connect to the MSN Messenger service." message box. * * @param errorCode The system error. */ 00513 void MsnSocketTcp::slotSocketError( QAbstractSocket::SocketError errorCode ) { #ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP kWarning() << "Received error" << errorCode << "from the socket" "(socket state=" << socket_->state() << ")."; #endif ErrorType type; // Find the most appropriate kind of error to report switch( errorCode ) { case QAbstractSocket::ConnectionRefusedError: case QAbstractSocket::HostNotFoundError: type = ERROR_CONNECTING; break; case QAbstractSocket::RemoteHostClosedError: case QAbstractSocket::NetworkError: type = ERROR_DROP; break; case QAbstractSocket::DatagramTooLargeError: case QAbstractSocket::UnknownSocketError: type = ERROR_DATA; break; case QAbstractSocket::ProxyConnectionRefusedError: case QAbstractSocket::ProxyConnectionClosedError: case QAbstractSocket::ProxyConnectionTimeoutError: case QAbstractSocket::ProxyNotFoundError: case QAbstractSocket::ProxyProtocolError: type = ERROR_PROXY; break; default: type = ERROR_UNKNOWN; break; } // Ignore errors when disconnected: avoids duplicate error dialogs if( connected_ ) { emit error( socket_->errorString(), type ); } } // Write data to the socket without conversions qint64 MsnSocketTcp::writeBinaryData( const QByteArray &data ) { qint64 noBytesWritten; if( ! isConnected() ) { kWarning() << "Attempted to write data to a disconnected socket, aborting."; return -1; } // Write to the socket noBytesWritten = socket_->write( data.data(), data.size() ); if ( noBytesWritten != data.size() ) { kWarning() << "Wanted to write " << data.size() << " bytes to the socket, wrote " << noBytesWritten << "."; } return noBytesWritten; } // Write data to the socket qint64 MsnSocketTcp::writeData( const QString &data ) { qint64 noBytesWritten; if( ! isConnected() ) { kWarning() << "Attempted to write data to a disconnected socket, aborting."; return -1; } // Write to the socket QByteArray binData = data.toUtf8(); noBytesWritten = socket_->write( binData.data(), binData.length() ); if( noBytesWritten != binData.size() ) { kWarning() << "Wanted to write " << binData.size() << " bytes to the socket, wrote " << noBytesWritten << "."; } return noBytesWritten; } #include "msnsockettcp.moc"