/*************************************************************************** applicationlist.cpp - description ------------------- begin : Mon 01 16 2005 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 "applicationlist.h" #include "../../contact/contactbase.h" #include "../../contact/msnobject.h" #include "../../currentaccount.h" #include "../../kmessdebug.h" #include "../extra/directconnectionpool.h" #include "../extra/msndirectconnection.h" #include "../chatmessage.h" #include "../mimemessage.h" #include "../msnswitchboardconnection.h" #include "../p2pmessage.h" #include "filetransfer.h" #include "filetransferp2p.h" #include "inktransferp2p.h" #include "mimeapplication.h" #include "msnobjecttransferp2p.h" #include "p2papplication.h" #include "unknownapplication.h" #include #ifdef KMESSDEBUG_APPLICATIONLIST #define KMESSDEBUG_APPLICATIONLIST_GENERAL #endif #define CEIL( val ) ( (int) (val) + 1) /** * @brief The constructor * * Initialize the application list for the given contact. * @param contactHandle The contact this application list is created for. */ ApplicationList::ApplicationList(const QString &contactHandle) : QObject() , addingConnections_(false) , contactHandle_(contactHandle.toLower()) , directConnection_(0) , directConnectionPool_(0) , pauseApplications_(false) , pendingConnectionInvitation_(false) { setObjectName("ApplicationList[" + contactHandle + "]"); // Configure timer connect( &sbReadyWriteTester_, SIGNAL(timeout()), this, SLOT(slotSwitchboardTestReadyWrite()) ); sbReadyWriteTester_.setSingleShot( true ); } /** * @brief The destructor. * * This method cleans up the direct connection and connection pool. * All MimeApplication and P2PApplication objects should be removed already. * If an application object still exists, it will be deleted directly. */ ApplicationList::~ApplicationList() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Starting removal. [handle=" << contactHandle_ << "]"; #endif // See if there are remaining MIME applications if( ! mimeApplications_.isEmpty() ) { kmWarning() << "still have " << mimeApplications_.count() << " active MimeApplication objects for " << contactHandle_ << "!"; // Remove all mime applications qDeleteAll( mimeApplications_ ); mimeApplications_.clear(); } // See if there are remaining P2P applications if( ! p2pApplications_.isEmpty() ) { kmWarning() << "still have " << p2pApplications_.count() << " active P2PApplication objects for " << contactHandle_ << "!"; // Remove all p2p applications qDeleteAll( p2pApplications_ ); p2pApplications_.clear(); } // Delete the connections directConnection_ = 0; // deleted by connection pool delete directConnectionPool_; directConnectionPool_ = 0; #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "DESTROYED. [handle=" << contactHandle_ << "]"; #endif } /** * @brief Abort all applications, the connection is closing or closed. * * This is called from contactLeftChat() and contactLeavingChat() to abort the * applications that can't operate without a switchboard connection (sometimes thats's only needed usability wise). * * If applications need to be aborted, this method returns true. * The applicationsAborted() signal will be fired when the aborting is completed. * * @param abortingConnection Connection that is closing. * @param userInitiated Indicates whether this action was initiated by the user (closing the chat window), * or the contact left the chat first. * @returns Returns whether the object started to abort applications. Returns false if there is nothing to abort. */ bool ApplicationList::abortApplications(const MsnSwitchboardConnection *abortingConnection, bool userInitiated) { // Check if there is nothing to do if( mimeApplications_.isEmpty() && p2pApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "No applications active."; #endif return false; } // If the contact is still in another private chat, all applications can use that one. // If the contact is only in a multi-chat, all applications need to close that don't support this. // If the contact left all chats, all applications need to be closed. bool hasMultiChats = false; // Check how many other chat sessions the contact still participates in. const ContactBase *contact = CurrentAccount::instance()->getContactByHandle(contactHandle_); if( ! KMESS_NULL(contact) ) { const QList switchboards = contact->getSwitchboardConnections(); foreach( const MsnSwitchboardConnection *connection, switchboards ) { if( connection != abortingConnection ) { if( connection->isExclusiveChatWithContact( contactHandle_ ) ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Contact is still in another private chat, not terminating applications."; #endif return (! abortingApplications_.isEmpty()); // in case the function was called before } else { hasMultiChats = true; } } } } // Check if any multi chats are found. #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL if( ! hasMultiChats ) { kmDebug() << "Contact left all chats, aborting applications."; } else { kmDebug() << "Contact left all private chats, aborting applications that don't support multi-chats."; } #endif // When aborting. verify whether the application is not being aborted here already. // Also check if the application was already started closing due a packet it received earlier. // This prevents nasty issues when a switchboard both reports the contact // will leave and actually left, or two switchboard report a contact has left. // First abort all mime-based applications. foreach( MimeApplication *app, mimeApplications_ ) { if( ! abortingApplications_.isEmpty() && ! abortingApplications_.contains( static_cast( app ) ) && ( app->isPrivateChatRequired() || ! hasMultiChats ) ) { abortingApplications_.append(app); if( ! app->isClosing() ) { if( userInitiated ) { app->userAborted(); // requests deletion after cancel message is delivered. } else { app->contactAborted(); // requests deletion. } } } } // Then abort all p2p-based applications. foreach( P2PApplication *app, p2pApplications_ ) { if( ! abortingApplications_.contains(app) && ( app->isPrivateChatRequired() || ! hasMultiChats ) ) { abortingApplications_.append(app); if( ! app->isClosing() ) { if( userInitiated ) { app->userAborted(); // requests deletion when bye message is delivered. } else { app->contactAborted(); // requests deletion. } } } } // The application may abort really quick and update the abortingApplications_ list directly from a signal. // In this case, the list is already emty, theirby also returning 'false'. #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Got " << abortingApplications_.count() << " applications left to abort."; #endif return (! abortingApplications_.isEmpty()); } /** * @brief Add a new MIME application to the list. * * After the application is added, it can be started with it's start() operation. * The putMsg() signal of the application will be connected to the sendMessage() slot, * the deleteMe() signal to the slotTerminateApplication() slot. * * @param application The configured application instance. */ void ApplicationList::addApplication(MimeApplication *application) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "connecting mime application signals."; #endif mimeApplications_.append( application ); connectApplication( application ); // The new-style P2P application manually call sendMessage() or put the message at the direct connection link. // The old-style MIME applications are directly connected here, so they don't need to have a reference to this object. connect( application, SIGNAL( putMsg(const MimeApplication*, const MimeMessage&) ), this, SLOT ( sendMessage(const MimeApplication*, const MimeMessage&) )); // Inform listeners about the new application (notably ChatMaster) emit newApplication( application ); } /** * @brief Add a new P2P application to the list. * * After the application is added, it can be started with it's start() operation. * The deleteMe() signal will be connected to the slotTerminateApplication() slot. * When an application needs to send a message, the sendMessage() methods determines whether the * message can be sent over the direct connection, or a switchboard connection is needed (fireing a putMsg() signal). * * @param application The configured application instance. */ void ApplicationList::addApplication(P2PApplication *application) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "connecting p2p application signals."; #endif p2pApplications_.append( application ); connectApplication( application ); // Inform listeners about the new application (notably ChatMaster) emit newApplication( application ); } /** * @brief Attempt to establish a direct connection at the given ipaddress/port. * * It uses the DirectConnectionPool class internally to handle multiple connection attempts. * When all connection attempts failed, a connectionFailed() signal will be sent. * * It's possible multiple P2PApplication instances require a direct connection. * Only the first application handles the connectionEstablished() signal. * The other applications wait until a connectionAuthorized() or connectionFailed() signal is fired. * * @param ipAddress The IP-Address to connect to. * @param port The port number to connect to. * @return Whether the socket could be created or not. * Returns false if the socket could not be created. */ bool ApplicationList::addConnection(const QString &ipAddress, const quint16 port) { // Avoid overwriting the current directConnection_ variable. if( directConnection_ != 0 ) { if( ! directConnection_->isConnected() ) { // Still had an old connection which went idle and closed, cleanup. if( ! KMESS_NULL(directConnectionPool_) ) { directConnectionPool_->verifyActiveConnection(); } directConnection_ = 0; } else { // Already connected, don't allow second connection. kmWarning() << "Refusing connection attempt, a connection has already been made " "(contact=" << contactHandle_ << " action=return)."; return 0; } } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "adding new connection attempt."; #endif // Avoid overwriting the current directConnection_ variable. if( directConnection_ != 0 ) { kmWarning() << "Refusing connection attempt, a connection has already been made " "(contact=" << contactHandle_ << " action=return)."; return 0; } // Init direct connection pool if( directConnectionPool_ == 0 ) { initializeDirectConnectionPool(); } // Init connection MsnDirectConnection *connection = new MsnDirectConnection(contactHandle_); // Connect connection connect(connection, SIGNAL( messageReceived(const QByteArray&) ), this, SLOT( slotGotDirectConnectionMessage(const QByteArray&) )); // Add to pool, and start return directConnectionPool_->addConnection(connection, ipAddress, port); } /** * @brief Attempt to establish a direct connection by listening at a random port. * * This method operates similar like the addConnection() method. * * @return The port number the connection is listening at. Returns zero if listening failed. */ int ApplicationList::addServerConnection() { // Avoid overwriting the current directConnection_ variable. if( directConnection_ != 0 ) { if( ! directConnection_->isConnected() ) { // Still had an old connection which went idle and closed, cleanup. if( ! KMESS_NULL(directConnectionPool_) ) { directConnectionPool_->verifyActiveConnection(); } directConnection_ = 0; } else { // Already connected, don't allow second connection. kmWarning() << "Refusing server connection attempt, a connection has already been made " "(contact=" << contactHandle_ << " action=return)."; return 0; } } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "adding new server socket."; #endif // Init direct connection pool if( directConnectionPool_ == 0 ) { initializeDirectConnectionPool(); } // Init connection MsnDirectConnection *connection = new MsnDirectConnection(contactHandle_); // Connect connection connect(connection, SIGNAL( messageReceived(const QByteArray&) ), this, SLOT( slotGotDirectConnectionMessage(const QByteArray&) )); // Add to pool, and start. return directConnectionPool_->addServerConnection(connection); } /** * @brief Connect the application object signals. * @param app The application object instance. */ void ApplicationList::connectApplication(Application *app) { connect( app, SIGNAL( deleteMe( Application* ) ), // Request for removal this, SLOT( slotTerminateApplication( Application* ) ) ); } /** * @brief Abort all applications, the contact is leaving a chat. * * This method is called by the MsnSwitchboardConnection to inform the contact is about the leave the chat. * * @param connection The connection the contact is leaving. * @param userInitiated Indicates whether this action was initiated by the user (closing the chat window), * or the contact left earlier. * @returns Returns whether the object started to abort applications. */ bool ApplicationList::contactLeavingChat(const MsnSwitchboardConnection *connection, bool userInitiated) { return abortApplications(connection, userInitiated); } /** * @brief Abort all applications, the contact left a chat. * * This method is called by the MsnSwitchboardConnection to inform the contact left the chat. * * @param userInitiated Indicates whether this action was initiated by the user (closing the chat window), * or the contact left earlier. * @returns Returns whether the object started to abort applications. */ bool ApplicationList::contactLeftChat(bool userInitiated) { return abortApplications(0, userInitiated); } /** * @brief Create the MIME application instance for the given invitation message. * * This extracts the Application-GUID field from the message and calls * createApplicationByGuid() to create the correct MimeApplication object. * When an invitation type is not recognized, it's rejected with sendMimeRejectMessage(). * * @param message The MimeMessage sent by the contact. * @returns The new application instance. */ MimeApplication * ApplicationList::createApplication(const MimeMessage &message) { MimeApplication *app = 0; QString appGuid( message.getValue("Application-GUID") ); // If no GUID is found: if( appGuid.isEmpty() ) { // Notify the user: kmWarning() << "MIME message can't be handled, " << "Application-GUID not found. (message dump follows)\n" << message.getFields(); return 0; } // Determine what application to start based on the Application-GUID app = createApplicationByGuid( appGuid ); if( app == 0 ) { MimeMessage reject; // TODO: create reject message. sendMimeRejectMessage( message.getValue("Invitation-Cookie"), true); return 0; } // Connect up the application #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Connecting MimeApplication signals"; #endif // Auto-add application addApplication(app); return app; } /** * @brief Create the P2P application instance for the given invitation message. * * This extracts the EUF-GUID field from the message and calls * createApplicationByEufGuid() to create the correct P2PApplication object. * When an invitation type is not recognized, a temporary * application will be initiated to reject and close the session properly. * * @param message The P2PMessage sent by the contact. * @returns The new application instance. */ P2PApplication * ApplicationList::createApplication(const P2PMessage &message) { #ifdef KMESSTEST KMESS_ASSERT( message.getSessionID() == 0 ); KMESS_ASSERT( message.getDataSize() > 0 ); #endif #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Creating new P2PApplication instance to parse the P2P message."; #endif P2PApplication *app = 0; // Extract the SLP data message QString slpMessage( QString::fromUtf8( message.getData(), (int) message.getDataSize() ) ); if(! slpMessage.startsWith("INVITE")) { // Not an invite message // this belongs to another session, but gotMessage() couldn't find it. // Likely a P2PApplication object terminated prematurely. QString preamble( slpMessage.left( qMin(slpMessage.indexOf(":"), 20) ) ); kmWarning() << "P2P message can't be handled, detected SLP message but no INVITE. " "P2PApplication already terminated? " "(slp-preamble=" << preamble << " contact=" << contactHandle_ << // TODO: get session ID from SLP body if available, it's not set in the headers. ")."; return 0; } // Find the EUF-GUID in the data QRegExp guidRE("EUF-GUID: (.+)\r\n"); guidRE.setMinimal(true); int guidPos = guidRE.indexIn(slpMessage); // If no EUF-GUID is found: if( guidPos == -1 ) { // Notify the user: kmWarning() << "P2P message can't be handled, " << "EUF-GUID not found. (message dump follows)\n" << slpMessage; return 0; } // Determine what application to start based on the EUF-GUID app = createApplicationByEufGuid( guidRE.cap(1) ); if( app == 0 ) { // Create an application instance which will reject the invitation correctly. app = new P2PApplication(this); // Application::contactStarted1 rejects correct invitatations automatically, // so don't set app->setMode(APP_MODE_ERROR_HANDLER) here..! } // Connect up the application #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Connecting P2PApplication signals"; #endif // Auto-add application addApplication(app); return app; } /** * @brief Factory method to create a MIME application instance for the given Application-GUID. * @param appGuid The Application-Guid field of the invitation message, identifies the application type to start. * @return The application object instance, or null if the Application-GUID is not recognized. */ MimeApplication * ApplicationList::createApplicationByGuid(const QString &appGuid) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Creating MimeApplication for GUID" << appGuid; #endif if( appGuid == FileTransfer::getAppId() ) { return new FileTransfer( contactHandle_ ); } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "No MimeApplication exists for this GUID!"; #endif return 0; } } /** * @brief Factory method to create a P2P application instance for the given EUF-GUID. * @param eufGuid The EUF-Guid field of the invitation message, identifies the application type to start. * @return The application object instance, or null if the EUF-GUID is not recognized. */ P2PApplication * ApplicationList::createApplicationByEufGuid(const QString &eufGuid) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Creating P2PApplication for GUID" << eufGuid; #endif if( eufGuid == MsnObjectTransferP2P::getAppId() ) { return new MsnObjectTransferP2P( this ); } else if( eufGuid == FileTransferP2P::getAppId() ) { return new FileTransferP2P( this ); } else if( eufGuid == UnknownApplication::getGamesAppId() || eufGuid == UnknownApplication::getMeetingAppId() || eufGuid == UnknownApplication::getRemoteDesktopAppId() || eufGuid == UnknownApplication::getWebcamPullAppId() || eufGuid == UnknownApplication::getWebcamPushAppId() || eufGuid == UnknownApplication::getVoiceAppId() ) { return new UnknownApplication( this, eufGuid ); } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "No P2PApplication exists for this GUID!"; #endif return 0; } } /** * @brief Find the P2P application which should handle the ACK message. * @param ackMessage The P2P ACK message. * @returns The application instance, or null if the object can't be found. */ P2PApplication * ApplicationList::getApplicationByAckData(const P2PMessage &ackMessage) const { unsigned long ackUniqueID = ackMessage.getAckUniqueID(); foreach( P2PApplication *app, p2pApplications_ ) { // The application may have multiple messages unacked, see whether the ACK matches. if( app->hasUnAckedMessage(ackMessage) ) { // Test whether the match was only partial (error displayed here because P2PApplication // calls hasUnAckedMessage() later again, causing the message to appear twice) if( ackUniqueID == 0 ) { // HACK: added for GAIM 1.5 (does not return UniqueID) kmWarning() << "Got P2P message with matching ackSessionID and no ackUniqueID set " << "(delivering anyway, contact=" << contactHandle_ << ")!"; } return app; } } return 0; } /** * @brief Find the P2P application which handles the session for the given Call-ID. * @param callID The CallID value of the application session. * @returns The application instance, or null if the object can't be found. */ P2PApplication * ApplicationList::getApplicationByCallId(const QString &callID) const { foreach( P2PApplication *app, p2pApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Demultiplexing P2P message" << " (CallID " << app->getCallID() << " == " << callID << " ?)"; #endif if( app->getCallID() == callID ) { // Found it, return reference return app; } } return 0; } /** * @brief Find the application with uses the given cookie. * * This is used to deliver accept/cancel commands from the chat window. * If the cookie exists, the method either returns a MimeApplication or P2PApplication instance. * * @param cookie The cookie string of the application. * @returns The application instance, or null if the object can't be found. */ Application * ApplicationList::getApplicationByCookie(const QString &cookie) const { // First see if there is a p2p application with the cookie. foreach( P2PApplication *app, p2pApplications_ ) { if( app->getCookie() == cookie ) { // Found it, return reference. return app; } } // Also look for a mime application with the cookie. foreach( MimeApplication *app, mimeApplications_ ) { if ( app->getCookie() == cookie ) { // Found it, return reference. return app; } } return 0; } /** * @brief Find the P2P application which handled the previous message fragment from the contact. * @param messageID The identifier of the last received message. * @returns The application instance, or null if the object can't be found. */ P2PApplication * ApplicationList::getApplicationByLastMessage(unsigned long messageID) const { foreach( P2PApplication *app, p2pApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL unsigned long debugMsgId = app->getLastContactMessageID(); kmDebug() << "Demultiplexing P2P message" << " (fragment MessageID " << debugMsgId << " == " << messageID << " ?)"; #endif if( app->getLastContactMessageID() == messageID ) { // HACK: Not tested for "getLastContactAckMessageID() == ackMessageID" to support broken implementations. // Found it, save reference return app; } } return 0; } /** * @brief Find the P2P application which authorizes the transfer for the given nonce. * @param nonce The Nonce value of the application session. * @returns The application instance, or null if the object can't be found. */ P2PApplication * ApplicationList::getApplicationByNonce(const QString &nonce) const { foreach( P2PApplication *app, p2pApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Demultiplexing P2P message" << " (Nonce " << app->getNonce() << " == " << nonce << " ?)"; #endif if( app->getNonce() == nonce ) { // Found it, return reference return app; } } return 0; } /** * @brief Find the P2P application identified with the given session ID. * @param sessionID The identifier of the application. * @returns The application instance, or null if the object can't be found. */ P2PApplication * ApplicationList::getApplicationBySessionId(unsigned long sessionID) const { foreach( P2PApplication *app, p2pApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Demultiplexing P2P message" << " (SessionID " << app->getSessionID() << " == " << sessionID << " ?)"; #endif if( app->getSessionID() == sessionID ) { // Found the session return app; } } return 0; } /** * @brief Return the handle of the contact this class manages. * @returns The contact handle. */ const QString & ApplicationList::getContactHandle() const { return contactHandle_; } /** * @brief Return the direct connection, if available. * @returns The direct connection object. */ MsnDirectConnection * ApplicationList::getDirectConnection() const { return directConnection_; } /** * @brief Deliver a received MIME message to the correct application instance. * * This method is called when a message was received by the MsnSwitchboardConnection class. * It examines the various fields of the message to deliver the message to the correct MimeApplication instance. * If the message contains an invitation, a new application instance will be created with createApplication(). * A warning will be displayed at the console when an application instance could not be found. * * @param message The received mime message. */ void ApplicationList::gotMessage(const MimeMessage &message) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Parsing Mime appliation message."; message.print(); #endif Application *app = 0; // If this is an invite message, an application need to be created if( message.getValue("Invitation-Command") == "INVITE" ) { // Create the application based on the type app = createApplication( message ); // Check if Application could not be created for some reason if( app == 0 ) { return; } } else { // Find the application based on the app cookie app = getApplicationByCookie( message.getValue("Invitation-Cookie") ); if( app == 0 ) { kmWarning() << "Unable to handle MIME message, " << "Invitation-Cookie not found (contact=" << contactHandle_ << ")."; return; } } // Last check before casting the object. if( ! app->inherits("MimeApplication") ) { kmWarning() << "found application is not a mime application!"; return; } // Deliver the message static_cast(app)->gotMessage( message ); } /** * @brief Deliver a received P2P message to the correct application instance. * * This method is called when a message was received by * the MsnSwitchboardConnection class (via the ChatMaster) or MsnDirectConnection class. * It examines the various fields of the message to deliver the message to the correct P2PApplication instance. * If the message contains an invitation, a new application instance will be created with createApplication(). * A warning will be displayed at the console when an application instance could not be found. * * @param message The received p2p message. */ void ApplicationList::gotMessage(const P2PMessage &message) { // Parse the message #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Parsing received P2P message:" << " sid=" << message.getSessionID() << " mid=" << message.getMessageID() << " ackSid=" << message.getAckSessionID() << " ackUid=" << message.getAckUniqueID() << " flags=0x" << QString::number(message.getFlags(), 16) << " size=" << message.getDataSize() << " offset=" << message.getDataOffset() // also useful for debugging! << " total=" << message.getTotalSize() << " no.sessions=" << p2pApplications_.count(); #endif P2PApplication *p2pApplication = 0; unsigned long sessionID = message.getSessionID(); QString slpMessage; // Session ID is 0 if clients negotiate (for both INVITE and BYE messages!) // Otherwise it's set. if( sessionID != 0 ) { // The message has a SessionID set p2pApplication = getApplicationBySessionId( sessionID ); // If there is no session for this ID, there are still some special cases. if(p2pApplication == 0) { switch( sessionID ) { case InkTransferP2P::SESSION_ID: #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Found reserved session ID for Ink transfers, creating InkTransferP2P instance."; #endif // Ink messages have a fixed session ID. p2pApplication = new InkTransferP2P( this ); addApplication( p2pApplication ); break; // MSN 6 also used this system for Winks. default: // Normal P2P session -> notify the user if the message was invalid. (send error-response later) kmWarning() << "Unable to handle P2P message, Session-ID " << sessionID << " not found."; } } } else if( message.getFlags() != 0 && message.getFlags() != P2PMessage::MSN_FLAG_OBJECT_DATA // WLM2009 uses 0x1000000 instead of 0x00. && message.getDataSize() == 0 ) { // This can also be a connection handshake, even though it shouldn't appear at the switchboard. // That occurs when the connection was closed before that packet could be sent. if( message.isConnectionHandshake() ) { kmWarning() << "Received a direct connection handshake message at the switchboard " "(contact=" << contactHandle_ << " action=return)."; return; } // It's some kind of low-level control message, typically without data/payload. // Both the ACK-Message-ID and ACK-Unique-ID fields // should resolve the orginal session it was sent from. p2pApplication = getApplicationByAckData(message); // No existing session found..? if(p2pApplication == 0) { // NOTE: if flags==64 (0x40), it it's possible this is actually a bug from MSN7.0. kmWarning() << "P2P message can't be handled," " no match for ackSessionID " << message.getAckSessionID() << " or ackUniqueID " << message.getAckUniqueID() << " found."; } } else if( message.getFlags() == 0 && message.getDataSize() == 0 && message.getAckUniqueID() == 0 ) { // HACK: added for bot Bot2k3 4.1 (sends first ack without UniqueID, flags or any other context) kmWarning() << "SwitchBoard P2P message has no ACK fields set at all, assuming base identifier message!"; } else if( message.isSlpData() ) { // It's a negotiation/MSNSLP message if( message.getDataOffset() > 0 ) { // This is another fragment of the SLP message. // Find the application which received the previous fragment too. p2pApplication = getApplicationByLastMessage( message.getMessageID() ); } else { // No offset, start of SLP message // Initialize Call and session ID regexps QRegExp callRE("Call-ID: (.+)\r\n"); // match everything because MSN6 accepts anything. callRE.setMinimal(true); // Find the call ID // Note this assumes that a Call-ID will be found in the first fragment. slpMessage = QString::fromUtf8( message.getData(), (int)message.getDataSize() ); int callPos = callRE.indexIn( slpMessage ); if( callPos == -1 ) { kmWarning() << "Unable to handle P2P message, CallID field not found."; } else { QString callID( callRE.cap(1) ); // See if it's a null-guid. // This happens for the out-of-band SLP "application/x-msnmsgr-transdestaddrupdate" and "application/x-msnmsgr-transudpswitch" messages. if( callID == "{00000000-0000-0000-0000-000000000000}" ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Found null Call-ID, using Session-ID to deliver the message."; #endif // HACK use the SessionID from the SLP body to route the message. // This is a bit like reading the "HTTP Host" field to route an IP packet, but MSNP2P leaves us with no other choice. QRegExp sessionRE("SessionID: ([0-9]+)\r\n"); sessionRE.setMinimal( true ); long sessionPos = sessionRE.indexIn( slpMessage ); if( sessionPos == -1 ) { kmWarning() << "Unable to handle P2P message, found null-guid CallID but no SessionID instead."; } else { unsigned long sessionID = sessionRE.cap(1).toLong(); if( sessionID != 0 ) { p2pApplication = getApplicationBySessionId( sessionID ); } else { // Also observed in some cases. #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Also found zero Session-ID, will create an empty application instance to handle the message."; #endif // Create it here so there won't be any message dumps. // We already know what those look like. p2pApplication = new P2PApplication(this); p2pApplication->setMode( Application::APP_MODE_ERROR_HANDLER ); addApplication( p2pApplication ); } } } else { // Normal call-id, normal operation. // first try to find an existing session searching by Call ID. (for 200 OK and BYE messages) p2pApplication = getApplicationByCallId( callRE.cap(1) ); // No existing applications have been found // Initialize an application class to handle the invitation. if( p2pApplication == 0 ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "No existing P2P session found for the MSNSLP message, " << "must be a new session invitation."; #endif p2pApplication = createApplication( message ); } } } } } else { // Didn't reconize the flag type. kmWarning() << "P2P message can't be handled, unknown flag type found."; if( message.getFlags() == 0 ) { // HACK: added for broken clients who sent an empty message with a zero flag (Bot2K3 v4.1) } } // If the application was found, deliver the message if( p2pApplication != 0 ) { // Deliver the message p2pApplication->gotMessage( message ); } else { // Force logging all unknown messages, to help improving the protocol knowledge // This is not hidden, because this information is essential for debugging user problems. kmWarning() << "A message could not be delivered to an existing P2P application " "(see previous warnings for details," " sid=" << message.getSessionID() << " flags=0x" << QString::number(message.getFlags(), 16) << " size=" << message.getDataSize() << " contact=" << contactHandle_ << ")."; // Also dump the SLP message if it's there. if( ! slpMessage.isEmpty() && message.getSessionID() == 0 && message.getDataSize() > 0 ) { kmDebug() << "Dumping SLP message payload:\n" << slpMessage << "\n"; } // If we didn't find the application, the message had bad fields // or we don't support the invitation type: if( message.getFlags() != 0 && message.getFlags() != P2PMessage::MSN_FLAG_OBJECT_DATA // WLM2009 uses 0x1000000 instead of 0x00. && message.getDataSize() == 0 ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Not sending a P2P Error response for error message."; #endif } else { kmDebug() << "Creating temporary P2PApplication instance to send error response."; // Create a default application to send the correct error response (also for unknown invitation types) // Application auto-deletes itself. p2pApplication = new P2PApplication(this); p2pApplication->setMode( Application::APP_MODE_ERROR_HANDLER ); addApplication( p2pApplication ); p2pApplication->gotMessage( message ); } } } /** * @brief Return whether the direct connection is connected and authorized. * If a direct connection is not authorized, it's not able to send the data transfer. */ bool ApplicationList::hasAuthorizedDirectConnection() const { return ( directConnection_ != 0 && directConnection_->isAuthorized() && directConnection_->isConnected() ); } /** * @brief Return whether there are applications which need to send data. */ bool ApplicationList::hasDataSendingApplications() const { return ! dataSendingApplications_.isEmpty(); } /** * @brief Return whether there is an active direct connection. * * If the direct connection is no longer connected, it returns false. * Methods like sendMessage() automatically clean up a disconnected instance. * * Note that the connection might not be authorized. Such connection * can only be used to send the direct connection handshake. * use hasAuthorizedDirectConnection() if the connection needs to * be able to transfer data too. * * @returns The direct connection if it's available *and* connected. */ bool ApplicationList::hasDirectConnection() const { return ( directConnection_ != 0 && directConnection_->isConnected() ); } /** * @brief Return whether an MSN object transfer for the given msn object is already running. * * This is called by ChatMaster to avoid multiple invitations for the same MsnObject (display picture, wink, emoticon). * * @param msnObject The requested msn object. * @return Whether the msn object is being transferred already. */ bool ApplicationList::hasMsnObjectTransfer( const MsnObject &msnObject ) const { foreach( P2PApplication *app, p2pApplications_ ) { MsnObjectTransferP2P *transferApp = qobject_cast( app ); if( transferApp != 0 ) { if( transferApp->getMsnObject().getDataHash() == msnObject.getDataHash() ) { return true; } } } return false; } /** * @brief Return whether the transfer invitation has been sent. * @return Whether there is a pending connection invitation. */ bool ApplicationList::hasPendingConnectionInvitation() const { return pendingConnectionInvitation_; } /** * @brief Return whether there are pending connection attempts. * @returns Returns true when there are pending connection attempts. */ bool ApplicationList::hasPendingConnections() const { return (directConnectionPool_ != 0 && directConnectionPool_->hasPendingConnections()); } /** * @brief Create the direct connection pool. * * This pool handles all connection attempts and returns * the connection that could be established. */ void ApplicationList::initializeDirectConnectionPool() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Initializing direct connection pool."; #endif #ifdef KMESSTEST KMESS_ASSERT( directConnectionPool_ == 0 ); #endif if( directConnectionPool_ == 0 ) { // The direct connection pool handles all connection attempts, // making it appear as if we only need to handle one connection. directConnectionPool_ = new DirectConnectionPool(); connect(directConnectionPool_, SIGNAL( activeConnectionAuthorized() ), // The connection was authorized this, SLOT( slotActiveConnectionAuthorized() )); connect(directConnectionPool_, SIGNAL( activeConnectionClosed() ), // The active connection was closed this, SLOT( slotActiveConnectionClosed() )); connect(directConnectionPool_, SIGNAL( connectionEstablished() ), // The connection was established this, SLOT( slotConnectionEstablished() )); connect(directConnectionPool_, SIGNAL( allConnectionsFailed() ), // All connection attempts failed. this, SLOT( slotAllConnectionsFailed() )); } } /** * @brief Return whether the application list does not have any applications. * Note that there could also be a direct connection. * @return Whether there are no MimeApplication or P2PAppliation objects left. */ bool ApplicationList::isEmpty() const { return ( mimeApplications_.isEmpty() && p2pApplications_.isEmpty() ); } /** * @brief Pause all applications, avoid flooding the switchboard message queue. * * This is called when the send queue of all switchboards is full. * Applications are paused to prevent flooding of the queue. * The resumeApplications() method is called when * a switchboard is ready to send more messages. */ void ApplicationList::pauseApplications() { // This only affects P2Papplications now, mime applications don't send many messages. pauseApplications_ = true; // setting this causes slotSwitchboardTestReadyWrite() to halt. } /** * @brief Register an application which is sending a file. * * This causes the application to receive requests to send new parts, * so the file transfer speed remains optimal for low and high speed network links. */ void ApplicationList::registerDataSendingApplication( P2PApplicationBase *application ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "adding application to the list of active outgoing transfers."; #endif // See if it's the first app. bool isFirstApp = ( dataSendingApplications_.isEmpty() ); // Add to list. dataSendingApplications_.append( application ); // Make sure the application is polled frequently for the next data packets. // For the switchboard: locally, frequent calls are made to slotSwitchboardTestReadyWrite(). // For the direct connection: each time the socket is ready, slotConnectionReadyWrite() is called. if( ! hasAuthorizedDirectConnection() ) { if( ! sbReadyWriteTester_.isActive() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no direct connection available, starting switchboard write handler instead."; #endif // Start the write handler for the switchboard. sbReadyWriteTester_.start( 10 ); } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no direct connection available, but switchboard write handler is already active."; #endif } } else { // Attach the the write handler of the direct connection. // This is only needed once for the first application. // Still check if the handler is connected already, avoid race conditions with slotSwitchboardTestReadyWrite() if( ! directConnection_->isWriteHandlerConnected() ) { if( isFirstApp ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "first application added, " << "starting connection write handler."; #endif } else { kmWarning() << "there are already applications, but no write handler connected yet."; } // Connect the write handler. directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) ); } } } /** * @brief Resume all applications, allowing messages to be sent again. * * This method is called when a MsnSwitchboardConnection is ready to send more messages. * @param isPrivateChat Whether the available switchboard is a private chat, * otherwise only multi-chat capable applications may resume. */ void ApplicationList::resumeApplications(bool /*isPrivateChat*/) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "resuming applications."; #endif #ifdef KMESSTEST KMESS_ASSERT( pauseApplications_ ); #endif // Allow slotSwitchboardTestReadyWrite() again. pauseApplications_ = false; // See if there are still applications that need to write data. if( dataSendingApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no action taken because no appliations need to transfer data."; #endif return; } // Restart the handler if( ! hasAuthorizedDirectConnection() ) { if( sbReadyWriteTester_.isActive() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "switchboard write handler is already active, " << "no special action needed."; #endif } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "restarting switchboard write handler."; #endif // No direct connection (as expected), restart switchboard write handler. sbReadyWriteTester_.start( 10 ); } } else { // A direct connection is available. // See if the direct connection has a write handler which triggers slotConnectionReadyWrite() if( directConnection_->isWriteHandlerConnected() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "direct connection is available with write handler, " << "no special action needed."; #endif } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "direct connection is available, " << "reconnecting write handler.."; #endif // Connect the write handler to resume! directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) ); } } } /** * @brief Send a message for a MIME application. * @param source The MimeApplication responsable for sending the message. * @param messageData The message body. */ void ApplicationList::sendMessage(const MimeApplication *source, const MimeMessage &messageData) { QString messageBody( messageData.getFields() ); #ifdef KMESSTEST // Check whether a QByteArray is passed. gcc is able to create the cast // because MimeMessage() has a constructor which accepts a QByteArray. KMESS_ASSERT( messageBody[0].isLetter() ); KMESS_ASSERT( messageData.getBinaryBody().size() == 0 ); #endif // Deliver the message over the switchboard in a mime message MimeMessage messageWrapper; messageWrapper.addField("MIME-Version", "1.0"); messageWrapper.addField("Content-Type", "text/x-msmsgsinvite; charset=UTF-8"); messageWrapper.setBody( messageBody ); emit putMsg( messageWrapper, contactHandle_, (source == 0 || source->isPrivateChatRequired()) ); } /** * @brief Send a P2P message over one of the available connections/bridges. * * If a direct connection is available, it will be used to transfer the message. * It's possible this connection is congested, and would block. In that case * false is returned. * * If a direct connection is not available, the message will be delivered * to a MsnSwitchboardConnection instance by the ChatMaster (connected to the putMsg() signal). * Before the P2PMessage can be handled by a switchboard it's wrapped in a MimeMessage, * as the switchboard sends all messages in MIME messages. * * @param source The P2PApplication responsable for sending the message. * @param header The message headers (48 bytes). * @param messageData The message body / payload. This field is optional. * @param footerCode The footer code (4 bytes), appended to messages which are sent over the switchboard. * @return Whether the message can be sent. If the socket would block, false is returned. * This only occurs for direct connections connections. */ bool ApplicationList::sendMessage(const P2PApplicationBase *source, const QByteArray &header, const QByteArray &messageData, uint footerCode) { // Verify whether a direct connection exists to send the message. // It needs to be connected and authorized. if( directConnection_ != 0 ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "there is a direct connection object, " "test if the socket is still connected and not timed out."; #endif if( ! KMESS_NULL(directConnectionPool_) ) { // verify connection, deletes closed connection. if( ! directConnectionPool_->verifyActiveConnection() ) { // not valid, also unset here. directConnection_ = 0; } } } // Deliver over a direct connection or switchboard session. // Now also test if the connection is authorized, before sending data over it. if( directConnection_ != 0 && directConnection_->isAuthorized() ) { // Deliver the message over the direct connection. // The footer will not be appended to the message. QByteArray p2pMessage = header + messageData; bool success = directConnection_->sendMessage( p2pMessage ); if( success ) { return true; } else if( directConnection_->hasTemporaryWriteError() ) { kmWarning() << "message could not be fully sent, " "but the remaining part is in the send buffer."; return true; } else { #ifdef KMESSTEST KMESS_ASSERT( directConnection_->hasLastWriteFailed() ); #endif kmWarning() << "P2P message could not be sent over the direct connection, " "using switchboard instead (contact=" << contactHandle_ << ")."; // NOTE: The direct connection will still appear as connected. // Next events are not processed yet. That's why there is a check for hasLastWriteFailed() below. } // NOTE: This method does not return false anymore on socket write errors. // Instead, it relies on the DirectConnectionBase class to send the remaining data if needed. } // Sending message over switchboard. Encapsulate in a MimeMessage. // At the end of the message we need to add an footer of 4 bytes. // This is little endian format, so we can't use P2PMessage::insertBytes() // // Possible values: // 0x00 session // 0x01 for display/emoticon // 0x02 filetransfer, or start menu code char footer[4]; footer[0] = (char) ((footerCode >> 24) & 0xFF); footer[1] = (char) ((footerCode >> 16) & 0xFF); footer[2] = (char) ((footerCode >> 8) & 0xFF); footer[3] = (char) ( footerCode & 0xFF); // Deliver the P2P message over the switchboard in a mime message MimeMessage messageWrapper; messageWrapper.addField("MIME-Version", "1.0"); messageWrapper.addField("Content-Type", "application/x-msnmsgrp2p"); messageWrapper.addField("P2P-Dest", getContactHandle()); messageWrapper.setBinaryBody(header, messageData, footer); emit putMsg( messageWrapper, contactHandle_, (source == 0 || source->isPrivateChatRequired()) ); return true; } /** * @brief Reject an invitation for a mime-application. * @param invitationCookie The cookie used in the invitation message. * @param notInstalled Whether the invitation was rejected because it is not supported by KMess. */ void ApplicationList::sendMimeRejectMessage( const QString &invitationCookie, bool notInstalled ) { // Create mime message MimeMessage message; message.addField( "Invitation-Command", "CANCEL" ); message.addField( "Invitation-Cookie", invitationCookie ); message.addField( "Cancel-Code", (notInstalled ? "REJECT_NOT_INSTALLED" : "CANCEL") ); // Forward to send sendMessage(0, message); } /** * @brief Set an application is adding connections, don't send "all failed" signal. */ void ApplicationList::setAddingConnections(bool state) { addingConnections_ = state; } /** * @brief Set whether the transfer invitation has been sent. * * This property is set by the P2PApplication class, to prevent * other P2PApplication classes from sending the same invitation. * The other P2PApplication classes wait until the first class establishes * the connection, aborts it, or the connection attempts failed. * * The state is automatically reset when a connection is established * or all connection attempts failed. If a class sets the state back to false * while a connection was pending, a connectionFailed() signal will be * fired so other P2P applications also continue to transfer data over the switchboard. * * @param state True if the transfer invitation has been sent, false when it was redrawn. */ void ApplicationList::setPendingConnectionInvitation(bool state) { // Detect first bool failed = ( ! state && pendingConnectionInvitation_ ); // set state pendingConnectionInvitation_ = state; // Emulate the connectionFailed() signal. The connection could not be made, // but it wasn't even attempted. if( failed ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Pending connection invitation was aborted by P2PApplication, " << "informing application no direct connection is possible."; #endif emit connectionFailed(); } } /** * @brief The direct connection is authorized. * * At this point, the direct connection can be re-used by other P2P applications. */ void ApplicationList::slotActiveConnectionAuthorized() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Direct connection with '" << contactHandle_ << "' authorized."; #endif #ifdef KMESSTEST KMESS_ASSERT( directConnection_ != 0 ); #endif // Inform *all* awaiting applications they can start to use the connection. // Each application will start their data transfer now by calling registerDataSendingApplication() emit connectionAuthorized(); } /** * @brief The direct connection was closed. * * It resets the internal state and fires a connectionFailed() or connectionClosed() signal to other classes. */ void ApplicationList::slotActiveConnectionClosed() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "The direct connection with '" << contactHandle_ << "' was closed."; #endif // Reset state variable. pendingConnectionInvitation_ = false; // Detect whether the connection was already set here. bool hadConnectionBefore = ( directConnection_ != 0 ); // Reset the pointers first. // No deletion needed, the connection will be removed by the DirectConnectionPool later directConnection_ = 0; // If the pool was still active, it can be closed now. if( directConnectionPool_ != 0 ) { directConnectionPool_->deleteLater(); directConnectionPool_ = 0; } // Notify the listeners if( ! hadConnectionBefore ) { // Active connection closed, but no pointer known. // It must be reset in slotGotDirectConnectionMessage() when the nonce was invalid. #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "The current connection was closed prematurely, " << "signaling instead that the connection failed."; #endif // Notify the connection failed. // Applications start their transfer over the switchboard. emit connectionFailed(); } else { // Also forward to our listeners. emit connectionClosed(); // If there are still data sending applications, resume transfer over the switchboard. if( ! dataSendingApplications_.isEmpty() ) { // Build a list of sessions for debugging QString activeSessions; foreach( P2PApplicationBase *app, dataSendingApplications_ ) { if( ! activeSessions.isEmpty() ) { activeSessions += ","; } activeSessions += QString::number( app->getSessionID() ); } // Give users some insight why transfer becomes slow. kmWarning() << "The direct connection closed " "while there are active file transfers over the connection. " "Switching sessions to slow indirect transfer " "(sessions=" << activeSessions << " contact=" << contactHandle_ << ")."; #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "direct connection closed, " << "starting switchboard write handler to continue transfers."; #endif // No direct connection (as expected), restart switchboard write handler. sbReadyWriteTester_.start( 10 ); } } } /** * @brief All direct connection attempts failed. * * This resets the internal state and fires the connectionFailed() signal. */ void ApplicationList::slotAllConnectionsFailed() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "The direct connection with '" << contactHandle_ << "' could not be made."; #endif #ifdef KMESSTEST KMESS_ASSERT( directConnectionPool_ != 0 ); KMESS_ASSERT( ! directConnectionPool_->hasActiveConnection() ); KMESS_ASSERT( ! directConnectionPool_->hasPendingConnections() ); #endif // It's possible a connection fails directly before all addConnection() calls are made. // Don't notify applications to revert the transfer. // This should also avoid hangs at the "connect()" call. if( directConnection_ == 0 && addingConnections_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "No connections left, but still adding connections. Ignoring event."; #endif return; } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL if( directConnection_ == 0 ) { kmDebug() << "No remaining direct connections left"; } else { kmDebug() << "The current connection was closed prematurely"; } #endif // Reset state variable. pendingConnectionInvitation_ = false; directConnection_ = 0; // somehow it could be != 0 with timeout events. // If the pool was still active, it can be closed now. if( directConnectionPool_ != 0 ) { directConnectionPool_->deleteLater(); directConnectionPool_ = 0; } // Also forward to our listeners. // This will cause the application to initiate the transfer. // The messages will be sent over the switchboard instead as fallback. emit connectionFailed(); } /** * @brief The direct connection is established. * * This sets the internal state and fires a connectionEstablished() signal. */ void ApplicationList::slotConnectionEstablished() { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Direct connection with '" << contactHandle_ << "' established."; #endif #ifdef KMESSTEST KMESS_ASSERT( directConnection_ == 0 ); KMESS_ASSERT( directConnectionPool_ != 0 ); #endif // Reset state variable. pendingConnectionInvitation_ = false; // Get active connection. directConnection_ = static_cast( directConnectionPool_->getActiveConnection() ); if(KMESS_NULL(directConnection_)) return; // Inform the requested application it can start to initialize the connection. // This application will authenticate the connection itself, // and slotActiveConnectionAuthorized() will be called afterwards. // If the authentication fails, slotAllConnectionsFailed() will be called instead. emit connectionEstablished(); } /** * @brief The direct connection is ready to send more data. */ void ApplicationList::slotConnectionReadyWrite() { if( KMESS_NULL(directConnection_) ) return; #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "The direct connection is ready to send more data, " << "notifying applications."; #endif // Disconnect the event handler if there are no longer applications that need to transfer data. if( dataSendingApplications_.isEmpty() || p2pApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no active outgoing transfers left, " << "disconnecting write handler."; #endif directConnection_->disconnectWriteHandler( this, SLOT(slotConnectionReadyWrite()) ); return; } // Determine the number of preferred fragments to send. // By default 3 for a direct connection, unless there are more parallel transfers. int preferredFragments = CEIL( qMin( 3, 4 / dataSendingApplications_.count() ) ); // Start notifying. bool writeSuccess = true; foreach( P2PApplicationBase *app, dataSendingApplications_ ) { // Tell the application to send the next file parts. writeSuccess = app->sendNextDataParts( preferredFragments ); // Avoid freezes, don't call the app again if will fail every time. if( ! writeSuccess ) { unregisterDataSendingApplication( app ); } } } /** * @brief A message was received from the direct connection. * * The message will be verified and forwarded to the correct P2PApplication instance (using gotMessage()). * If it's a handshake message containing a Nonce, it returns the message to the application * which generated the Nonce. If no application was found, the connection will be dropped * to indicate the Nonce was invalid. * * @param messageData The received message data. */ void ApplicationList::slotGotDirectConnectionMessage(const QByteArray &messageData) { #ifdef KMESSTEST KMESS_ASSERT( directConnection_ != 0 ); #endif // Last check to know this is really a P2P message. if(messageData.size() < 48) { kmWarning() << "Received an unknown message from the " << " direct-connection-link (message dump follows).\n" << messageData.data(); return; } // Parse the message P2PMessage message(messageData); #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Received data message from the direct connection link" << " (flags=0x" << QString::number(message.getFlags(), 16) << ")."; #endif // The nonce/handshake message doesn't have ack fields, so parse it here. if( message.isConnectionHandshake() ) { P2PApplication *app = getApplicationByNonce( message.getNonce() ); if( app == 0 ) { // Invalid nonce! Drop connection kmWarning() << "Contact sent invalid nonce or session was already terminated, " << "closing direct connection."; #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kmDebug() << "Closing connection, moving transfer to switchboard."; #endif #ifdef KMESSTEST KMESS_ASSERT( sender() != 0 ); KMESS_ASSERT( static_cast(sender()) == directConnectionPool_->getActiveConnection() ); #endif // Close the connection. directConnection_ = 0; // used for slotConnectionClosed() directConnectionPool_->getActiveConnection()->closeConnection(); // DirectConnectionPool deletes it. directConnectionPool_->deleteLater(); directConnectionPool_ = 0; } else { // Nonce is valid. app->gotMessage( message ); } } else { // Normal message // Deliver the message to the correct p2p application. gotMessage(message); } } /** * @brief The switchboard is ready to send more data. * * This method works very much like slotConnectionReadyWrite(), * except the source of the signal differs. */ void ApplicationList::slotSwitchboardTestReadyWrite() { // Test if the switchboard requested a pause, // meaning it's no longer ready. if( pauseApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "switchboard requested pause, " << "postponing action."; #endif return; } // Disconnect the event handler if there are no longer applications that need to transfer data. if( dataSendingApplications_.isEmpty() || p2pApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no active outgoing transfers left, " << "disconnecting write handler."; #endif return; } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "a switchboard is ready to send more data, " << "notifying applications."; #endif // Start notifying. bool writeSuccess = true; foreach( P2PApplicationBase *app, dataSendingApplications_ ) { // Tell the application to send the next file parts. writeSuccess = app->sendNextDataParts( 1 ); // Avoid freezes, don't call the app again if will fail every time. if( ! writeSuccess ) { unregisterDataSendingApplication( app ); } } // Find out if the applications are done now. if( dataSendingApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "all applications are done."; #endif return; } // See if we can switch to a Direct connection instead. // // The "last write failed" check is required, because isConnected() still returns true, // even if writeBlock() failed with a "broken pipe" error, and used the SB instead. The stack becomes: // this->slotSwitchboardTestReadyWrite() -> p2papp->sendNextDataParts() -> this->sendMessage() -> dc->writeBlock() // // Also look out for direct connections which did have a temporary write error, // but have the remaining message buffered to send later. bool switchToDc = ( hasAuthorizedDirectConnection() && ( ! directConnection_->hasLastWriteFailed() || directConnection_->hasTemporaryWriteError() ) ); #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL if( hasAuthorizedDirectConnection() ) { if( directConnection_->hasLastWriteFailed() ) { kmDebug() << "A direct connection became available, " "but had a error while writing. Will continue to use the switchboard."; } } #endif // Applications are not done with the transfer yet. // Find out how to make sure sendNextDataParts() is called again. if( ! switchToDc ) { // Continue with normal behavour // Currently this method triggers itself, // until the switchboards are busy and "pausedApplications_" is set. if( ! pauseApplications_ ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "scheduling next call."; #endif // Instead of receiving a signal from the switchboard, // fake one right now. This is currently good enough. sbReadyWriteTester_.start( 200 ); } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "switchboard requested to pause the transfer, " << "postponing next action."; #endif } } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "switching to direct connection transfer."; #endif // There appears to be a direct connection! // It's no longer needed to transfer the data over the switchboard // so find out if the write handler is not connected yet. if( ! directConnection_->isWriteHandlerConnected() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no write handler connected yet, " << "connecting write handler to direct connection."; #endif directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) ); } } } /** * @brief An application was finished and requested removal. * @param application The application object instance. */ void ApplicationList::slotTerminateApplication(Application *application) { MimeApplication *mimeApp = qobject_cast( application ); P2PApplicationBase *p2pApp = qobject_cast( application ); // Update the internal lists. if( mimeApp != 0 ) { // It's a mime application if( mimeApplications_.removeAll( mimeApp ) == 0 ) { kmWarning() << "Could not remove MimeApplication" << " (type=" << mimeApp->metaObject()->className() << " contact=" << contactHandle_ << ")!"; } } else if( p2pApp != 0 ) { // Remove from data sending apps too! // Use the same unregister function so the write handler will also be disconnected. if( dataSendingApplications_.contains( p2pApp ) ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "application is terminating while sending data, unregistering as data sending application first."; #endif unregisterDataSendingApplication( p2pApp ); } // Remove from collection if( p2pApplications_.removeAll( static_cast( p2pApp ) ) == 0 ) { kmWarning() << "Could not remove P2PApplication" " (type=" << p2pApp->metaObject()->className() << " contact=" << contactHandle_ << ")!"; } } else { // Warn when there is a problem deleting the object. kmWarning() << "Can't determine type of application " " (type=" << application->metaObject()->className() << " contact=" << contactHandle_ << ")!"; } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Removing application."; #endif // Remove the application application->deleteLater(); // Check whether this application was scheduelled to be aborted by contactLeftChat() // When all applications aborted, signal so that the switchboard connection can be terminated. // This is using a QPtrList so the process is not disturbed by newly added applications or apps left active. if( ! abortingApplications_.isEmpty() ) { if( abortingApplications_.removeAll( application ) ) { if( ! abortingApplications_.isEmpty() ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Still " << abortingApplications_.count() << " other applications left to abort..."; #endif } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Signalling that applications of '" << contactHandle_ << "' have been aborted."; #endif // This may cause listeners to delete this object.. emit applicationsAborted(contactHandle_); } } else { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "Application was not part of the scheduelled applications to abort."; #endif } } } /** * @brief Remove the registration of an application which sends a file. */ void ApplicationList::unregisterDataSendingApplication( P2PApplicationBase *application ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "removing application from the list of active outgoing transfers."; #endif bool found = dataSendingApplications_.removeAll( application ) > 0; if( ! found ) { kmWarning() << "application not found in list."; } // Stop write handler is all applications stopped sending a file. if( dataSendingApplications_.isEmpty() && directConnection_ != 0 ) { #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL kmDebug() << "no applications remaining, disabling connection write handler."; #endif directConnection_->disconnectWriteHandler( this, SLOT(slotConnectionReadyWrite()) ); } #ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL else { // Debug why the write handler was not disabled yet. if( directConnection_ != 0 ) { kmDebug() << "connection write handler not disabled, there are still " << dataSendingApplications_.count() << " applications sending file data."; } else { kmDebug() << "connection write handler not disabled, no direct connection active already."; } } #endif }