Files
kmess/src/network/applications/applicationlist.cpp
T
2026-07-06 09:37:59 +02:00

2201 lines
71 KiB
C++

/***************************************************************************
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 <QRegExp>
#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<const MsnSwitchboardConnection*> 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<Application*>( 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 <code>start()</code> 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 <code>start()</code> 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<MimeApplication*>(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<MsnObjectTransferP2P*>( 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<MsnDirectConnection*>( 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<const MsnDirectConnection*>(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<MimeApplication*>( application );
P2PApplicationBase *p2pApp = qobject_cast<P2PApplicationBase*>( 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<P2PApplication*>( 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
}