/*************************************************************************** p2papplicationbase.cpp - description ------------------- begin : Mon Nov 22 2004 copyright : (C) 2004 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 "p2papplication.h" #include "../../kmessdebug.h" #include "../../currentaccount.h" #include "../../contact/msnobject.h" #include "../../utils/kmessshared.h" #include "../extra/msndirectconnection.h" #include "../mimemessage.h" #include "../p2pmessage.h" #include "applicationlist.h" #include <QBuffer> #include <QDateTime> #include <QIODevice> #include <QTimer> #include <KLocale> #ifdef KMESSDEBUG_P2PAPPLICATION #define KMESSDEBUG_P2PAPPLICATION_GENERAL #endif // Settings for debugging. #define P2PAPPLICATION_AVOID_DC_CLIENT 0 #define P2PAPPLICATION_AVOID_DC_SERVER 0 // Timeout settings, to prevent stale instances. // Measured an ACK response time of 90 seconds once for an overloaded machine (Pentium 2 running Skype + WLM). #define P2PAPPLICATION_TIMEOUT_ACCEPT 120000 #define P2PAPPLICATION_TIMEOUT_ACK 120000 #define P2PAPPLICATION_TIMEOUT_DATA 120000 #define P2PAPPLICATION_TIMEOUT_MOREDATA 120000 #define P2PAPPLICATION_TIMEOUT_SLP 120000 // 120 seconds #define P2PAPPLICATION_TIMEOUT_DESTROY 10000 // 10 seconds /** * @brief Constructor, initializes the P2PApplicationBase instance fields. * * The ApplicationList object contains the direct connection, which is shared between all P2P applications of one same contact. * This object is also used to deliver messages back to the contact. * * @param applicationList The application list object for the contact */ 00064 P2PApplicationBase::P2PApplicationBase(ApplicationList *applicationList) : Application( applicationList->getContactHandle() ), applicationList_(applicationList), buffer_(0), dataSource_(0), dataType_(P2P_TYPE_NEGOTIATION), fragmentMessageID_(0), fragmentOffset_(0), fragmentTotalSize_(0), nextMessageID_(0), shouldSendAck_(false), userAborted_(false), waitingState_(P2P_WAIT_DEFAULT) { // For debugging setObjectName( QLatin1String( metaObject()->className() ) + "[0/" + getContactHandle() + "]" ); // Reset other internal fields lastIncomingMessage_.dataSize = 0; lastIncomingMessage_.messageID = 0; lastIncomingMessage_.messageType = P2P_MSG_UNKNOWN; lastIncomingMessage_.sentTime = 0; lastIncomingMessage_.sessionID = 0; lastIncomingMessage_.totalSize = 0; lastIncomingMessage_.ackSessionID = 0; lastIncomingMessage_.footerCode = 0; // Initiaize the timer waitingTimer_ = new QTimer(this); waitingTimer_->setSingleShot( true ); connect( waitingTimer_, SIGNAL(timeout()), this, SLOT(slotCleanup()) ); // Crash prevention. connect( applicationList_, SIGNAL(destroyed()), this, SLOT(slotApplicationListDeleted()) ); } /** * @brief Class destructor. * * Cleans up buffers and timers. */ 00107 P2PApplicationBase::~P2PApplicationBase() { #ifdef KMESSDEBUG_APPLICATION // Can't include getSessionID() here, is virtual method of destructed derived class. kDebug() << " closing=" << isClosing() << " timer=" << waitingTimer_->isActive() << " state=" << waitingState_ << " unackedCount=" << outgoingMessages_.count() << "."; #endif #ifdef KMESSTEST KMESS_ASSERT( outgoingMessages_.isEmpty() ); #endif // Stop the timer waitingTimer_->stop(); applicationList_ = 0; dataSource_ = 0; // Delete pointers qDeleteAll( outgoingMessages_ ); delete waitingTimer_; delete buffer_; } /** * @brief Make sure no more data will be sent. * * Note this method does nothing special, * except for closing down the data source. * Typically you want to call a high-level method * instead which calls this method internally. */ 00141 void P2PApplicationBase::abortDataSending() { // Abort sending. if( dataSource_ != 0 ) { #ifdef KMESSDEBUG_APPLICATION kDebug() << "application was still sending data, resetting."; #endif dataSource_ = 0; dataType_ = P2P_TYPE_NEGOTIATION; fragmentTotalSize_ = 0; fragmentOffset_ = 0; applicationList_->unregisterDataSendingApplication( this ); } } /** * @brief Scheduelle termination, but wait for last incoming packets */ 00163 void P2PApplicationBase::endApplicationLater() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "setting timeout to destroy object, allow contact to send a final message."; #endif // If a transfer was active, make sure it ends. abortDataSending(); if( waitingState_ != P2P_WAIT_END_APPLICATION ) { // Test if there are still unacked messages (make sure it's only tested once). // This is done explicitly before the data transfer ends. testUnAckedMessages( false ); } // Set timeout to destroy setWaitingState( P2P_WAIT_END_APPLICATION, P2PAPPLICATION_TIMEOUT_DESTROY ); // Set flag to avoid second attempts. setClosing( true ); } /** * @brief Return the message ID used in the previous message from the contact. * * This value is used to match fragmented messages. * It's the second field of the binary P2P header. * * @return The message ID field used in the binary P2P header. */ 00196 unsigned long P2PApplicationBase::getLastContactMessageID() const { return lastIncomingMessage_.messageID; } /** * @brief Returns the nonce that's being expected. * * This value is used to authenticate the direct connection. * * @return The Nonce value; a randonly generated GUID. */ 00210 const QString & P2PApplication::getNonce() const { return nonce_; } /** * @brief Return the number of transferred bytes. */ 00220 unsigned long P2PApplicationBase::getTransferredBytes() const { return dataType_ != P2P_TYPE_NEGOTIATION ? fragmentOffset_ // outgoing : fragmentTracker_.getTransferredBytes(); // incoming } /** * @brief Find the unacked message which corresponds with the received P2P ACK message. * * This method compares the binary P2P fields to find the original message which was sent. * The returned 'messageType' field is used later in gotAck() to handle ACKs for some specific messages. * * @param ackMessage The ACK message to test for. * @return The meta information about the original message ACKed by the contact. */ 00238 P2PApplicationBase::UnAckedMessage * P2PApplicationBase::getUnAckedMessage(const P2PMessage &ackMessage) const { #ifdef KMESSTEST KMESS_ASSERT( ackMessage.getFlags() != 0 && ackMessage.getDataSize() == 0 ); #endif unsigned long ackSessionID = ackMessage.getAckSessionID(); // ackSid unsigned long ackUniqueID = ackMessage.getAckUniqueID(); // ackUid unsigned long ackFlag = ackMessage.getFlags(); foreach( UnAckedMessage *unAcked, outgoingMessages_ ) { // This works for normal ACKs (0x02) to P2P messages. // Ack message | original message // ----------------------------------------- // ackSid = messageID // ackUid = ackSid // ackSize = size if( ackUniqueID == unAcked->ackSessionID ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "message is matched, option 1: " "ackUniqueID = original uniqueID " << ackUniqueID << "."; #endif #ifdef KMESSTEST KMESS_ASSERT( ackFlag == P2PMessage::MSN_FLAG_ACK ); #endif // Found it, save reference return unAcked; } // This works for control messages (like 0x08): // uniqueID is reffers to uniqueID of original message. // Ack message | original message // ----------------------------------------- // total = total // ackSid = ackSid // ackUid = 0 // ackSize = 0 if( ackSessionID == unAcked->ackSessionID && ackUniqueID == 0 ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "message is matched, option 2: " "ackSessionID = original uniqueID " << ackSessionID << "."; #endif #ifdef KMESSTEST KMESS_ASSERT( ackFlag == P2PMessage::MSN_FLAG_ERROR ); #endif // Found it, save reference return unAcked; } // Also attempt to match on the messageID alone if: // - it's a 0x80 message. In this case sendP2PMessageImpl() has overwritten the previous uniqueID. // - the UniqueID is not set. (also happens with normal ack messages in GAIM 1.5). // // This happens with 0x01 control messages: // Ack message | original message // ----------------------------------------- // ackSid = messageID // ackUid = 0 // ackSize = bytepos // // This happens with 0x80 messages: // Ack message | original message // ----------------------------------------- // ackSid = messageID // ackUid = new // ackSize = size if( ackSessionID == unAcked->messageID && ( ackFlag == P2PMessage::MSN_FLAG_ABORTED_RECEIVING || ackUniqueID == 0 ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "message is matched, option 3: " "ackMessageID = original messageID " << ackSessionID << "."; #endif #ifdef KMESSTEST KMESS_ASSERT( ackFlag == P2PMessage::MSN_FLAG_ABORTED_RECEIVING || ackFlag == P2PMessage::MSN_FLAG_NEGATIVE_ACK ); #endif // Found it, save reference return unAcked; } } return 0; } /** * @brief Find the unacked message which corresponds with given message type. * * @param messageType The P2P message type. * @return The message unacked message details. */ 00336 P2PApplicationBase::UnAckedMessage * P2PApplicationBase::getUnAckedMessage(const P2PMessageType messageType) const { foreach( UnAckedMessage *unAcked, outgoingMessages_ ) { if( unAcked->messageType == messageType ) { return unAcked; } } return 0; } /** * @brief Return the current waiting state. * * This function is intended to be used for debugging. * Use isWaitingState() to test for the current state. * * @return The current waiting state */ 00359 P2PApplicationBase::P2PWaitingState P2PApplicationBase::getWaitingState() const { return waitingState_; } /** * @brief Called internally when data is received, this eventually calls gotData() * * It validates the current state, calls showTransferProgress() and gotData(). * After receiving the last message, it calls showTransferComplete() and sends the SLP BYE message if needed. * * @param message The received data message. */ 00374 void P2PApplicationBase::gotDataFragment(const P2PMessage &message) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Received file data, calling gotData()."; #endif #ifdef KMESSDEBUG KMESS_ASSERT( waitingState_ == P2P_WAIT_FOR_FILE_DATA ); #endif unsigned long dataOffset = message.getDataOffset(); // See if the class is trying to abort. // Avoid bothering the derived class with these issues. if( waitingState_ == P2P_WAIT_FOR_SLP_BYE_ACK ) { kWarning() << "Still receiving data while BYE is sent " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x08)."; sendP2PAbort(); return; } else if( waitingState_ == P2P_WAIT_FOR_SLP_BYE ) { // This actually happened when there were some delayed packets in the buffer with WLM. // When switching bridges (SB/DC) it actually cannot guarantee the messages will be sent in order. // This is visible at slow computers. kWarning() << "Still receiving data while last fragment was received " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x08)."; sendP2PAbort(); return; } else if( waitingState_ == P2P_WAIT_END_APPLICATION ) { // This happend when the data preparation was rejected, // and the contact kept sending data. kWarning() << "Still receiving data while the application wants to close " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x08)."; sendP2PAbort(); return; } // See if data is already sent before establishing a connection. // Happens with WLM, likely at slow computers. if( dataOffset == 0 && ! applicationList_->hasDirectConnection() && ( applicationList_->hasPendingConnections() || applicationList_->hasPendingConnectionInvitation() ) ) { kWarning() << "Contact is already sending data " "while attempting to establish a direct connection " "(state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=continue)."; } // Initialize at the first message if( dataOffset == 0 ) { fragmentTracker_.initialize( message.getMessageID(), message.getTotalSize() ); } else { // Check if the new data fragment belongs to the current one. if( ! fragmentTracker_.isInitialized( message.getMessageID() ) ) { // Send negative ack message, like WLM does (0x01) // TODO: provide last failed position, allow contact to resume kWarning() << "Received unexpected offset for message " "(offset=" << dataOffset << " messageID=" << message.getMessageID() << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x01)."; sendP2PAckImpl( P2PMessage::MSN_FLAG_NEGATIVE_ACK ); sendCancelMessage( CANCEL_FAILED ); // sends slp bye return; } } // Update the tracker fragmentTracker_.registerFragment( message.getDataOffset(), message.getDataSize() ); // Dispatch the message to the derived class showTransferProgress( fragmentTracker_.getTransferredBytes() ); gotData(message); // See if the transfer should be complete now. if( ! fragmentTracker_.isComplete() ) { // Wait for more data to arrive. waitingTimer_->start( P2PAPPLICATION_TIMEOUT_MOREDATA ); if( message.isLastFragment() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got the last fragment, " "but some messages are not received yet."; kDebug() << "Current message is " << fragmentTracker_.getDebugMap(); #endif shouldSendAck_ = false; // could be true if received out of order. } } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got the last fragment. Sending ack, calling gotDataComplete() and showTransferComplete()."; #endif // After all data is received, ACK it and send the BYE // TODO: if the messages were received out of order, should we ACK the final message instead? shouldSendAck_ = true; // could be false if received out of order. sendP2PAckImpl(); // Notify the derived class, so it can process it's buffered data. // The P2PApplication class also uses this to send the SLP BYE if needed. setWaitingState( P2P_WAIT_DEFAULT, 0 ); gotDataComplete( message ); // For datacast applications (ink), request termination immediately. if( getMode() == APP_MODE_DATACAST ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Application is a datacast handler, request termination immediately."; #endif // FIXME we must decide if it's better implement in derived ink class the method // gotDataComplete called few lines above or keep showTransferComplete(). Also we check // if it's possible to move this code fragment ( from if statement ) over the "sendP2PAckImp()" // line and figure out if the send Ack is necessary also for INK... showTransferComplete(); endApplication(); return; } // Warn if developers mess up with gotDataComplete() // e.g. forget to call the parent P2PApplication::gotDataComplete() method to send the BYE. if( waitingState_ == P2P_WAIT_DEFAULT ) { kWarning() << "The overwritten gotDataComplete() method did not send anything, nor changed the waiting state, " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << ")."; } // Signal derived class the transfer is complete showTransferComplete(); // Next, either the derived class: // - terminated this object, or // - sent the SLP BYE, or // - waits for the SLP BYE from the contact. } } /** * @brief Called when a P2P message was received with the error-flag set. * * This method aborts the application by calling contactAborted(). * * @param message The received error message. */ 00550 void P2PApplicationBase::gotErrorAck(const P2PMessage &message) { #ifdef KMESSTEST KMESS_ASSERT( message.getDataSize() == 0 ); if( message.isNegativeAck() ) { KMESS_ASSERT( message.getTotalSize() == 0 ); } else if( message.isError() ) { KMESS_ASSERT( message.getTotalSize() != 0 ); } KMESS_ASSERT( message.getAckSessionID() != 0 ); KMESS_ASSERT( message.getAckUniqueID() == 0 ); KMESS_ASSERT( message.getAckDataSize() == 0 ); #endif // When the contact aborted, it could send some 0x08 messages back for the // next fragments which were already sent (e.g. already in the DC at that time). // Ignore these error messages. if( isClosing() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an P2P error-response while closing " "(flag=0x" << QString::number( message.getFlags(), 16 ) << ", ignoring message)."; #endif // Make sure the timer resumes. endApplicationLater(); return; } if( message.isNegativeAck() ) { // Received when the transfer failed, e.g. write errors at the direct connection. kWarning() << "Got an unexpected P2P error-response " "(flag=nak" << " byte=" << message.getDataSize() << " state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=continue)."; // TODO: resume transfer at the indicated byte position. abortDataSending(); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send a BYE message."; #endif // Wait for BYE at the moment. setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP ); } else { // Error in our headers? kWarning() << "Got an unexpected P2P error-response " "(flag=0x" << QString::number( message.getFlags(), 16 ) << " state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=contactaborted)."; if( dataType_ == P2P_TYPE_NEGOTIATION && ! P2P_WAIT_FOR_DATA_ACK ) { // We likely made an error in the invitation/bye reponse contactAborted( i18n("The contact rejected the invitation. An internal error occured.") ); } else { // TODO: make this error custom. // We were sending or receiving a file/picture // Can also happen if the contact can't handle our data for a different reason. contactAborted( i18n("The transfer failed.") ); } } } /** * @brief Main entry function for incoming incoming messages. * * This method receives the incoming P2P messages, analyses it's * flags and delivers it to various handling methods: * - ACK messages are delivered to gotAck(), gotDirectConnectionHandshake(), gotErrorAck() or gotTimeoutAck(). * - SLP negotiation messages are delivered to gotNegotiationFragment(). * - P2P data preparation messages are delivered to gotDataPreparation(). * - P2P data messages are delivered to gotDataFragment(). * * Other P2P messages are rejected with an error message. * * @param p2pMessage A reference to a P2PMessage object. */ 00650 void P2PApplicationBase::gotMessage(const P2PMessage &p2pMessage) { // Reset session variables. waitingTimer_->stop(); // stop timer #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL if( getMode() == APP_MODE_DATACAST ) { // Special case, abuse of P2P system kDebug() << "P2P message from " << getContactHandle() << " is handled by a datacast session."; } else if( nextMessageID_ == 0 ) { // No messages sent, new application kDebug() << "P2P message from " << getContactHandle() << " is handled by a newly created session."; } else { // s/found/received/ since ApplicationList::gotMessage() finds it, but this makes it easier to filter the logs. kDebug() << "New P2P message from " << getContactHandle() << " is handled by session " << getSessionID() << "."; } #endif // Cancel termination if a new message is received // while the application was already scheduelled to be closed. // Ignore typical messages which are sent while a transfer is aborting. if( waitingState_ == P2P_WAIT_END_APPLICATION && ! p2pMessage.isError() && ! p2pMessage.isAbortedReceivingAck() && ! p2pMessage.isAbortedSendingAck() ) { kWarning() << "received another message while awaiting termination " "(flags=0x" << QString::number( p2pMessage.getFlags(), 16 ) << " size=" << p2pMessage.getDataSize() << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=unsetclosing)."; // Don't reset the waiting state. should happen by next method which responds to the message. // It could be a SLP BYE for example or "481 No Such Call". userAborted_ = false; setClosing( false ); } // If this object was initialized to handle a Bad packet. if( getMode() == APP_MODE_ERROR_HANDLER ) { // Make an exception for SLP packets bool isSlpMessage = ( p2pMessage.isSlpData() && p2pMessage.getDataSize() > 20 ); // for cvs produced data-preparation messages without a session-id if(! isSlpMessage) { // We received an unknown control message, this object was initialized to send the correct response. // Inform the user showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, ! isUserStartedApp() ); // Abort sendP2PAbort(); endApplication(); return; } } // Handle ACKs before everything else. if( p2pMessage.getDataSize() == 0 ) // HACK: for Bot2K3 4.1: no testing for flags { UnAckedMessage *unAcked = getUnAckedMessage(p2pMessage); P2PMessageType ackedMessageType = P2P_MSG_UNKNOWN; if( unAcked == 0 ) { if( p2pMessage.isAck() ) // other messages can't relate have the control flags set. { kWarning() << "Unable to handle ACK message, " "original message not found " "(flags=0x" << QString::number(p2pMessage.getFlags(), 16) << " size=0 state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=return)."; return; } } else { // Remove ack record // Also avoids that hasUnAckedMessage() returns the message we just received. ackedMessageType = unAcked->messageType; outgoingMessages_.removeAll( unAcked ); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Removed ack slot for message " << "(sid=" << unAcked->sessionID << " mid=" << unAcked->messageID << " ackSid=" << unAcked->ackSessionID << " size=" << unAcked->dataSize << " type=" << unAcked->messageType << ")"; #endif delete unAcked; } if( p2pMessage.isAck() ) // 0x02 { gotAck( p2pMessage, ackedMessageType ); } else if( p2pMessage.isConnectionHandshake() ) // 0x100 { gotDirectConnectionHandshake( p2pMessage ); } else if( p2pMessage.isNegativeAck() ) // 0x01 { gotErrorAck( p2pMessage ); } else if( p2pMessage.isWaitingForReply() ) // 0x04 { gotTimeoutAck( p2pMessage ); } else if( p2pMessage.isWaitingForAck() ) // 0x06 { gotTimeoutAck( p2pMessage ); } else if( p2pMessage.isError() ) // 0x08 { gotErrorAck( p2pMessage ); } else if( p2pMessage.isAbortedSendingAck() ) // 0x40 { gotTransferAbortedAck( p2pMessage ); } else if( p2pMessage.isAbortedReceivingAck() ) // 0x80 { gotTransferAbortedAck( p2pMessage ); } else { kWarning() << "Unable to handle ACK message, " "unknown flag value encountered " "(flags=0x" << QString::number(p2pMessage.getFlags(), 16) << " size=0 state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=assumerror)."; // Make sure the application does abort somehow. gotErrorAck( p2pMessage ); } return; } // Send the ack if needed (also applies to INVITE messages) shouldSendAck_ = p2pMessage.isLastFragment(); // Store some message parameters for Acknowledgement later, or to send an error message. // This should not be inserted in a if(shouldSendAck_) line, this info is used to resolve fragmented messages. lastIncomingMessage_.dataSize = p2pMessage.getDataSize(); lastIncomingMessage_.messageID = p2pMessage.getMessageID(); lastIncomingMessage_.messageType = P2P_MSG_UNKNOWN; lastIncomingMessage_.sentTime = 0; lastIncomingMessage_.sessionID = p2pMessage.getSessionID(); lastIncomingMessage_.totalSize = p2pMessage.getTotalSize(); lastIncomingMessage_.ackSessionID = p2pMessage.getAckSessionID(); lastIncomingMessage_.footerCode = 0; // Special cases (abuse of the P2P system) if( getMode() == APP_MODE_DATACAST ) { // Ink data, etc.. gotDataFragment(p2pMessage); return; } // Session ID is 0? -> the clients negotiate for session. if(p2pMessage.getSessionID() == 0) { if( p2pMessage.isSlpData() ) { // Parse the fragment gotNegotiationFragment(p2pMessage); } else { kWarning() << "P2PApplicationBase:gotMessage() - P2P message can't be handled, " "unknown negotiation message type " "(flags=0x" << QString::number(p2pMessage.getFlags(), 16) << " size=" << p2pMessage.getDataSize() << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x08)."; // Don't know what to do with a negotiation message with flags set. sendP2PAbort(); // Abort the application showEventMessage( i18n("The transfer failed. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); endApplicationLater(); return; } } // Session ID not zero? -> clients exchange data in the session else { // Check for data preparation messages if( p2pMessage.isDataPreparation() ) { shouldSendAck_ = true; lastIncomingMessage_.messageType = P2P_MSG_DATA_PREPARATION; gotDataPreparation(p2pMessage); } else if( waitingState_ != P2P_WAIT_FOR_FILE_DATA && p2pMessage.isData() && p2pMessage.getDataSize() == 4 && p2pMessage.getTotalSize() == 4 ) { // HACK: added for Encarta Instant Answers (has data flag set with preparation message). kWarning() << "Expecting data-preparation message, " "got message of 4 bytes with incorrect flags " "(assuming data preparation," " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << ")!"; shouldSendAck_ = true; lastIncomingMessage_.messageType = P2P_MSG_DATA_PREPARATION; gotDataPreparation(p2pMessage); } // Check for normal data messages else if( p2pMessage.isData() ) { lastIncomingMessage_.messageType = P2P_MSG_DATA; gotDataFragment(p2pMessage); } else if( waitingState_ == P2P_WAIT_FOR_FILE_DATA && p2pMessage.getFlags() == 0 && p2pMessage.isFragment()) { // HACK: added for Kopete 0.9.2 code (has no flag set with data messages). kWarning() << "Expecting data message, " "got fragmented message with no flags set " "(assuming file data," " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << ")!"; lastIncomingMessage_.messageType = P2P_MSG_DATA; gotDataFragment(p2pMessage); } else if( p2pMessage.getFlags() == 0 && p2pMessage.getDataSize() >= 18 && ( p2pMessage.getData()[0] == '\x80' // start of each webcam data message || fragmentTracker_.getMessageID() == p2pMessage.getMessageID() ) // continuation of setup message (<receiver>)) TODO: add native support for "data streams" in this base class. ) { // Webcam setup. Even WLM sends this without flags. // At the switchboard the "footercode" is set to 4. // Pass it to the WebcamTransferP2P class to deal with it. lastIncomingMessage_.messageType = P2P_MSG_WEBCAM_SETUP; lastIncomingMessage_.footerCode = 4; // FIXME: direct assumption. gotDataFragment( p2pMessage ); } else { // Unknown p2p message kWarning() << "P2P message can't be handled, " "unknown data message type " "(flags=0x" << QString::number(p2pMessage.getFlags(), 16) << " size=" << p2pMessage.getDataSize() << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x08)."; // Got an P2P binary message we can't handle. (some new kind of flag..?) sendP2PAbort(); // Abort the application showEventMessage( i18n("The transfer failed. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); endApplicationLater(); return; } } #ifdef KMESSTEST // At the end of this method, we should have sent the ACK..! KMESS_ASSERT( isP2PAckSent() ); #endif } /** * @brief Called when a SLP message fragment was received. * * The fragments is buffered until all parts are received. * The gotNegotiationMessage() will be called afterwards. * * @param p2pMessage The received P2P message with SLP payload. */ 00957 void P2PApplicationBase::gotNegotiationFragment(const P2PMessage &p2pMessage) { if(! p2pMessage.isLastFragment()) { // Not everything received yet, buffer it. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an SLP fragment, buffering it."; #endif if(buffer_ == 0) { buffer_ = new QBuffer(); buffer_->open(QIODevice::ReadWrite); } buffer_->write( p2pMessage.getData(), p2pMessage.getDataSize() ); } else { // Read the message or buffer QString slpData; QByteArray bufferData; if(buffer_ != 0) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got the last SLP fragment."; #endif // Append the last MSNSLP fraqment and call the method. buffer_->write( p2pMessage.getData(), p2pMessage.getDataSize() ); buffer_->reset(); // Read the buffer bufferData = buffer_->readAll(); slpData = QString::fromUtf8( bufferData.data(), bufferData.size() ); delete buffer_; buffer_ = 0; } else { // Read the data directly from the message slpData = QString::fromUtf8( p2pMessage.getData(), (int)p2pMessage.getDataSize() ); } // Remove the first preamble line, because it's not in MIME format. // Parse the remaining as MIME fields. int preambleEnd = slpData.indexOf("\r\n"); QString preamble ( slpData.left( (preambleEnd == -1) ? 20 : preambleEnd ) ); QString slpMimePart ( slpData.mid ( (preambleEnd == -1) ? 0 : preambleEnd + 2 ) ); MimeMessage slpMessage( slpMimePart ); // Parse the negotiation message gotNegotiationMessage( slpMessage, preamble ); } } /** * @brief Called when a P2P message with timeout-flag was received. * * This method aborts the application with contactAborted(). * * @param message The received P2P message. */ 01024 void P2PApplicationBase::gotTimeoutAck(const P2PMessage &message) { // Error in our headers? if(isWaitingForUser()) { // Contact aborted this session because we didn't press "accept/cancel" within 120 secs. contactAborted( i18n("The transfer failed. Timeout while waiting for user to accept.") ); } else { // Apparently the contact was waiting for something, likely some packet we had to send. kWarning() << "Got an unexpected P2P timeout-error response " "(flag=" << QString::number( message.getFlags(), 16 ) << " state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=contactaborted)."; if( dataType_ == P2P_TYPE_NEGOTIATION ) { contactAborted( i18n("The contact rejected the invitation. An internal error occured.") ); } else { contactAborted( i18n("The transfer failed. An internal error occured.") ); } } } /** * @brief Got a transfer aborted ack. * * This message acknowledges the data transfer was aborted. * The flag type indicates whether the sender aborted, or the receiver aborted the transfer. * * @param message The received error message. */ 01064 void P2PApplicationBase::gotTransferAbortedAck(const P2PMessage &message) { // Got some kind of confirmation when we indicated to abort. // Accept it by closing directly // Error in our headers? if( ! isClosing() ) { kWarning() << "Got an unexpected P2P transfer aborted-response " "(flag=" << QString::number( message.getFlags(), 16 ) << " state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=endapplication)."; // Make sure the app still quits. abortDataSending(); endApplicationLater(); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Message confirms abort, requesting terination."; #endif // Make sure the app will exist, but leave it half-open to receive the final 0x08 messages // for all messages which were already being sent whle the BYE was underway. abortDataSending(); endApplicationLater(); } } /** * @brief Returns whether the application can handle the given ACK message. * * It checks whether an unacked message exists for the given ACK message. * * @return Whether the application can handle the ACK message. */ 01106 bool P2PApplicationBase::hasUnAckedMessage(const P2PMessage &p2pMessage) { // Simply test using the internal function, don't expose the UnAckedMessage pointer to external classes. return (getUnAckedMessage(p2pMessage) != 0); } /** * @brief Return whether a given message type is still unacked. * * @return Whether the given message is still unacked. */ 01119 bool P2PApplicationBase::hasUnAckedMessage(const P2PMessageType messageType) { return (getUnAckedMessage(messageType) != 0); } /** * @brief Test whether the first message has been sent. */ 01128 bool P2PApplicationBase::isFirstMessageSent() const { return nextMessageID_ != 0; } /** * @brief Test whether a P2P ack was sent for the current message. * @return Whether a P2P ack was sent or the current message. */ 01138 bool P2PApplicationBase::isP2PAckSent() const { return ! shouldSendAck_; } /** * @brief Test whether the transfer is active. * @return Whether the transfer is active. */ 01148 bool P2PApplicationBase::isTransferActive() const { return dataType_ != P2P_TYPE_NEGOTIATION // sending data || ! fragmentTracker_.isEmpty(); // receiving data } /** * @brief Verify whether the transfer was initiated and was completed. * @return Whether the transfer is complete. * @todo This only works for receiving data at the moment. */ 01161 bool P2PApplicationBase::isTransferComplete() const { return fragmentTracker_.isComplete(); // TODO: sending data. } /** * @brief Test whether a given waiting state is set. * @param waitingState The state to test for. * @return Whether the waiting state is currently set. */ 01173 bool P2PApplicationBase::isWaitingState( P2PWaitingState waitingState ) const { // TODO: later perhaps make it a flag instead (to wait for both the DC response and data preparation). return waitingState_ == waitingState; } /** * @brief Send file data in P2P packets. * * A timer starts that transmits all data packets automatically. * It uses the dataSource object to determine the total size of the data to send. * * @param dataSource Source of the data to send * @param dataType Type of the data, used to set the correct flag in the P2P footer code. */ 01190 void P2PApplicationBase::sendData(QIODevice *dataSource, P2PDataType dataType) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Begin of data transfer"; #endif #ifdef KMESSTEST KMESS_ASSERT( getSessionID() != 0 ); KMESS_ASSERT( dataSource != 0 ); KMESS_ASSERT( nextMessageID_ != 0 ); #endif // Make sure we don't activate a slot without a data source. if(dataSource == 0) { kWarning() << "Couldn't send data, data source is null " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send500)."; showEventMessage( i18n("The transfer failed. Could not open data source."), ChatMessage::CONTENT_APP_FAILED, false ); // TODO: Official client sends BYE when data preparation failed. sendCancelMessage(CANCEL_FAILED); // MSNSLP messages terminate the application eventually. return; } // Set the global data fields dataSource_ = dataSource; dataType_ = dataType; // Reserve the next message ID. nextMessageID_++; // Set the total size field, this causes sendP2PMessageImpl() // to handle fragmented messages correctly. // The 'fragmentMessageID_' is set so KMess sends all fragments with the same message ID. // It could happen the client receives a SLP ACK meanwhile, and ACKs it with a new message ID. // The current fragmented message should not be affected. fragmentMessageID_ = nextMessageID_; fragmentOffset_ = 0; fragmentTotalSize_ = (quint32) qAbs( dataSource->size() ); // Inform the application list this application wants to send a file. // It will call sendNextDataParts() each time the connection or switchboard can accept new packets. applicationList_->registerDataSendingApplication( this ); } /** * @brief Send the data preparation message. * * This is a consists of a P2P message with <tt>0x00000000</tt> as message body. * The contact should return with a data-preparation ACK, which initiates the file transfer. * Note that Windows Live Messenger (v8) will start a direct connection setup first * before it sends the data preparation ACK. */ 01248 void P2PApplicationBase::sendDataPreparation() { #ifdef KMESSTEST KMESS_ASSERT( getSessionID() != 0 ); KMESS_ASSERT( waitingState_ == P2P_WAIT_DEFAULT || waitingState_ == P2P_WAIT_FOR_FILE_DATA ); #endif // TODO: Check the various footer codes send for certain transfers. QByteArray p2pMessage( 4, 0x00 ); sendP2PMessageImpl( p2pMessage, 0, P2P_TYPE_PICTURE, P2P_MSG_DATA_PREPARATION ); // TODO: after the data preparation is sent, WLM8 starts a direct connection setup: // - it does not send an ACK yet // - it sends the transreq message, // - when the invitation finished (or the DC is up..??) it sends the data preparation ACK. // - after that it sends the ACK of the last "SLP 200 OK" message. // - at this point, the data preparation ACK triggers KMess to send the picture data. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for data preparation ACK..."; #endif // Wait until our preparation message is ACK-ed setWaitingState( P2P_WAIT_FOR_PREPARE_ACK, P2PAPPLICATION_TIMEOUT_ACK ); } /** * @brief Send the direct connection handshake * * This message contains the Nonce field, * used by the other contact to authenticate the direct connection. * The confirmation message is identical to the handshake message. */ 01283 void P2PApplicationBase::sendDirectConnectionHandshake( const QString &nonce ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending direct connection handshake " << "(nonce=" << nonce << ")."; #endif #ifdef KMESSTEST KMESS_ASSERT( applicationList_->hasDirectConnection() ); #endif #ifdef KMESSTEST // NOTE: the handshake could happen while the file transfer was in progress, or even completed already. KMESS_ASSERT( waitingState_ == P2P_WAIT_FOR_CONNECTION2 // we just established connection, send the first handshake || waitingState_ == P2P_WAIT_FOR_HANDSHAKE // contact established connection, waiting to receive first handshake || waitingState_ == P2P_WAIT_FOR_HANDSHAKE_OK // we sent the first handshake, waiting for contact to return second. ); #endif // Message flag is 0x100 const int handshakeFlag = P2PMessage::MSN_FLAG_DC_HANDSHAKE; // Prepare the header QByteArray header( 48, 0x00 ); nextMessageID_++; // Insert the bytes P2PMessage::insertBytes(header, 0, 0); // 1: SessionID P2PMessage::insertBytes(header, nextMessageID_, 4); // 2: MessageID // offset and total size are 0 P2PMessage::insertBytes(header, handshakeFlag, 28); // 6: Message type (flags) // Insert the nonce at fields 7-9 P2PMessage::insertNonce(header, nonce); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL // Display binary data as hexadecimal string const int nonceOffset = 32; const char hexMap[] = "0123456789ABCDEF"; QString hex; for(int i = 0; i < 16; i++) { if( i == 4 || i == 6 || i == 8 || i == 10 ) { hex += "-"; } int upper = (header[nonceOffset + i] & 0xf0) >> 4; int lower = (header[nonceOffset + i] & 0x0f); hex += hexMap[upper]; hex += hexMap[lower]; } kDebug() << "copied nonce '" << nonce << "' to p2p message header as '" << hex << "'."; #endif // Forcefully Send the message over the direct connection applicationList_->getDirectConnection()->sendMessage( header ); // Not updating the waiting state here. // The handshake process could happen while the data transfer is already in progress } /** * @brief Called when the connection is ready to send more file data. * @param preferredFragments The preferred number of fragments to send at once. * * The sendData() method calls ApplicationList::registerDataSendingApplication() * to start this notification process. Each time the direct connection is * ready to send more data, ApplicationList::slotConnectionReadyWrite() is called which * calls this method. * * @return Returns false if something failed, and this method should not be called again. */ 01356 bool P2PApplicationBase::sendNextDataParts( int preferredFragments ) { // Abort if the data sending was reset. // Also fixes race conditions with signals. if( dataType_ == P2P_TYPE_NEGOTIATION || isClosing() || KMESS_NULL(dataSource_) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "called while aborting " "(datatype=" << dataType_ << " isclosing=" << isClosing() << " userAborted=" << userAborted_ << // sort-of reserved for aborts from ApplicationList. " datasource=" << dataSource_ << " waitingState=" << waitingState_ << " contact=" << getContactHandle() << ")."; #endif // Request caller not to call us again. return false; } // Allow the caller to specify the number of loops, // e.g. more for a direct connection, less for parallel transfers. int loops = qMax( preferredFragments, 1 ); while( --loops >= 0 && fragmentOffset_ < fragmentTotalSize_ && dataSource_ != 0 && ! dataSource_->atEnd() ) { // Read the data from the file. char buffer[1202]; // 1202 is the max P2P data size. qint64 bytesRead = dataSource_->read( buffer, 1202 ); if( bytesRead < 0 ) { kWarning() << "Couldn't send more data, " "read error at " << fragmentOffset_ << " of " << fragmentTotalSize_ << " bytes " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=slpcancel)."; sendCancelMessage( CANCEL_FAILED ); return ( dataSource_ == 0 ); // return true if already aborted by sendCancelMessage(). } else if( bytesRead == 0 ) { break; } // Wrap in buffer, without copying. QByteArray data = QByteArray::fromRawData( buffer, (int)bytesRead ); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending data:" << " offset=" << fragmentOffset_ << " buffer=" << bytesRead; #endif // Determine the progress value now because mesageOffset_ it reset by the last sendP2PMessageImpl() call. unsigned long nextProgressValue = ( fragmentOffset_ + (quint32)bytesRead ); // Determine the flags: // Funny, MSN6 doesn't seam to care which flag was set // while transferring the file. KMess however, does. uint flag = 0; switch( dataType_ ) { case P2P_TYPE_PICTURE: flag = P2PMessage::MSN_FLAG_OBJECT_DATA; break; case P2P_TYPE_FILE: flag = P2PMessage::MSN_FLAG_FILE_DATA; break; default: // Display error message for the first message only. if( fragmentOffset_ == 0 ) { kWarning() << "Unknown data transfer mode " "(mode=" << dataType_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=tryflag)."; } } // Send the message bool writeSuccess = sendP2PMessageImpl( data, flag, dataType_, P2P_MSG_DATA, fragmentMessageID_ ); // Avoid next round if write was blocked already. // This could happen if a direct connection can't write all data. // The DirectConnectionBase class will automatically send the remaining parts, so don't worry about that here. if( ! writeSuccess ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "message could not be written entirely, leaving loop early."; #endif break; } // Dispatch the progress to the derived class showTransferProgress( nextProgressValue ); } // Rounds completed, make final assertions. // Be verbose if something internally goes wrong. if( dataSource_ != 0 && dataSource_->atEnd() && fragmentOffset_ < fragmentTotalSize_ ) { kWarning() << "data source is at the end, " "but offset byte count is not " "(fragmentoffset=" << fragmentOffset_ << " fragmenttotalsize=" << fragmentTotalSize_ << " sourceopen=" << dataSource_->isOpen() << " sourcesize=" << dataSource_->size() << " datatype=" << dataType_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=resetstate)!"; // Reset for next p2p message. fragmentTotalSize_ = 0; fragmentOffset_ = 0; } // See if transfer completed, // if so tell the application list to stop calling this method. if( fragmentOffset_ == 0 || fragmentOffset_ >= fragmentTotalSize_ ) { dataSource_ = 0; dataType_ = P2P_TYPE_NEGOTIATION; applicationList_->unregisterDataSendingApplication( this ); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for all data received ACK..."; #endif // Now wait for the contact to ACK that it received all data. setWaitingState( P2P_WAIT_FOR_DATA_ACK, P2PAPPLICATION_TIMEOUT_ACK ); // no return false, will unregister the app twice. } return true; } /** * @brief Send a low-level control message to terminate the session. * * The session will automatically be scheduelled termination using a call to endApplicationLater(). */ 01502 void P2PApplicationBase::sendP2PAbort() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL if( ! shouldSendAck_ ) { kDebug() << "NOTICE: sending 0x08 control packet while the contact is not expecting an ack response."; } #endif // Inform the client we want to kill the session right away. sendP2PAckImpl( P2PMessage::MSN_FLAG_ERROR ); #ifdef KMESSTEST KMESS_ASSERT( isP2PAckSent() ); #endif // Scheduelle termination of the session. endApplicationLater(); } /** * @brief Send an ACK message if this is needed. * * This method called each time a P2P payload is fully received (such as an SLP message). * The ACK system turns the P2P communication into a stop-and-wait protocol. * The other client waits until it receives an ACK or error before it continues with the next step. * * This function is mostly used by the derived P2PApplication class. * It calls sendP2PAck() when a SLP message is properly received. * * @return Whether an ack had to be sent. */ 01536 bool P2PApplicationBase::sendP2PAck() { // Only send an ACK is this is really needed. if( ! shouldSendAck_ ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "NOTICE: not sending 0x02 ack packet, something is already sent (or nothing needs to be sent)."; #endif return false; } // Send the ack sendP2PAckImpl(); // type = P2PMessage::MSN_FLAG_ACK. return true; } /** * @brief Internal function to send a P2P ACK message. * * This method is the internal implementation for sendP2PAck() and others. * * @param ackType The ACK type (flag field) to use, defaults to <tt>MSN_FLAG_ACK</tt>. * @param originalMessageData The meta data of the original message, used to fill some parts of the ACK message. * Defaults to the last received message. */ 01564 void P2PApplicationBase::sendP2PAckImpl(int ackType, UnAckedMessage *originalMessageData) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL if(ackType == P2PMessage::MSN_FLAG_ACK) { if( nextMessageID_ == 0 && getMode() == APP_MODE_NORMAL ) { kDebug() << "Sending P2P ACK (INVITE-ACK)"; } else { kDebug() << "Sending P2P ACK"; } } else if(ackType == P2PMessage::MSN_FLAG_ERROR) { kDebug() << "Sending P2P ACK (Error-response)"; } else if(ackType == P2PMessage::MSN_FLAG_WAITING) { kDebug() << "Sending P2P ACK (Timeout-response)"; } else if(ackType == P2PMessage::MSN_FLAG_WAITING_FOR_ACK) { kDebug() << "Sending P2P ACK (ACK-timeout-response)"; } else if(ackType == P2PMessage::MSN_FLAG_ABORTED_SENDING) { kDebug() << "Sending P2P ACK (Aborted-myself-ACK)"; } else if(ackType == P2PMessage::MSN_FLAG_ABORTED_RECEIVING) { kDebug() << "Sending P2P ACK (Aborted-you-ACK)"; } else { kDebug() << "Sending P2P ACK (Unknown-type)"; } #endif #ifdef KMESSTEST KMESS_ASSERT( shouldSendAck_ ); // TODO: test for control messages, which do not have to obey this rule. #endif // Default to previous message if no argument is given if( originalMessageData == 0 ) { originalMessageData = &lastIncomingMessage_; } // Fill the header: // // 0 4 8 16 24 28 32 36 40 48 // |....|....|....|....|....|....|....|....|....|....|....|....| // |sid |mid |offset |totalsize|size|flag|asid|auid|a-datasz | // QByteArray header( 48, 0x00 ); // Determine the message ID quint32 messageID; if( nextMessageID_ == 0 ) { // We don't have a message ID set yet, we're acknowledging // the first INVITE message. Generate an ID nextMessageID_ = KMessShared::generateID(); messageID = nextMessageID_; // Somehow, the next message we send (200 OK) should contain a messageID - 3: // We decrement the messageID with 4 because it will be incremented in the next call. nextMessageID_ -= 4; } else { nextMessageID_++; messageID = nextMessageID_; } // Determine the ack settings. quint32 ackTotalSize = 0; quint32 ackSessionID = 0; quint32 ackUniqueID = 0; quint32 ackDataSize = 0; // New ack field schema seen with Windows Live Messenger: if( ackType == P2PMessage::MSN_FLAG_ERROR ) { // This ack tells the contact it should reset the session (TCP RST). ackTotalSize = originalMessageData->totalSize; ackSessionID = originalMessageData->ackSessionID; } else if( ackType == P2PMessage::MSN_FLAG_NEGATIVE_ACK ) { #ifdef KMESSTEST KMESS_ASSERT( ! fragmentTracker_.isEmpty() ); KMESS_ASSERT( fragmentTracker_.isInitialized( originalMessageData->messageID ) ); #endif // This ack tells the contact we haven't received the data properly (e.g. invalid offsets) // TODO: guess the ackDataSize is the the last offset which was received. ackSessionID = originalMessageData->messageID; ackDataSize = fragmentTracker_.isInitialized( originalMessageData->messageID ) ? fragmentTracker_.getTransferredBytes() : 0; } else if( ackType == P2PMessage::MSN_FLAG_ABORTED_RECEIVING ) { // This ack tells the contact we won't ack the data anymore, since it's aborted. ackSessionID = originalMessageData->messageID; ackUniqueID = KMessShared::generateID(); ackDataSize = originalMessageData->totalSize; } else if( ackType == P2PMessage::MSN_FLAG_ABORTED_SENDING ) { #ifdef KMESSTEST KMESS_ASSERT( fragmentMessageID_ != 0 ); KMESS_ASSERT( originalMessageData->messageType == P2P_MSG_DATA ); KMESS_ASSERT( hasUnAckedMessage( P2P_MSG_DATA) ); #endif // this ack tells the contact to terminate it's ack session. ackSessionID = originalMessageData->messageID; // must reffer to our OWN message. } else if( ackType == P2PMessage::MSN_FLAG_WAITING ) { // The ackSid seams to be random if it was waiting for an ack. ackSessionID = originalMessageData->ackSessionID; // we give it a more precise value. } else { #ifdef KMESSTEST KMESS_ASSERT( ackType == P2PMessage::MSN_FLAG_ACK ); #endif // This is a normal ack which confirms the arrival. ackSessionID = originalMessageData->messageID; ackUniqueID = originalMessageData->ackSessionID; ackDataSize = originalMessageData->totalSize; } // Insert the bytes P2PMessage::insertBytes(header, originalMessageData->sessionID, 0); // SessionID P2PMessage::insertBytes(header, messageID, 4); // BaseIdentifier // offset of 8 bytes is always 0 (position 8) P2PMessage::insertBytes(header, ackTotalSize, 16); // Confirm total size set in previous message // size of 4 bytes is always 0 (position 24) P2PMessage::insertBytes(header, ackType, 28); // Set the ACK flag. P2PMessage::insertBytes(header, ackSessionID, 32); // ID referring to a previous message. P2PMessage::insertBytes(header, ackUniqueID, 36); // ID referring to a specific fragment. P2PMessage::insertBytes(header, ackDataSize, 40); // reference to the Size of that message. // The Ack needs to have the same footer code (visible in webcam setup for example). uint footerCode = originalMessageData->footerCode; // Deliver the message over the correct link (switchboard or direct connection) applicationList_->sendMessage(this, header, QByteArray(), footerCode); shouldSendAck_ = false; #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "ACK Transmitted " << "(flags=0x" << QString::number(ackType, 16) << ")."; #endif } /** * @brief Sends a complete P2P message payload. * * It splits the data message into chunks which fit in the individual P2P messages. * * If you need to send data, use sendData() instead. * This method is stream based, and also takes care of state changes. * * @param messageData The message payload to send. * @param flagField The value for the flag field in the P2P header. * @param footerCode The message footer, which is appended when the message is sent over the switchboard. * @param messageType The type of the message. This value is returned with gotAck(). */ 01740 void P2PApplicationBase::sendP2PMessage(const QByteArray &messageData, int flagField, P2PDataType footerCode, P2PMessageType messageType) { // Make sure the message gets sent in chunks of 1202 bytes. // Only set fragmentTotalSize_ if there are chunks, for useless debug messages. int remainingBytes = messageData.size(); fragmentOffset_ = 0; fragmentTotalSize_ = ( remainingBytes <= 1202 ? 0 : remainingBytes ); do { // Let another QByteArray encapsulate a part of the utf8Message const char *dataPointer = messageData.data() + fragmentOffset_; uint dataSize = qMin( remainingBytes, 1202 ); QByteArray messagePart = QByteArray::fromRawData( dataPointer, dataSize ); // Send the message with the generic sendP2PMessageImpl() method // fragmentOffset_ is updated automatically sendP2PMessageImpl( messagePart, flagField, footerCode, messageType ); remainingBytes -= dataSize; #ifdef KMESSTEST KMESS_ASSERT( (uint) remainingBytes == fragmentTotalSize_ - fragmentOffset_ ); #endif } while( remainingBytes > 0 ); // Reset for normal operations fragmentOffset_ = 0; fragmentTotalSize_ = 0; } /** * @brief Internal function to send a single P2P message. * * This method constructs the P2P binary header fields. * The constructed message is delivered to ApplicationList::sendMessage(). * * The following instance fields are updated automatically: * - nextMessageID_ is generated for every new P2P message. * - fragmentOffset_ is updated when a splitted message is sent. * - fragmentTotalSize_ and messageOffse_ are reset when the last message fragment is sent. * - outgoingMessages_ is updated to trace ACKs back later. * * The fragmentTotalSize_ should be set to the total size before sending message fragments. * This is done automatically by sendP2PMessage() function, which is part of the public API. * * @param messageData The message payload to send. This can be binary file data or a SLP MIME data. * @param flagField The value for the flag field in the P2P header. * @param footerCode The message footer, which is appended when the message is sent over the switchboard. * @param messageType The type of the message. This value is stored with in the unacked message queue. * This value is used later to respond to ACKs of certain special messages. * @param messageID Optional message ID to enforce. If nothing is selected, * the next available message ID will be used automatically. * This parameter is used to send file data with a specific message ID. * @return Whether the message can be sent. If the socket would block, false is returned. * This only occurs for direct connections connections. */ 01799 bool P2PApplicationBase::sendP2PMessageImpl(const QByteArray &messageData, int flagField, P2PDataType footerCode, P2PMessageType messageType, quint32 messageID) { #ifdef KMESSTEST KMESS_ASSERT( messageData.size() <= 1202 ); #endif // Initialize the header QByteArray header( 48, 0x00 ); // Determine the message ID, if none is given. if( messageID == 0 ) { if( nextMessageID_ == 0) { // This is the first P2P message we send, initialize message ID nextMessageID_ = KMessShared::generateID(); } else if(fragmentOffset_ == 0) { // Only increments the ID for the first message. // Don't increment it for the next fragments (if it's a splitted message). // Better approach: pass the desired messageID to this function. nextMessageID_++; } messageID = nextMessageID_; } // Verify we're not configured to send a splitted message. if( fragmentTotalSize_ > 0 && flagField != 0 && flagField != P2PMessage::MSN_FLAG_OBJECT_DATA && flagField != P2PMessage::MSN_FLAG_FILE_DATA ) { kWarning() << "Attempting to send splitted ACK message " "(flags=0x" << QString::number( flagField, 16 ) << " state=" << waitingState_ << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=continue)!"; } // Determine the message size: // Internal trick: fragmentTotalSize_ is set by the caller if this is a splitted message. quint32 messageSize = messageData.size(); quint32 totalSize; quint32 offsetField; if( fragmentTotalSize_ == 0 ) { // Normal message totalSize = messageData.size(); offsetField = 0; } else { // Splited message: total size is set. totalSize = fragmentTotalSize_; offsetField = fragmentOffset_; } // Generate ack session identifier for this message quint32 ackSessionID = KMessShared::generateID(); // Set session ID to zero when SLP messages are sent. quint32 sessionID = getSessionID(); if( flagField == 0 && footerCode == 0 // fixes all data transfers, like msnobjects and webcam. && messageType != P2P_MSG_DATA_PREPARATION ) { sessionID = 0; } // Update the header P2PMessage::insertBytes(header, sessionID, 0); // 1: Session ID (zero for SLP messages). P2PMessage::insertBytes(header, messageID, 4); // 2: BaseIdentifier (starts random [4, 4294967295]) P2PMessage::insertBytes(header, offsetField, 8); // 3: Offset (if more then 1202 bytes to send) P2PMessage::insertBytes(header, totalSize, 16); // 4: Total size of the message P2PMessage::insertBytes(header, messageSize, 24); // 5: This message size, size of previous if no data P2PMessage::insertBytes(header, flagField, 28); // 6: Message type (flags) P2PMessage::insertBytes(header, ackSessionID, 32); // 7: Used to link ack messages (ACK session ID) //P2PMessage::insertBytes(header, 0, 36); // 8: Field7 of previous for ACK-messages (unique ID) //P2PMessage::insertBytes(header, 0, 40); // 9: Field4 of previous for ACK-messages (size) // Deliver the message over the correct link (switchboard or direct connection) bool writeSuccess = applicationList_->sendMessage( this, header, messageData, (uint) footerCode ); if( ! writeSuccess ) { // This only happens when a direct connection transfer is congested. // The same message will be re-sent later. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Message sending was blocked, should try again later."; #endif return false; } // Update unacked message queue to handle incoming ACKs if( flagField == 0 || messageType != 0 ) { if( offsetField == 0 ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Storing message fields to track the ACK response."; #endif // First message, or single message, store new record for ACKs. QDateTime currentTime; UnAckedMessage *ackData = new UnAckedMessage; ackData->sessionID = getSessionID(); ackData->messageID = messageID; ackData->dataSize = messageSize; ackData->totalSize = totalSize; ackData->ackSessionID = ackSessionID; ackData->messageType = messageType; // used later to handle certain ACKs. ackData->sentTime = currentTime.toTime_t(); outgoingMessages_.append(ackData); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Updating message fields to track the ACK response."; #endif // Follow-up part of a splitted/fragmented message. // Update the uniqueID aka ackSID field to handle new acks. // TODO: it appears MSN7 uses the same ackSID for some messages, not sure // whether that is a feature (to capture acks in a short time) or a problem in their random function. foreach( UnAckedMessage *unAcked, outgoingMessages_ ) { if( unAcked->messageID == messageID ) { unAcked->ackSessionID = ackSessionID; break; } } } } // Update the offset fields for splitted messages if(fragmentTotalSize_ != 0) { // Update offset fragmentOffset_ += messageSize; // If we transmitted all parts, reset the fields for the next normal message if(fragmentOffset_ >= fragmentTotalSize_) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "All message parts sent, resetting fragmentOffset_ and fragmentTotalSize_."; #endif fragmentOffset_ = 0; fragmentTotalSize_ = 0; } } #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Message transmitted " << "(sid=" << getSessionID() << " mid=" << messageID << " ackSid=" << ackSessionID << " flags=0x" << QString::number(flagField, 16) << " size=" << messageSize << " type=" << messageType << ")"; #endif return true; } /** * @brief Send a low-level error message that the application is waiting for a certain message. */ 01979 void P2PApplicationBase::sendP2PWaitingError() { sendP2PMessageImpl( 0, P2PMessage::MSN_FLAG_WAITING ); shouldSendAck_ = false; } /** * @brief Send a complete SLP message in multiple P2P packets. * * This is a low-level function which is primary used by * P2PApplication::sendSlpInvitation(), P2PApplication::sendSlpBye(), etc.. * * It converts the SLP string to the payload data, * which could be splitted across multiple P2P messages. * * @param slpMessage The whole SLP message to send, including SLP headers. * @param messageType The type of the message. This value is returned with gotAck(). */ 01999 void P2PApplicationBase::sendSlpMessage(const QString &slpMessage, P2PMessageType messageType) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "SLP message=" << slpMessage; #endif // Get the raw utf8 encoded bytearray QByteArray utf8Message = slpMessage.toUtf8(); QByteArray messagePart; // Make sure the extra padded '\0' character is there. MSN 6-WLM rely on this. utf8Message.append('\0'); sendP2PMessage( utf8Message, 0, P2P_TYPE_NEGOTIATION, messageType ); // will split the message. } /** * @brief Set the current message type, for debugging. * * This method can be set from gotNegotiationMessage() to set the current message type. * It's used for debugging, and setting the proper footer code in ACK responses. */ 02023 void P2PApplicationBase::setCurrentMessageType( P2PMessageType messageType ) { // Only send an ACK is this is really needed. if( ! shouldSendAck_ ) { kWarning() << "NOTICE: ack is likely sent already, setting setCurrentMessageType() has no use anymore."; } lastIncomingMessage_.messageType = messageType; } /** * @brief Notify this base class the user is aborting the session. * * This method sets a flag to avoid sending certain error messages while the user is aborting * (e.g. closing the chat window). It prevents crashes or spawning new chat sessions. * * @param userAborted Whether the user is aborting. */ 02044 void P2PApplicationBase::setUserAborted( bool userAborted ) { userAborted_ = userAborted; } /** * @brief Indicate which packet is expected next. * @param waitingState The new waiting state. * @param timeout Number of milliseconds before a timer fires and showTimeoutMessage() is called. * The contact's client will be informed automatically that it forgot to send some data. * If this value is 0, it means the class temporary changes the state for debugging. */ 02058 void P2PApplicationBase::setWaitingState( P2PWaitingState waitingState, int timeout ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL unsigned long sessionID = getSessionID(); if( timeout == 0 ) { #ifdef KMESSTEST KMESS_ASSERT( ! waitingTimer_->isActive() ); #endif if( waitingState_ != waitingState ) { kDebug() << "temporary switching waitingState of session" << sessionID << "from" << waitingState_ << "to" << waitingState; } } else { if( waitingState_ != waitingState ) { kDebug() << "switching waitingState of session" << sessionID << "from" << waitingState_ << "to" << waitingState; } else { kDebug() << "resuming waitingState of session" << sessionID << "for" << waitingState_; } } #endif // Update the state waitingState_ = waitingState; // Restart the timer if( timeout != 0 ) { waitingTimer_->start( timeout ); } } /** * @brief Crash prevention method. * * While it should never happen that the ApplicationList is deleted before the P2PApplication class, * this did happen in the past (see http://trac.kmess.org/ticket/263). This method is an extra * safety guard to prevent crashes after many things do wrong. */ 02106 void P2PApplicationBase::slotApplicationListDeleted() { kWarning() << "ApplicationList is deleted before the P2PApplication class! This should not happen!" "(maybe an old session, " " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=endapplication)."; applicationList_ = 0; // ignores the derived P2PApplication class for now. // No one can or needs to receive the deleteMe() signal anymore. // Forcefuly delete ourselves and hope for the best. deleteLater(); } /** * @brief Cleanup function, is used to handle timeout events. * * When a timeout occurs before the contact sent something we expect, * this function terminates the application automatically. * * Depending on the internal "waiting state", an error message will be outputted with <tt>kWarning()</tt>. */ 02132 void P2PApplicationBase::slotCleanup() { // See if the waiting time for new packets is over (like TCP's TIME_WAIT state). if( waitingState_ == P2P_WAIT_END_APPLICATION ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "timer of endApplicationLater() fired, finally destroying object."; #endif // Directly destroy ...finally. testUnAckedMessages( true ); endApplication(); return; } #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "timeout fired, cleaning up " "(contact=" << getContactHandle() << " session=" << getSessionID() << " waitingState=" << waitingState_ << ")."; #endif #ifdef KMESSTEST KMESS_ASSERT( waitingState_ != P2P_WAIT_DEFAULT ); #endif // Not waiting for ourselves to connect. // Not waiting for contact to accept. // Not waiting for contact to send BYE ack. if( waitingState_ == P2P_WAIT_FOR_CONNECTION2 || waitingState_ == P2P_WAIT_CONTACT_ACCEPT || waitingState_ == P2P_WAIT_FOR_SLP_BYE_ACK ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Not sending timeout message for current waiting state."; #endif } else { if( userAborted_ ) { // Don't send messages when aborting. This could potentially crash KMess, or cause new windows to appear. // - suppose this is the last aborting application which requests removal. // - ApplicationList informs the MsnSwitchboardConnection it can remove itself. // - ChatMaster processes the putMsg() call, and attempts to deliver the message. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Not sending error message because application is aborting."; #endif testUnAckedMessages( false ); } else { /* // Tell the other client we were waiting for a message to receive! // TODO: this is still not as it should be, but a start. uint waitFlag = P2PMessage::MSN_FLAG_WAITING; switch(waitingState_) { case P2P_WAIT_FOR_FILE_DATA: // Waiting for file data. Application is shutting down sender right now. waitFlag = P2PMessage::MSN_FLAG_ABORTED_RECEIVING; // 0x80 break; if waiting for ack, wlm also sends 0x04. case P2P_WAIT_FOR_SLP_OK_ACK: case P2P_WAIT_FOR_PREPARE_ACK: case P2P_WAIT_FOR_TRANSFER_ACK: case P2P_WAIT_FOR_DATA_ACK: //not P2P_WAIT_FOR_SLP_BYE_ACK: case P2P_WAIT_FOR_SLP_ERR_ACK: // we guess 0x06 is used as waiting-for-ack error. waitFlag = P2PMessage::MSN_FLAG_WAITING_FOR_ACK; break; default: break; } // Send the message sendP2PMessageImpl(0, waitFlag, 0); // TODO: pass the original message which is unacked!! */ testUnAckedMessages( true ); // TODO: argument is not used, and replaces sendP2PWaitingError(). sendP2PWaitingError(); } } // Pass to delivered class showTimeoutMessage( waitingState_ ); } /** * @brief Test if the contact aborted the sending of data. * * This method is intended to be used by the P2PApplication class. * When a transfer to the contact is aborted, this method sends a <code>0x40</code> message. * That control packet terminates the "ack slot" in the remote client. */ 02236 void P2PApplicationBase::testDataSendingAborted() { #ifdef KMESSTEST KMESS_ASSERT( waitingState_ == P2P_WAIT_FOR_SLP_BYE_ACK ); #endif // See if the data was not acked. UnAckedMessage *unAckedDataMessage = getUnAckedMessage( P2P_MSG_DATA ); if( unAckedDataMessage != 0 ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sent data was not acked, sending 0x40 to abort ack request."; #endif // It's possible WLM also closed it's data stream at this point, // so ignores the 0x40 and sends a 0x80 at the same time back. sendP2PAckImpl( P2PMessage::MSN_FLAG_ABORTED_SENDING, unAckedDataMessage ); // Remove from collection outgoingMessages_.removeAll( unAckedDataMessage ); delete unAckedDataMessage; } } /** * @brief Test if there are still unacked messages. * @param sendError Whether to send an error to the client back as well. */ 02266 void P2PApplicationBase::testUnAckedMessages( bool /*sendError*/ ) { // Check if there are still unacked messages. if( ! outgoingMessages_.isEmpty() ) { uint unackedCount = outgoingMessages_.count(); if( unackedCount == 1 ) { UnAckedMessage *unAcked = outgoingMessages_.at(0); // More simple debug statement for most used case. kWarning() << "there is still " << unackedCount << " unacked message " "(type=" << unAcked->messageType << " ackSid=" << unAcked->ackSessionID << " totalsize=" << unAcked->totalSize << " state=" << waitingState_ << " contact=" << getContactHandle() << " class=" << metaObject()->className() << " session=" << getSessionID() << ")"; } else { kWarning() << "there are still " << unackedCount << " unacked messages " "(state=" << waitingState_ << " contact=" << getContactHandle() << " class=" << metaObject()->className() << " session=" << getSessionID() << ")"; foreach( UnAckedMessage *unAcked, outgoingMessages_ ) { kWarning() << "Message " << unAcked->messageID << " is unacked " "(type=" << unAcked->messageType << " ackSid=" << unAcked->ackSessionID << " totalsize=" << unAcked->totalSize << ")."; } } } } /** * @brief Stop the waiting timer * @return Whether the timer was still running, or not. */ 02313 bool P2PApplicationBase::stopWaitingTimer() { bool wasActive = waitingTimer_->isActive(); waitingTimer_->stop(); return wasActive; } /** * @brief Utility function to write P2P data to a file, also deals with message offsets. * * When an error occurs, the required error message * will be sent back to the other client. * * While Windows Live Messenger does not allow messages to be out of order, * this method explicitly allows it because WLM itself seams to send messages out of order * in some cases when it's switching to the direct connection (likely error prone at slow systems). * * @param message The P2P message with a file part. * @param file The file to output the data to. * @return True if the data could be written to the file, false on error. */ 02336 bool P2PApplicationBase::writeP2PDataToFile( const P2PMessage &message, QIODevice *file ) { unsigned long offset = message.getDataOffset(); // See if the offset should be changed, // and if so, update it. if( offset != (ulong)file->pos() && ! file->seek( offset ) ) { #ifdef KMESSDEBUG_APPLICATION kDebug() << "data is received out of order, updating file offset."; #endif // File position was different and could not be changed.. unsigned long fileSize = (quint32)file->size(); // See if the offset is too far ahead.. (e.g. don't make us allocate 1GB) if( offset > ( fileSize + 100000 ) ) // ~100 kB { // TODO: allow contacts to resume at the correct byte position. kWarning() << "data offset is too far ahead of current end of file " "(filepos=" << file->pos() << " eofpos=" << file->size() << " offset=" << offset << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=send0x01)"; sendP2PAckImpl( P2PMessage::MSN_FLAG_NEGATIVE_ACK ); sendCancelMessage( CANCEL_FAILED ); // sends slp bye return false; } else if( offset <= fileSize ) { // seek( offset ) should only fail if the offset is beyond the end of the file. // If this is not the case, moving within the file failed so abort. kWarning() << "file pointer could not be set to message offset " "(filepos=" << file->pos() << " eofpos=" << file->size() << " offset=" << offset << " contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=sendcancel)"; sendCancelMessage( CANCEL_FAILED ); return false; } // Create padding to write at the end unsigned long remainingSize = offset - fileSize; QByteArray padding( 4096, 0x00 ); // Add padding at the end of the file. file->seek( fileSize ); while( remainingSize > 0 ) { if( file->write( padding, qMin( (unsigned long) 4096, remainingSize ) ) <= 0 ) { break; } remainingSize -= 4096; } // Make sure the file position is correct now. if( offset != (ulong)file->pos() ) { sendCancelMessage( CANCEL_FAILED ); return false; } } // Offsets are correct. // Write the data to the file, qint64 status = file->write( message.getData(), message.getDataSize() ); if( status == -1 ) { kWarning() << "Failed to write the data to the file " "(contact=" << getContactHandle() << " session=" << getSessionID() << " class=" << metaObject()->className() << " action=sendcancel)"; sendCancelMessage(CANCEL_FAILED); return false; } // Indicate success! return true; } #include "p2papplicationbase.moc"