/*************************************************************************** p2papplication.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 <QRegExp> #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 P2PApplication 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 */ 00061 P2PApplication::P2PApplication(ApplicationList *applicationList) : P2PApplicationBase( applicationList ), applicationList_( applicationList ), // local copy for now, don't want to expose it as protected. gotSlpMessage_(false), invitationCSeq_(0), invitationSessionID_(0), sessionID_(0), userShouldAcknowledge_(false) { // For debugging setObjectName( QLatin1String( metaObject()->className() ) + "[0/" + getContactHandle() + "]" ); } /** * @brief Class destructor. * * Cleans up buffers and timers. */ 00081 P2PApplication::~P2PApplication() { #ifdef KMESSDEBUG_APPLICATION kDebug() << " session=" << getSessionID(); #endif applicationList_ = 0; } /** * @brief The contact aborted the session * * This method is overwritten from the base class, so * endApplicationLater() is called instead of endApplication(). * This leaves the P2P session open for a short while, to accept any remaining * packets that could be sent (like TCP's TIME_WAIT state). * * @param message Optional message to display, defaults to getContactAbortMessage(). */ 00102 void P2PApplication::contactAborted(const QString &message) { #ifdef KMESSDEBUG_APPLICATION kDebug() << "state=" << (int) getWaitingState(); #endif // Make sure a second message is not displayed here. // The contact could be sending multiple error messages. if( isWaitingState( P2P_WAIT_END_APPLICATION ) ) { #ifdef KMESSDEBUG_APPLICATION kDebug() << "Abort the application again, " "application was already awaiting termination."; #endif // Make sure the timer is started again. endApplicationLater(); return; } else if( isClosing() ) { kWarning() << "Attempted to close application twice " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)"; // Make sure the timer is started again. endApplicationLater(); return; } if( isTransferComplete() ) { // Make sure the user won't get a "transfer complete" message // followed by a "contact aborted" message. // // This could happen if: // - WLM sends data // - KMess acks // - WLM closes the SB // - contactAborted() called for every app. // Also interesting: // - signout / signin with WLM. // - establish a SB connection again by starting a chat. // - The P2P session is still active in WLM, // and has a bridge now, so WLM sends the SLP BYE. // - kmess will ack it with a temporary p2p session. kWarning() << "not displaying message because data transfer is complete. " "The switchboard was likely closed after receiving all data. " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)"; } else { // Remove the accept links modifyOfferMessage(); if( message.isEmpty() ) { showEventMessage( getContactAbortMessage(), ChatMessage::CONTENT_APP_CANCELED, true ); } else { showEventMessage( message, ChatMessage::CONTENT_APP_CANCELED, true ); } } endApplicationLater(); } /** * @brief Step 1 of a contact-started chat: the contact invites the user * * This default implementation automatically declines the invitation with a "feature not supported" notification. * * Override this method to handle the invitation instead. * It can typically do one of the following things: * - parse the 'Context' field of the invitation and store the information. * - display the accept/cancel link with offerAcceptOrReject(). * - automatically accept the invitation by calling contactStarted2_UserAccepts(). * - decline the invitation by calling sendCancelMessage(). * * @param message The body (payload fields) of the SLP INVITE message, * which typically has the fields 'Context' and 'AppID'. * The 'SessionID' and 'EUF-GUID' fields are be processed automatically. */ 00196 void P2PApplication::contactStarted1_ContactInvitesUser(const MimeMessage &message) { #ifdef KMESSDEBUG_APPLICATION kDebug() << "(rejects invitation)"; #endif // We can assume the switchboard already displayed a better error message #ifdef KMESTEST kWarning() << "The Application subclass didn't implement contactStarted1_ContactInvitesUser!"; #endif // Send some debug info to the console MimeMessage slpMimeBody(message.getBody()); kWarning() << "The contact sent an MSNSLP invitation type KMess can't handle yet " "(euf-guid=" << slpMimeBody.getValue("EUF-GUID") << " appid=" << slpMimeBody.getValue("AppID") << " action=send500)."; // Display a warning to the user showSystemMessage( i18n("The contact has invited you to an activity not yet supported by KMess."), ChatMessage::CONTENT_SYSTEM_NOTICE, true ); // Cancel the invitation sendCancelMessage( CANCEL_NOT_INSTALLED ); // Unlike Application::contactStarted1_ContactInvitesUser() // we don't quit here!! We should wait for the BYE message from the other contact! } /** * @brief Step 4 of a contact-started chat: the contact confirms the data-preparation message. * * This method was added for P2P-style chats to support the last step of a P2P session. * When the contact requests to send data, call sendData() here to start the transfer. */ 00233 void P2PApplication::contactStarted4_ContactConfirmsPreparation() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug(); #endif } /** * @brief Return the identifier the SLP INVITE message. * @return The Branch-ID value from the SLP INVITE message. */ 00246 QString P2PApplication::getBranch() const { return branch_; } /** * @brief Return the identifier of the SLP session. * @return The Call-ID value from the SLP INVITE message. */ 00257 QString P2PApplication::getCallID() const { return callID_; } /** * @brief Return the Content-Type of the invitation message * * This method is useful for some contactStarted1_ContactInvitesUser() implementations. * * @return The Content-Type value, e.g. <tt>application/x-msnmsgr-sessionreqbody</tt>. */ 00271 const QString& P2PApplication::getInvitationContentType() const { return invitationContentType_; } /** * @brief Return the identifier of the SLP INVITE message. * * This method is useful for some contactStarted2_UserAccepts() implementations. * It's possible getSessionID() still returns zero, because the client should confirm the Session-ID first. * * @return The 'SessionID' field from the SLP payload of the invitation message. */ 00286 quint32 P2PApplication::getInvitationSessionID() const { return invitationSessionID_; } /** * @brief Return the session ID, this identifies the P2P session. * * It's the first field of the binary P2P header. * When a negotiation message is sent, it's session ID field will always be zero, * regardless of the real session ID. * * @return The session ID field used in the binary P2P header. */ 00302 quint32 P2PApplication::getSessionID() const { return sessionID_; } /** * @brief Parse the received ACK message. * * The messageType field is used to detect actions * like "contact ACKed the OK message" or "contact received all data". * * It handles the following ACK message types: * - ACK of a SLP INVITE message invokes gotAck_SlpSessionInvitation(). * - ACK of a SLP session OK message invokes gotAck_slpSessionOk(). * - ACK of a SLP tranfer INVITE message invokes gotAck_slpTransferInvitation(). * - ACK of a SLP tranfer OK message invokes gotAck_slpTransferOk(). * - ACK of a SLP transfer decline message invokes gotAck_slpTransferDecline(). * - ACK of the data preparation message invokes gotAck_dataPreparation() * - ACK of the sent file data invokes gotAck_dataReceived(). * - ACK of the SLP BYE message, invokes gotAck_slpBye(). * - ACK of a SLP error message invokes gotAck_slpError(). * - all other ACKs are not treated specially. * * @param message The ACK message. * @param ackedMessageType The type of the original message this ACK corresponds with. */ 00330 void P2PApplication::gotAck(const P2PMessage &message, const P2PMessageType ackedMessageType) { #ifndef KMESSTEST Q_UNUSED( message ); // Avoid compiler warning #endif #ifdef KMESSTEST if( message.isAck() ) // normal 0x02 ack { KMESS_ASSERT( message.getDataSize() == 0 ); KMESS_ASSERT( message.getTotalSize() == 0 ); KMESS_ASSERT( message.getAckSessionID() != 0 ); KMESS_ASSERT( message.getAckUniqueID() != 0 ); KMESS_ASSERT( message.getAckDataSize() != 0 ); } #endif switch( ackedMessageType ) { // Are we waiting for the contact to accept our invitation (e.g. send "200 OK")? case P2P_MSG_SESSION_INVITATION: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (INVITE ACK)."; #endif gotAck_slpSessionInvitation(); return; // Are we waiting for the ACK to confirm our "200 OK" message? case P2P_MSG_SESSION_OK: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (200 OK ACK)."; #endif gotAck_slpSessionOk(); return; // Are we waiting for the contact to accept our transfer invitation (e.g. send "200 OK")? case P2P_MSG_TRANSFER_INVITATION: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (INVITE transfer ACK)."; #endif gotAck_slpTransferInvitation(); return; // Are we waiting for the ACK to confirm our "200 OK" message? case P2P_MSG_TRANSFER_OK: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (200 transfer OK ACK)."; #endif gotAck_slpTransferOk(); return; // Are we waiting for the ACK to confirm our data preparation message? case P2P_MSG_DATA_PREPARATION: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (data-preparation ACK)."; #endif #ifdef KMESSTEST KMESS_ASSERT( message.getAckDataSize() == 4 ); #endif gotAck_dataPreparation(); return; // Are we waiting for the ACK to confirm that all data was received? case P2P_MSG_DATA: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (all-received ACK)."; #endif gotAck_dataReceived(); return; // Are we waiting for the BYE ACK to quit the session/application? case P2P_MSG_SESSION_BYE: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (BYE ACK, terminating)."; #endif gotAck_slpBye(); return; // Are we expecting a ACK for our decline of the transfer? case P2P_MSG_TRANSFER_DECLINE: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (transfer decline ACK)."; #endif gotAck_slpTransferDecline(); return; // Are we waiting for the SLP Error message to be ACK-ed? case P2P_MSG_SLP_ERROR: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message (Confirmed SLP Error)."; #endif gotAck_slpError(); return; // Standard ACK message or unhandled ACK message. default: #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an ACK message. " "waitingState=" << (int) getWaitingState() << " ackedMessageType=" << ackedMessageType << "."; #endif // See if the derived class needs to handle this ack. if( gotUnhandledAck( message, ackedMessageType ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Message is handled by gotUnhandledAck(), no further action needed."; #endif return; } // Avoid warnings when we're waiting for something else, and the ACK is not important. if( isWaitingState( P2P_WAIT_FOR_CONNECTION ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "No special action taken (waiting for a connection)."; #endif } else if( ackedMessageType != P2P_MSG_UNKNOWN || ! isWaitingState( P2P_WAIT_DEFAULT ) ) { // Unexpected, this should be a normal ACK message. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kWarning() << "Unhandled state for ACK message:" " waitingState=" << (int) getWaitingState() << " ackedMessageType=" << ackedMessageType << "!"; #endif // If we were waiting: the timer is stopped and // we forgot an if-block in this method. // So make sure the object will still be killed eventually. setWaitingState( getWaitingState(), P2PAPPLICATION_TIMEOUT_DATA ); return; } } } /** * @brief Called when the ACK for the data preparation was received. * * This invokes initiateTransfer(), unless the the application * waits for an ACK to a SLP transfer OK message. */ 00493 void P2PApplication::gotAck_dataPreparation() { #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) || isWaitingState( P2P_WAIT_FOR_CONNECTION ) ); #endif // Temporary switch state, if the derived class // sends a message this state will be changed. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // This should happen after the data preparation message is sent. // If the user sent the invitation, // we shouldn't receive the prepare-ack, but sent it. if(isUserStartedApp()) { // We received a data preparation ACK but didn't expect it. kWarning() << "P2P message can't be handled, " "this ack is not expected for applications started by the user " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; // Send error back. sendP2PAbort(); showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, false ); endApplicationLater(); return; } // Keep waiting if a direct connection setup is about to be complete. if( hasUnAckedMessage( P2P_MSG_TRANSFER_OK ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Application is waiting for a 200 transfer OK ACK, " "not initiating transfer yet."; #endif setWaitingState( P2P_WAIT_FOR_TRANSFER_ACK, P2PAPPLICATION_TIMEOUT_SLP ); return; } // Dispatch to derived implementation initiateTransfer(); } /** * @brief Called when the ACK for the sent file data was received. * * This method invokes showTransferComplete(), and sends the SLP BYE if needed. */ 00550 void P2PApplication::gotAck_dataReceived() { #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_DATA_ACK ) ); #endif if(isUserStartedApp()) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "All data sent and confirmed, " "sending BYE message..."; #endif // We sent the INVITE, now we send the BYE. sendSlpBye(); // All data was acked // Telling derived class the transfer is complete. showTransferComplete(); // Don't terminate yet, that message needs to be ACK-ed as well. } else { #ifdef gotAck_dataReceived kDebug() << "All data sent and confirmed, " "waiting for BYE message..."; #endif // All data was acked, this is good enough. // The remaining teardown happens behind the scenes. showTransferComplete(); // The other contact sent the INVITE, so we also wait for the BYE message. // This happens for msnobject transfers for example. setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP ); } } /** * @brief Called when the ACK for the SLP BYE message was received. * * This schedules the application for termination. */ 00596 void P2PApplication::gotAck_slpBye() { if( ! isWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK ) ) { kWarning() << "Unexpected SLP BYE received!"; } // Test whether the data transfer to the contact was aborted. testDataSendingAborted(); // Session complete. endApplicationLater(); } /** * @brief Called when the ACK for a SLP Error was received. * * if the contact started the application, this will wait for the SLP BYE message. * Otherwise the application is terminated. */ 00618 void P2PApplication::gotAck_slpError() { #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) ); #endif if(! isUserStartedApp()) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "The contact started this application, waiting for BYE."; #endif // We wait for the BYE setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP ); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "The user started this application, terminating."; #endif // TODO: send BYE after the client confirmed our SLP error? endApplicationLater(); } } /** * @brief Called when the ACK for the first SLP INVITE message was received. * * This only changes the waiting state, to wait for the SLP OK message. */ 00651 void P2PApplication::gotAck_slpSessionInvitation() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for 200 OK message..."; #endif // Continue to wait for the contact to send the accept message (SLP OK). setWaitingState( P2P_WAIT_CONTACT_ACCEPT, P2PAPPLICATION_TIMEOUT_ACCEPT ); stopWaitingTimer(); } /** * @brief Called when the ACK of the SLP OK message was received. * * This method invokes contactStarted3_ContactConfirmsAccept() */ 00669 void P2PApplication::gotAck_slpSessionOk() { // This should happen after the "200 OK" message is sent. // If the user started the inviation, // we send the SLP OK ack, not receive it if( isUserStartedApp() ) { // We received an 200 OK ACK but didn't expect it. // This is very unlikely, but still prevent worse situations when contactStarted3_.. is invoked(). kWarning() << "P2P message can't be handled, " "this ack is not expected for applications started by the user " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; // Show message showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, false ); // Abort sendCancelMessage( CANCEL_ABORT ); return; } // Reset, will be changed by initiateTransfer->contactStarted3..->sendDataPreparationAck. // TODO: rewrite the sending of the data-preparation message using the incoming/outgoing queue. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // SLP OK message ACKed // For P2P apps, we can't prove any mime fields here, so an empty constructor is what's left.. // This is where the data preparation ACK could be sent. contactStarted3_ContactConfirmsAccept( MimeMessage() ); #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) || isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) ); #endif // If the contactStarted3() didn't sent anything, we'll wait for incoming file data. if( isWaitingState( P2P_WAIT_DEFAULT ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting state not changed, " "waiting for file data..."; #endif // For Pictures, the contact must send the data preparation ACK at this point (P2P_WAIT_FOR_PREPARE_ACK) // For Files, the contact must initiate the data transfer now. (P2P_WAIT_DEFAULT) setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA ); } } /** * @brief Called when the ACK for the SLP transfer decline message was received. * * This only changes the waiting state, * the contact should start to send the data now. */ 00729 void P2PApplication::gotAck_slpTransferDecline() { #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) || isWaitingState( P2P_WAIT_FOR_FILE_DATA ) ); #endif if( isTransferActive() || isWaitingState( P2P_WAIT_FOR_FILE_DATA ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got ack while data transfer is already active."; #endif } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for file data..."; #endif // Continue to wait for the contact to send the file data setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA ); } } /** * @brief Called when the ACK for the SLP transfer INVITE message was received. * * This only changes the waiting state, to wait for the SLP OK message. */ 00759 void P2PApplication::gotAck_slpTransferInvitation() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for 200 OK message..."; #endif #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_INVITE_TR_ACK ) ); #endif // Continue to wait for the contact to send the accept message (SLP OK). setWaitingState( P2P_WAIT_FOR_TRANSFER_ACCEPT, P2PAPPLICATION_TIMEOUT_ACCEPT ); // note KMess uses a fixed time user to accept/decline here. // WLM8 has no timeout here for file transfer. } /** * @brief Called when the ACK for the SLP transfer OK mesages was received. * * This invokes initiateTransfer() or waits * for an other direct connection to complete. */ 00783 void P2PApplication::gotAck_slpTransferOk() { // For some reason, our "MSNSLP/1.0 200 OK" message can be ACKed when we just ACKed all data is received. // Detect this, otherwise the chat window is filled with "File transfer complete.. Contact aborted" messages. if( isTransferComplete() || isWaitingState( P2P_WAIT_FOR_SLP_BYE ) ) { kWarning() << "Received ACK for 'transrespbody' message " "while closing the session " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=return)."; // Make sure the timer starts again. setWaitingState( getWaitingState(), P2PAPPLICATION_TIMEOUT_SLP ); return; } // Temporary switch the waiting state. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // This should happen after the "200 OK" message is sent. // SLP ransfer OK message ACKed if( ! applicationList_->hasDirectConnection() && ! applicationList_->hasPendingConnections() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "No connection attempts were made, " "so starting transfer immediately."; #endif // TODO: the contact may actually send an INVITE back to become the server. initiateTransfer(); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "200 transfer OK ACK received, " "but waiting for connection attempts to succeed or fail..."; #endif } // If the contactStarted3() didn't sent anything, we'll wait for incoming file data. if( isWaitingState( P2P_WAIT_DEFAULT ) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting state not changed, " "waiting for file data..."; #endif // At some slow systems, it seams WLM can already start // the data transfer before waiting for a connection. if( isTransferActive() ) { kWarning() << "already received " << getTransferredBytes() << " bytes before receiving the 200 transfer OK ACK " "(state=" << (int) getWaitingState() << " dc=" << applicationList_->hasDirectConnection() << " dcauth=" << applicationList_->hasAuthorizedDirectConnection() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)."; } // For Files, the contact must initiate the data transfer now. (P2P_WAIT_DEFAULT) setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA ); } } /** * @brief Called when data is received. * * This method should be overwritten in the derived class * to store the data in a file, buffer, etc.. * * The order of the data fragments is not guaranteed by WLM, especially when it's switching to a direct connection. * The writeP2PDataToFile() function can be used to cope with this behavour. * Also don't rely on P2PMessage::isLastFragment() to determine when the transfer is complete. * Overwrite showTransferComplete() instead. * * @param message The P2P message containing the data. */ 00868 void P2PApplication::gotData(const P2PMessage &/*message*/) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kWarning() << "Data not handeled by derived class."; #endif } /** * @brief Called internally after all data is received. * * It determines how to continue the session. * * It's possible to overwrite this method. * However don't forget to class this parent method too * if you want to make sure the BYE message is sent/received properly. */ 00886 void P2PApplication::gotDataComplete( const P2PMessage &lastMessage ) { Q_UNUSED( lastMessage ); // Determine what to send next if(isUserStartedApp()) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Application was started by the user, also send SLP BYE."; #endif // Current state could be P2P_WAIT_FOR_FILE_DATA, but reset it here. // Make the debugging clean. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // We sent the INVITE, now we send the BYE. sendSlpBye(); // Don't terminate yet, that message needs to be ACK-ed as well. } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Application was started by the contact, waiting for SLP BYE..."; #endif // Otherwise we wait for the BYE setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP ); } } /** * @brief Got an direct connection handshake ack. * * This method verifies the Nonce field and marks the connection as authenticated. * If the Nonce field is invalid, the connection will be closed without any notice. * * @param message The handshake message received from the direct connection. */ 00926 void P2PApplication::gotDirectConnectionHandshake(const P2PMessage &message) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got a direct connection handshake message."; #endif // If the contact was sending a handshake when we dropped the connection, // it's possible this packet will be delivered over the switchboard. if( ! applicationList_->hasDirectConnection() ) { // Ignore the packet kWarning() << "Received direct connection handshake at the switchboard " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=return)."; return; } MsnDirectConnection *directConnection = applicationList_->getDirectConnection(); // Get connection info QString nonce ( message.getNonce() ); bool isServer = directConnection->isServer(); // Verify the nonce // Nonce is already used in ApplicationList to find this application, // but this is kept here for consistency, and if ApplicationList would change one time. if( nonce != nonce_ ) { // Invalid nonce! Drop connection kWarning() << "Contact sent invalid nonce " "(received=" << nonce << " expected=" << nonce_ << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=closeconnection)."; #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Closing connection, moving transfer to switchboard."; #endif // Like the official client, close the connection without any notice. // The transfer continues over the switchboard instead. directConnection->closeConnection(); // DirectConnectionPool deletes it. } else if( isServer ) { // If we're the server, we still need to send a handshake back #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Contact sent correct nonce, " "sending direct connection handshake ACK."; #endif #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_HANDSHAKE ) || isTransferActive() ); #endif // KMess is the server, it should authenticate the handshake message. sendDirectConnectionHandshake( nonce_ ); } else { // KMess is the client, it should verify the handshake response only. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Contact sent correct nonce, " "marking connection as authorized."; #endif #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_HANDSHAKE_OK ) || isTransferActive() ); #endif // Detect when the contact sent a second INVITE, runs the direct connection server, but sends the nonce first. // This happened once with WLM8 after the data transfer because KMess didn't send the handshake. if( isWaitingState( P2P_WAIT_FOR_HANDSHAKE ) ) { kWarning() << "received nonce from contact first, " "but it's running the server" "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x100)!"; sendDirectConnectionHandshake( nonce_ ); } } // See if the transfer was already started. if( isTransferComplete() ) { kWarning() << "connection is authorized " "but data transfer is already complete" "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x100)!"; } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "marking connection as authorized, transfer should start."; #endif // Note this could be a reverse invitation. setWaitingState( P2P_WAIT_FOR_FILE_DATA, 0 ); // no timeout, happens by DirectConnectionPool. } // Mark connection as authorized // This also starts signals which call slotConnectionAuthorized() directConnection->setAuthorized(true); } /** * @brief Called when the data preparation message is received. * * It calls initiateTransfer() to inform the derived class. * When sendDataPreparationAck() is not called by the derived class, * the session will abort automatically. * * @param message The P2P data preparation message. */ 01053 void P2PApplication::gotDataPreparation(const P2PMessage &/*message*/) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got the data preparation message"; #endif #ifdef KMESSTEST KMESS_ASSERT( ! isP2PAckSent() ); // WLM invites for a DC first before sending the data preparation. // Since KMess declines this with a 603, P2P_WAIT_FOR_SLP_ERR_ACK is also a valid state here. KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_PREPARE ) || isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) ); #endif // Tell the derived class it can send the data. // You typically want to call sendDataPreparation() or sendCancelMessage() here. // sendDataPreparationAck() only sets a boolean, so we're able to send the // correct ack or error message here. The methods changes one the following variable: userShouldAcknowledge_ = true; // Let the derived class call the methods. initiateTransfer(); // if sendDataPreparation() was not called, determine now what to send: if( userShouldAcknowledge_ ) { kDebug() << "Handling of data preparation failed " "(contact=" << getContactHandle() << " action=send0x08)"; // Derived class still didn't acknowledge or cancel at all // (both disable userShouldAcknowledge_). This shouldn't occur! // Reject the data-preparation message showEventMessage( i18n("The transfer failed. Data preparation failed."), ChatMessage::CONTENT_APP_FAILED, false ); // Initiate abort. sendP2PAbort(); return; } // If the user did acknowledge something, we no longer have to send an ack any more // it must have been sendCancelMessage(CANCEL_ABORT) calling sendP2PAbort(). if( isP2PAckSent() ) { // No error message here, assume your derived class already displayed something. // If sendP2PAck() was called, the app won't terminate, so end it now. endApplicationLater(); return; } // TODO: for WLM8, we need to delay the P2P ack message until the DC is established. // User did acknowledge, we can send the *actual* preparation ACK now. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Data preparation successful, sending ACK"; #endif sendP2PAck(); // Now wait for the data to arrive #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for file data to arrive..."; #endif setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA ); } /** * @brief Called when an complete SLP message is received. * * These messages are used to negotiate session parameters. * The SLP message itself also has a MIME header and MIME body. * * This method validates the 'To' field, * and dispatches the to the correct handler: * - messages starting with <tt>INVITE MSNMSGR</tt> are delivered to gotSlpInvite(). * - messages starting with <tt>MSNSLP/</tt> are delivered to gotSlpStatus(). * - messages starting with <tt>BYE MSNMSGR</tt> are delivered to gotSlpBye(). * - other messages are rejected with an P2P error. * * The P2P message is not ACKed yet, each handler does this * when it's able to parse the SLP body. * * A preamble could look like one of the following: * @code INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 @endcode @code MSNSLP/1.0 200 OK @endcode @code BYE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 @endcode * * The message itself looks like: * @code To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> Via: MSNSLP/1.0/TLP ;branch={068676F2-9B0C-4945-8ABA-4E6F585F710F} CSeq: 0 Call-ID: {E9BCFBDB-2768-4BD3-9E4C-A97DC70261E8} Max-Forwards: 0 Content-Type: application/x-msnmsgr-sessionreqbody Content-Length: 348 {the message contents} @endcode * * @param slpMessage The payload extracted from the P2P message fragments (the MIME fields and body). * @param preamble The first status line sent before the MIME fields, which describes the message type. */ 01168 void P2PApplication::gotNegotiationMessage(const MimeMessage &slpMessage, const QString &preamble) { // Parse the "negotiation message" (P2PMessage with sessionID 0) // Verify it is indeed directed to us.. QString msgTo( slpMessage.getValue("To") ); if(! msgTo.isEmpty()) { // Only test if the field is available. If it's not, either something // is wrong in this application, or the client sent a bad message and // we should respond with an 500 error instead. // For some reason this can be received in upper case too if the user entered it that way (likely happens with 3rd party clients). if( msgTo.toLower() != "<msnmsgr:" + CurrentAccount::instance()->getHandle().toLower() + ">" ) { // In the future, the <msnmsgr: > string might be replaced with // something to support MSN to AOL chats, etc.. kWarning() << "P2P message can't be handled, " "addressed to someone else " "(to=" << msgTo << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send404)."; showEventMessage( i18n("The invitation was cancelled. Message was not directed to us."), ChatMessage::CONTENT_APP_CANCELED, true ); // Send error message back sendSlpError("404 Not Found"); // Waits for ACK and terminates return; } } #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got an SLP message, preamble=" << preamble << "."; #endif // Find out what kind of message this is: if( preamble.startsWith("INVITE MSNMSGR") ) { // Got an invitation! gotSlpInvite( slpMessage ); } else if( preamble.startsWith("MSNSLP/") ) { // Error message, or "MSNSLP/1.0 200 OK" message gotSlpStatus( slpMessage, preamble ); } else if( preamble.startsWith("ACK MSNMSGR") ) { // Got a SLP ACK message (Windows Live Messenger feature) gotSlpAck( slpMessage ); } else if( preamble.startsWith("BYE MSNMSGR") ) { // Got a BYE message gotSlpBye( slpMessage ); } else { // Unknown SLP message kWarning() << "P2P message can't be handled, " "unsuppored SLP negotiation message " "(preamble=" << preamble << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; // Got an unknown SLP preamble sendP2PAbort(); // Abort the application showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); return; } } /** * @brief Called when an SLP ACK message was received. * * This message should not be confused with a P2P ACK. * * Windows Live Messenger uses this message to acknowlegde * IP/port information for a direct connection. * * This method is currently a STUB to keep Windows Live Messenger happy. * * One of the observed messages is: * @code ACK MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> Via: MSNSLP/1.0/TLP ;branch={67683AAA-A6F2-4B11-895F-EF46B483F3A9} CSeq: 0 Call-ID: {00000000-0000-0000-0000-000000000000} Max-Forwards: 0 Content-Type: application/x-msnmsgr-transudpswitch Content-Length: 157 IPv4ExternalAddrsAndPorts: 111.222.333.444:3495 IPv4InternalAddrsAndPorts: 192.168.1.12:3495 SessionID: 15076776 SChannelState: 0 Capabilities-Flags: 1 @endcode * * @code ACK MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> Via: MSNSLP/1.0/TLP ;branch={BBE6AAC0-10F4-4419-B01C-1F7C2838BC3D} CSeq: 0 Call-ID: {00000000-0000-0000-0000-000000000000} Max-Forwards: 0 Content-Type: application/x-msnmsgr-transdestaddrupdate Content-Length: 197\r\n \r\n IPv4ExternalAddrsAndPorts: 11.222.3.44:62570\r\n IPv4InternalAddrsAndPorts: 192.168.1.69:1343\r\n IPv4External-Connecting-Port-End-Range: 62576\r\n SessionID: 15751111\r\n SChannelState: 0 Capabilities-Flags: 1 @endcode * * @param slpMessage The parsed SLP message. */ 01299 void P2PApplication::gotSlpAck(const MimeMessage &slpMessage ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got SLP ACK message, usage is unknown. Message dump follows.\n" << slpMessage.getFields() << "\n" << slpMessage.getBody(); #else Q_UNUSED( slpMessage ); #endif bool emptySession = ( ! isFirstMessageSent() || getMode() == APP_MODE_ERROR_HANDLER ); // This is some internal message of WLM8. // Accept it for now. sendP2PAck(); // Also terminate this instance if it was just created for this SLP ACK messasge. if( emptySession ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Directly terminating temporary P2PApplication instance for SLP ACK message."; #endif endApplication(); } } /** * @brief Called when an SLP BYE message was received. * * It sends the ACK back, and calls endApplication() to terminate the session. * A contact may sent a BYE message to abort the transfer. * In this case, contactAborted() will be called instead. * * The SLP BYE message looks like: * @code BYE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> Via: MSNSLP/1.0/TLP ;branch={A1DBCADF-6DA2-43FE-BE27-0AEE26816D91} CSeq: 0 Call-ID: {E9BCFBDB-2768-4BD3-9E4C-A97DC70261E8} Max-Forwards: 0 Content-Type: application/x-msnmsgr-sessionclosebody Content-Length: 40 SessionID: 114164 SChannelState: 0 @endcode * * @param slpMessage The parsed SLP message. */ 01352 void P2PApplication::gotSlpBye( const MimeMessage &slpMessage ) { Q_UNUSED( slpMessage ); // Avoid compiler warning // If the BYE was received early, it means the session was aborted prematurely. // // There appears to be a small bug in MSN 7.0. It doesn't sent the final "all data received" ACK, // but sends the BYE directly. After we ACK it, we get a "BYE sent" message back. // This issue appears to be fixed in MSN 7.5. // // Microsoft Messenger for the Mac (6.0.3) takes this even further with msnobject transfers. // It sends the BYE first and the "all data received" ACK afterwards. // This happens when KMess is already in the P2P_WAIT_END_APPLICATION state (see "action=unsetclosing"). // Also terminate without errors if we're waiting for a BYE ACK and instead receive another BYE. This // means that both clients have finished sending, so don't issue errors. This fixes the double message // "transfer completed" followed by "transfer canceled" when sending files to Mercury Messenger. setCurrentMessageType( P2P_MSG_SESSION_BYE ); sendP2PAck(); bool isComplete = isTransferComplete(); // TODO: this only works for incoming data. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got SLP BYE message, closing application " "(state=" << (int) getWaitingState() << ")."; #endif // Make sure the data is not sent anymore abortDataSending(); if( ! isWaitingState( P2P_WAIT_FOR_SLP_BYE ) && ! isWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK ) && ! isWaitingState( P2P_WAIT_FOR_DATA_ACK ) ) // HACK: added for Messenger for the Mac 6.0.3 and MSN 7.0 { // Make a final check if our states have been messed up. Had the following situation once: // - WLM invites to send a file // - KMess hosted the server socket. // - WLM started to send data. // - transfer was complete, acked, waiting for BYE // - WLM establishes the connection with the socket. // - connection is authenticated. // - sockets are disconnected // - WLM sends BYE if( isComplete ) { kWarning() << "Transfer seems complete but state suggests otherwise " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)"; endApplicationLater(); } else if( isWaitingState( P2P_WAIT_END_APPLICATION ) ) { // When WLM aborts at a very late stage, it actually processes our final data messages, // ack's those, handle our BYE, and send it's "aborting BYE" afterwards. kWarning() << "Received BYE to abort when all data is already transferred " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)"; // Resume timer endApplicationLater(); } #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "BYE message was unexpected; contact aborted " << "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << ")"; #endif // End with failure, bye sent at a different moment. contactAborted(); // closes the application // TODO: after this, the contact should send a 0x40 or 0x80 ack of the data stream. // the message flag indicates whether the contact aborted it's own stream, or aborted ours. } else { // Normal end endApplicationLater(); } } /** * @brief Called when a SLP INVITE message was received. * * It extracts the header fields, updates the instance fields, and parses the MIME body. * The invitation message is delivered to one of the following methods: * - The <tt>application/x-msnmsgr-sessionreqbody</tt> message is delivered to gotSlpSessionInvitation(). * - The <tt>application/x-msnmsgr-transreqbody</tt> message is delivered to gotSlpTransferInvitation(). * - The <tt>application/x-msnmsgr-transrespbody</tt> message is delivered to gotSlpTransferResponse(), * with the <tt>secondInvite</tt> parameter set to <tt>true</tt>. * - Other messages are rejected as "unrecognized content-type". * * @param slpMessage The parsed SLP message. */ 01455 void P2PApplication::gotSlpInvite(const MimeMessage &slpMessage) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got SLP INVITE message"; #endif #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) // First invitation || isWaitingState( P2P_WAIT_FOR_FILE_DATA ) // Second invitation for file transfers. || isWaitingState( P2P_WAIT_FOR_PREPARE ) // WLM8: reverse invitation for msnobject transfer, holds back it's data-preparation message. || isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) // WLM8: second invitation for msnobject transfer, we already sent data-preparation ack. ); #endif // Reset the waiting state if this is the second INVITE for a file transfer // TODO: test what to do for the new WLM8 feature (sending transfer invites before data preparation). if( isWaitingState( P2P_WAIT_FOR_FILE_DATA ) ) { setWaitingState( P2P_WAIT_DEFAULT, 0 ); } // Indicate this is an SLP message (sendCancelMessage() uses this) // This value is reset once a message is sent. gotSlpMessage_ = true; // Session invitation // Extract the fields of the message. This is required for userRejected() MimeMessage slpMimeBody( slpMessage.getBody() ); // Set global fields from INVITE message. QString slpVia ( slpMessage.getValue("Via") ); callID_ = slpMessage.getValue("Call-ID"); invitationCSeq_ = slpMessage.getValue("CSeq").toInt(); invitationContentType_ = slpMessage.getValue("Content-Type"); // Extract branch from the "Via" parameter QRegExp callRE(";branch=(.+)"); // don't use a guid-pattern here, msn6 seams to accept random strings. callRE.indexIn( slpVia ); branch_ = callRE.cap(1); // Don't forget the initialize the base class // The cookie is empty if this is a contact-started session. if( getCookie().isEmpty() ) { startByInvite(generateCookie()); } // Handle invitation based on the Content-Type field if( invitationContentType_ == "application/x-msnmsgr-sessionreqbody" ) { // Send the ACK message setCurrentMessageType( P2P_MSG_SESSION_INVITATION ); sendP2PAck(); // Client invites us for a session // Will set the waitingState to P2P_WAIT_USER_ACCEPT later. gotSlpSessionInvitation( slpMimeBody ); } else if( invitationContentType_ == "application/x-msnmsgr-transreqbody" ) { // Send the ACK message setCurrentMessageType( P2P_MSG_TRANSFER_INVITATION ); sendP2PAck(); // Client requested to transfer the session // Handle the invitation internally gotSlpTransferInvitation( slpMimeBody ); } else if( invitationContentType_ == "application/x-msnmsgr-transrespbody" ) { // Send the ACK message setCurrentMessageType( P2P_MSG_TRANSFER_INVITATION ); sendP2PAck(); // Client sent a second invitation to become a direct-connection server because we can't be. gotSlpTransferResponse( slpMimeBody, true ); } else { // Send the ACK message sendP2PAck(); // Client sent an unknown invitation type kWarning() << "Received unexpected Content-Type " "(type=" << invitationContentType_ << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send500)."; // Indicate we don't like that content-type: sendCancelMessage(CANCEL_INVALID_SLP_CONTENT_TYPE); // Don't QUIT, the error will be ACK-ed. } // Reset state again. // So if the base class calls sendCancelMessage() outside // this method, it won't respond with a SLP message. gotSlpMessage_ = false; } /** * @brief Called when a SLP 200 OK message was received. * * This method is invoked from gotSlpStatus(). * It extracts the Content-Type status code, depending on the value it proceeds with a different action: * - The payload of the <tt>application/x-msnmsgr-sessionreqbody</tt> message * is delivered to userStarted2_ContactAccepts(). * - The payload of the <tt>application/x-msnmsgr-transrespbody</tt> message * is delivered to gotSlpTransferResponse(). * - Any other message is rejected with a SLP error. * * A session confirmation looks like: * @code MSNSLP/1.0 200 OK To: <msnmsgr:contact@kmessdemo.org> From: <msnmsgr:user@kmessdemo.org> ... Content-Type: application/x-msnmsgr-sessionreqbody Content-Length: 22 SessionID: 114164 @endcode * * For an example of a transfer confirmation, see gotSlpTransferResponse(). * * @param slpMessage The parsed SLP message. */ 01587 void P2PApplication::gotSlpOk(const MimeMessage &slpMessage) { // With a 200 code, we reset the waiting state for every situation, // to make the next check work. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // Parse the data of the message QString contentType( slpMessage.getValue("Content-Type") ); MimeMessage slpMimeBody( slpMessage.getBody() ); // body consists of more mime fields // The content-type tells us which fields are present if( contentType == "application/x-msnmsgr-sessionreqbody" ) { // Message confirms the SessionID #ifdef KMESSTEST KMESS_ASSERT( slpMimeBody.getValue("SessionID").toULong() == getSessionID() ); #endif // Send ACK now, with proper message type. setCurrentMessageType( P2P_MSG_SESSION_OK ); sendP2PAck(); // Tell the derived class the invitation was accepted userStarted2_ContactAccepts(slpMimeBody); } else if( (contentType == "application/x-msnmsgr-transrespbody") || // HACK: allow transreqbody too for broken clients, including KMess 1.4.2: (contentType == "application/x-msnmsgr-transreqbody" && slpMimeBody.hasField("Nonce") )) { // Send ACK now, with proper message type. setCurrentMessageType( P2P_MSG_TRANSFER_OK ); sendP2PAck(); // Message tells us how we can create the direct connection. // This response is handled here, so it's transparent for the derived class. gotSlpTransferResponse( slpMimeBody ); } else { sendP2PAck(); // Unsupported response type. kWarning() << "Received unexpected 200/OK message " "(type=" << contentType << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send500)!"; showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); // Send error message back sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE ); return; } // Determine whether we should be waiting for the data-preparation message. // If we didn't send anything that changed the state and no transfer has started, // we're likely waiting for some kind of prepare message if( isWaitingState( P2P_WAIT_DEFAULT ) && ! isTransferActive() ) { // Waiting for prepare message #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send some prepare message..."; #endif setWaitingState( P2P_WAIT_FOR_PREPARE, P2PAPPLICATION_TIMEOUT_ACK ); } } /** * @brief Called when a SLP session invitation was received. * * This method stores the session ID and * invokes contactStarted1_ContactInvitesUser(). * That method should parse the 'Context' field to handle the invitation. * * An SLP INVITE message for a msnobject transfer looks like: * @code INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> ... Content-Type: application/x-msnmsgr-sessionreqbody Content-Length: 348 EUF-GUID: {A4268EEC-FEC5-49E5-95C3-F126696BDBF6} SessionID: 114164 AppID: 12 Context: PG1zbm9ia.. @endcode * * @param slpMimeBody The SLP message with the fields 'EUF-GUID', 'SessionID', 'AppID' and 'Context'. */ 01685 void P2PApplication::gotSlpSessionInvitation( const MimeMessage &slpMimeBody ) { // Read requested session id from the message invitationSessionID_ = slpMimeBody.getValue("SessionID").toUInt(); // Don't accept invites if we sent one. if( ! isFirstMessageSent() && isUserStartedApp() ) { kWarning() << "Got an INVITE response for " "user-started application, rejecting " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send500)."; sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE ); return; } // For debugging setObjectName( QLatin1String( metaObject()->className() ) + "[" + QString::number( invitationSessionID_ ) + "/" + getContactHandle() + "]" ); // Mark waiting state for user. // Don't set a timeout just as WLM. setWaitingState( P2P_WAIT_USER_ACCEPT, 0 ); // state is used later to abort with 500 instead of BYE. // Tell the derived class we've got an invitation contactStarted1_ContactInvitesUser( slpMimeBody ); } /** * @brief Called when a SLP status message was received. * * This method extracts the status code, depending on the status code it proceeds with a different action: * - The <tt>200 OK</tt> message invokes gotSlpOk(). * - The <tt>404 Not Found</tt> message invokes contactAborted(). * - THe <tt>481 No Such Call</tt> message invotes contactAborted(). * - The <tt>500 Internal Error</tt> message invokes contactAborted(). * - The <tt>603 Decline</tt> message invokes contactRejected(). * - Any other message is rejected with a P2P error. * * @param slpMessage The parsed SLP message. * @param preamble The status line sent before the MIME fields, * which contains the error code (much like HTTP status codes). */ 01733 void P2PApplication::gotSlpStatus(const MimeMessage &slpMessage, const QString &preamble) { // An Error message, or "MSNSLP/1.0 200 OK" message #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Got SLP status message (" << preamble << ")"; #endif // See if this object was initialized to handle a Bad packet. if(getMode() == APP_MODE_ERROR_HANDLER) { // This object was initialized to send an error response for an unknown packet (at SLP level). showSystemMessage( i18n("The transfer failed. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_SYSTEM_NOTICE, true ); sendSlpError("500 Internal Error"); // Waits for ACK and terminates return; } // Should only be received if we started it. if(! isUserStartedApp()) { kWarning() << "P2P message can't be handled, " "unexpected SLP response containing status code " "(preamble=" << preamble << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; sendP2PAbort(); // Abort the application showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); return; } // Parse the preamble QRegExp re("MSNSLP/\\d+\\.\\d+ (\\d+) ([^\r\n]+)"); re.indexIn( preamble ); int code = re.cap(1).toUInt(); QString detail( re.cap(2) ); // Reset the waiting state if we were waiting. if( isWaitingState( P2P_WAIT_CONTACT_ACCEPT ) ) { setWaitingState( P2P_WAIT_DEFAULT, 0 ); } // Handle the status codes switch( code ) { // If the invitation was accepted case 200: // We don't need to do much here: // The message will be ACK-ed with the correct session ID automatically // because that ID was given in the sendSlpInvitation() method. // That will be the first ACK to be sent with the SessionID set. // NOTE: sendAck() is not called here, but in gotSlpOk() so // it can set setCurrentMessageType() before the sendAck() call. // Parse in separate function gotSlpOk( slpMessage ); break; // We sent a bad address in the to: header case 404: setCurrentMessageType( P2P_MSG_SLP_ERROR ); sendP2PAck(); // Inform the user KMess has an error kWarning() << "KMess sent a bad address in the 'to:' header " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=contactaborted)."; contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") ); break; // Somehow a message was sent twice (e.g. BYE) // or the contact aborts exactly when all data is being received + acked (so we sent the BYE). case 481: // No Such Call setCurrentMessageType( P2P_MSG_SLP_ERROR ); sendP2PAck(); // Content-Type of this message is "application/x-msnmsgr-session-failure-respbody" // Inform the user KMess has an error kWarning() << "KMess sent an invalid Call-ID " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=contactaborted)."; contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") ); break; // Something was not supported case 500: setCurrentMessageType( P2P_MSG_SLP_ERROR ); sendP2PAck(); // Content-Type of this message is application/x-msnmsgr-session-failure-respbody if( slpMessage.getValue("Content-Type") == "null" ) { // The Content-Type was not supported kWarning() << "Content-Type of our invitation was not understood " "(type=" << invitationContentType_ << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=contactaborted)!"; } else { // Bad header sent in SLP fields, or invitation type was not supported kWarning() << "Our invitation was not supported " "or the contact client had an internal error " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=contactaborted)"; } contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") ); break; // The invitation was declined case 603: setCurrentMessageType( P2P_MSG_SESSION_DECLINE ); // FIXME test if it's a transfer decline or not (like gotSlpOk()). sendP2PAck(); // Contact Declined contactRejected(); break; // Last option: tell the contact it sent an unknown message default: kWarning() << "P2P message can't be handled, " "unsupported SLP response " "(preamble=" << preamble << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; // Got an unknown SLP status code sendP2PAbort(); // Scheduelle the application to terminate showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true ); } } /** * @brief Called when a SLP transfer invitation was received. * * This is typically received when the contact initiated the file transfer session. * The Content-Type of the message <tt>application/x-msnmsgr-transreqbody</tt>. * The message contains meta-information about the contact's network configuration. * * A typical incoming message looks like: * @code INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> ... Content-Type: application/x-msnmsgr-transreqbody Content-Length: 193 Bridges: TRUDPv1 TCPv1 NetID: -789490475 Conn-Type: IP-Restrict-NAT UPnPNat: true ICF: false Hashed-Nonce: {754C0129-F286-4882-5793-70BA45D62B6A} SessionID: 114164 SChannelState: 0 @endcode * * As of Windows Live Messenger 8.1, the message body looks like: @code Bridges: TRUDPv1 TCPv1 SBBridge TURNv1 NetID: 140808277 Conn-Type: Port-Restrict-NAT TCP-Conn-Type: Port-Restrict-NAT UPnPNat: false ICF: false Hashed-Nonce: {78F2E9F6-06B7-9080-1D83-A222F143F681} SessionID: 2674976 SChannelState: 0 Capabilities-Flags: 1 @endcode * * Currently the <tt>Hashed-Nonce</tt> is ignored, a normal <tt>Nonce</tt> is sent back. * * This method attempts to create a server socket and send the possible connection options back to the contact. * The response message also contains a <tt>Listening</tt> field * and supported transport layers (<tt>Bridge</tt> field). * It optionally contains one of the following: * - a generated nonce, the internal/external IP addresses (<tt>IPv4Internal-Addrs</tt> and <tt>IPv4external-Addrs</tt> values), or * - a null nonce to indicate the file data must be sent over the switchboard connection. * * See gotSlpTransferResponse() for an example message. * * The other contact can decide to start a server socket instead (sending a second invitation message), * or transfer the file data over the switchboard. * * @param slpMimeBody The SLP message with the fields "Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF". */ 01949 void P2PApplication::gotSlpTransferInvitation(const MimeMessage &slpMimeBody) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Invite message is a request to transfer the session."; #endif // For some reason it's also possible to receive SLP transfer invitations from WLM8 // with an entirely different callid while trying to setup a direct connection. // Perhaps some packet is stalled in the outgoing message channel? if( sessionID_ == 0 || getMode() == APP_MODE_ERROR_HANDLER ) { kWarning() << "Received an invitation for a direct connection," " but no session is set up " "(maybe an old session, " " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send603)."; sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE); return; } // See if there is already a connection attempt pending. // This really happened with WLM8.1, somehow it sent multiple connection invites // the the contact, even for the same session ID. if( applicationList_->hasPendingConnections() ) { kWarning() << "Received an invitation for a direct connection," " while a connection attempt is already in progress " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send603)."; sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE); return; } // See if the connection already exists if( applicationList_->hasDirectConnection() ) { kWarning() << "Received an invitation " "to start a direct connection, but a direct connection is already available " "(authorized=" << applicationList_->hasAuthorizedDirectConnection() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=closeconnection)."; // Drop the previous connection. HACK: added for amsn 0.97. // aMsn 0.97 does some interesting things: // - DC's it hosted are directly closed after sending all file data. // - DC's it connected to are left open forever. P2P messages are still received at it, // but somehow all responses are sent over the SB back as if amsn doesn't know it has a DC. applicationList_->getDirectConnection()->closeConnection(); } // Abort if the file data was just received. // For some reason WLM8 sometimes sends the invitation directly after the file was sent. // First cause some invites to fail (don't accept/cancel). // Then accept one but send an "transfer accept" with invalid IP/port. // Not sure it can always be reproduced this way though. if( isWaitingState( P2P_WAIT_FOR_SLP_BYE ) ) { kWarning() << "Received an invitation for " "a direct connection, but expecting BYE instead " "(file transfer just completed, " " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=slpcancel)."; sendCancelMessage( CANCEL_ABORT ); return; } // New in WLM8 reverse direct connection invites for msnobject transfer bool isReverseInvite = isUserStartedApp(); if( isReverseInvite ) { // Receiving invitations for your own session only happens for msnobject transfer at the moment. // It means KMess needs: // - some way to delay the data preparation acks // - the ability to detect if a SLP error was sent for the transfer INVITE only. // - fixes for new issues with the state tracking. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Received a reverse invitation to for a direct connection, we started the session."; #endif } // Create the accept message MimeMessage acceptMessage; bool startListening = false; // Get network details QString localIp ( getLocalIp() ); QString externalIp( getExternalIp() ); // Read the data from the message QString bridges ( slpMimeBody.getValue( "Bridges" ) ); // Transport layers the client supports // int netID = slpMimeBody.getValue("NetID").toInt(); // Some ID, unused QString connectionType ( slpMimeBody.getValue( "Conn-Type") ); // Suggested connection type QString hasUPnPNat ( slpMimeBody.getValue( "UPnPNat" ) ); // uses UPnP-enabled NAT router QString hasICF ( slpMimeBody.getValue( "ICF" ) ); // uses Internet Connection Firewall // Based on the contact's network configuration, // we choose how we want to send the file. // // The client can include a Hashed-Nonce field, // and we can respond with another Hashed-Nonce field. // This algorithm is not known, but including a normal Nonce field // in the accept message seams to work (the client has some fallback). // Listen at the internal port // The connection can be used for both the internal and external port // because it only accepts one incoming packet, the second one will be rejected. #if P2PAPPLICATION_AVOID_DC_SERVER int listeningPort = 0; // for debugging. #else int listeningPort = applicationList_->addServerConnection(); #endif if( listeningPort != 0 ) { // Get parameters for message nonce_ = KMessShared::generateGUID(); // used for authentication #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Generating Nonce value " << nonce_ << " for incoming connections."; #endif acceptMessage.addField( "Bridge", "TCPv1" ); acceptMessage.addField( "Listening", "true" ); acceptMessage.addField( "Nonce", nonce_ ); if(localIp != externalIp) { acceptMessage.addField("IPv4Internal-Addrs", localIp ); acceptMessage.addField("IPv4Internal-Port", QString::number(listeningPort) ); } acceptMessage.addField("IPv4External-Addrs", externalIp ); // TODO: include other aliases acceptMessage.addField("IPv4External-Port", QString::number(listeningPort) ); // Inform the user about the file transfer progress. showTransferMessage( i18n("Awaiting connection at %1, port %2", externalIp, QString::number( listeningPort ) ) ); // Prepare to send the nonce too when connection to other contact is established connect( applicationList_, SIGNAL( connectionEstablished() ), // when connected, send nonce this, SLOT ( slotConnectionEstablished() )); connect( applicationList_, SIGNAL( connectionAuthorized() ), // when authorized, send data this, SLOT ( slotConnectionAuthorized() )); connect( applicationList_, SIGNAL( connectionFailed() ), // when failed, revert to switchboard this, SLOT ( slotConnectionFailed() )); startListening = true; // avoids double-checking certain conditions. } else if( isReverseInvite ) { // Could not listen at a local port, send reject instead. kWarning() << "Could not listen at local port, " "redirecting transfer to the switchboard " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send603)."; // Note this doesn't kill the session. setWaitingState( P2P_WAIT_DEFAULT, 0 ); // avoid assertion. sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE); return; } else { // Could not listen at a local port, use switchboard instead. kWarning() << "Could not listen at local port, " "redirecting transfer to the switchboard " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)."; // This configuration sends each file over the switchboard: acceptMessage.addField( "Bridge", "TCPv1" ); acceptMessage.addField( "Listening", "false" ); acceptMessage.addField( "Nonce", "{00000000-0000-0000-0000-000000000000}"); // Inform the user about the file transfer progress. showTransferMessage( i18n("Reverting to indirect file transfer (this could be slow).") ); // TODO: at this point, it's assumend the other client will initiate the transfer. // perhaps set the waitingState to detect when the contact does not return a request? // Windows Live Messenger is also capable of sending a new INVITE message so it becomes the server instead. } #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending 200 transfer OK message, " "listening=" << (listeningPort != 0) << "."; #endif // Confirm the session sendSlpOkMessage( acceptMessage ); // Wait for the contact to ACK the message.. if( startListening ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for a connection to succeed or fail..."; #endif setWaitingState( P2P_WAIT_FOR_CONNECTION, P2PAPPLICATION_TIMEOUT_DATA ); } else { // Expecting file data now we've rejected the invite. // TODO: it's also possible another invite is sent, hoping for a second chance. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send file data..."; #endif setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA ); } } /** * @brief Called when a response to a SLP transfer message was received. * * The message body contains the IP address to connect to. * It can be received the following situations: * - We sent the transfer invitation with a sendSlpTransferInvitation() call. * The contact sends the "200 OK" message with the ipaddress/port information. * - We sent a transfer response back with "Listening: false" field. * The contact sends a second SLP INVITE to become the server instead. * * When the contact starts a server, it sends the following message: * @code MSNSLP/1.0 200 OK To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> ... Content-Type: application/x-msnmsgr-transrespbody Content-Length: 144 Bridge: TCPv1 Listening: true Nonce: {9BA191DB-3B68-93DA-91A2-BB9031D5B92F} IPv4External-Addrs: 111.222.333.444 IPv4External-Port: 6891 IPv4Internal-Addrs: 192.168.0.111 IPv4Internal-Port: 6891 @endcode * As of Windows Live Messenger 8.0, a <code>SChannelState: 0</code> field is added. * Version 8.1 also adds a <code>Capabilities-Flags: 1</code>, <code>Conn-Type</code> and <code>TCP-Conn-Type</code> field. * * When the contact is not able to open a port, it sends the following message: * @code MSNSLP/1.0 200 OK To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> ... Content-Type: application/x-msnmsgr-transrespbody Content-Length: 83 Bridge: TCPv1 Listening: false Nonce: {00000000-0000-0000-0000-000000000000} @endcode * * When the contact likes to become the server instead it sends the following message: * @code INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0 To: <msnmsgr:user@kmessdemo.org> From: <msnmsgr:contact@kmessdemo.org> ... Content-Type: application/x-msnmsgr-transrespbody Content-Length: 146 Bridge: TCPv1 Listening: true Hashed-Nonce: {B00B2A8A-B3B6-95EB-322F-379747842156} IPv4Internal-Addrs: 10.0.0.152 IPv4Internal-Port: 1182 @endcode * * @param slpMimeBody The SLP message with the fields "Listening", "Nonce", * and optionally ipaddress and port information. * @param secondInvite Used to indicate the contact sent a second invitation. */ 02245 void P2PApplication::gotSlpTransferResponse(const MimeMessage &slpMimeBody, bool secondInvite) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL if( ! secondInvite ) { kDebug() << "Parsing 200 OK message."; } else { kDebug() << "Parsing 2nd transfer INVITE message, " << "contact offers to become the direct connection server."; } #endif // Test for an existing (possibly even authorized) direct connection. // This could happen with local-lan file transfer, the roundtrip of the switchboard // response message could take longer then the time to authenticate the connection. if( applicationList_->hasDirectConnection() ) { if( applicationList_->hasAuthorizedDirectConnection() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Connection already authorized " << "before invitation response was received, starting transfer."; #endif initiateTransfer(); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Connection already established " << "before invitation response was received, waiting for connection to authorize..."; #endif // Slots already connected at this point. } return; } // Handle some special situations when the contact sent an INVITE message back instead. // This happens when: // - gotSlpTransferInvitation() was called become the contact wanted to initialize a direct connection. // - we sent a "200 OK" message back with "Listening: false" to indicate we can't be the server. // - this method gets called because the contact sends an INVITE to become the server instead. if( secondInvite ) { // TODO: send SLP 200/OK message back when contact sent a second INVITE message? // The 'connectionAuthenticated()' and 'connectionFailed()' slots are connected in gotSlpTransferInvitation() // The 'connectionEstablished()' signal was not connected because we were supposed to become the server. connect( applicationList_, SIGNAL( connectionEstablished() ), // when connected, send nonce this, SLOT ( slotConnectionEstablished() )); } // The Conn-type field of the message is the least important here, // the "Listening", "ICF" and "UPnPNat" tell us more about the situation of the contact, // and the probability to create a successful direct connection in either way. // Get fields from message bool listening = slpMimeBody.getValue("Listening") == "true"; bool listenInternal = slpMimeBody.hasField("IPv4Internal-Addrs"); bool listenExternal = slpMimeBody.hasField("IPv4External-Addrs"); nonce_ = slpMimeBody.getValue("Nonce"); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "P2PApplication:gotSlpTransferResponse() - Storing contact nonce value " << nonce_ << " to authenticate later."; #endif #if P2PAPPLICATION_AVOID_DC_CLIENT listening = false; // for debugging. #endif // If the contact is not listening, we can do the following things: // - invite the contact to connect to connect to this host instead (not implemented yet, TODO) // - send the data over the switchboard instead. (always works, but it's slow). // If the contact is listening, try to create a direct connection to the contact. if( listening && (listenExternal || listenInternal) ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Contact is listening, " << "attempting to connect to the given IP-address."; #endif // Get the IP addresses. QStringList externalIpAddresses; QStringList internalIpAddresses; quint16 externalPort = 0; quint16 internalPort = 0; if( listenInternal ) { internalIpAddresses = slpMimeBody.getValue("IPv4Internal-Addrs").split(" "); internalPort = (quint16)slpMimeBody.getValue("IPv4Internal-Port").toUInt(); } if( listenExternal ) { externalIpAddresses = slpMimeBody.getValue("IPv4External-Addrs").split(" "); externalPort = (quint16)slpMimeBody.getValue("IPv4External-Port").toUInt(); } // See if the clients are likely at the same lan. bool sameLan = ( listenInternal && externalIpAddresses.contains( CurrentAccount::instance()->getExternalIp() ) ); // Avoid sending slotAllConnectionsFailed() if one connection attempt fails immediately. applicationList_->setAddingConnections( true ); // Connect to the internal IP address if( listenInternal ) { // Add connection attempt for each local IP address for ( QStringList::Iterator it = internalIpAddresses.begin(); it != internalIpAddresses.end(); ++it ) { // Ignore when connection was made if( applicationList_->hasDirectConnection() ) { break; } // Show "connecting to <internal ip>" message when connecting if( applicationList_->addConnection( *it, internalPort ) ) { showTransferMessage( i18n("Connecting to %1, port %2", *it, QString::number( internalPort ) ) ); } } } // Connect to the external IP address if( listenExternal ) { // Add connection attempt for each external IP address for ( QStringList::Iterator it = externalIpAddresses.begin(); it != externalIpAddresses.end(); ++it ) { // Ignore when connection was made if( applicationList_->hasDirectConnection() ) { break; } // Show "connecting to <external ip>" message message unless the hosts are _likely_ at the same lan. // In that case, a previous "Connecting to ..." message should stay visible. if( applicationList_->addConnection( *it, externalPort ) && ! sameLan ) { showTransferMessage( i18n("Connecting to %1, port %2", *it, QString::number( externalPort ) ) ); } } } // Reset again. applicationList_->setAddingConnections( false ); } // After passing the connections to addConnection() it's possible // some slots are already fired and the connection was established. if( applicationList_->hasDirectConnection() ) { return; } // See if there aren't connections established already. if( applicationList_->hasPendingConnections() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for connection attempt to succeed..."; #endif // Wait for connection to establish setWaitingState( P2P_WAIT_FOR_CONNECTION2, P2PAPPLICATION_TIMEOUT_DATA ); } else { // Nothing is pending, nothing is connected. #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "No connection possible, " << "informing ApplicationList."; #endif // If no direct connection attempt will be made. // Reset the pending invitation connection flag, ApplicationList sends the connectionFailed signal. // This also signals other applications the connection attempt failed. applicationList_->setPendingConnectionInvitation(false); } } /** * @brief Called when an ack is received, which is not handled internally. * * This method can be overwritten by the derived class, in case it needs to handle a specific ack message. * * @return Whether the ack was handled by this method. */ 02446 bool P2PApplication::gotUnhandledAck( const P2PMessage &message, P2PMessageType ackedMessageType ) { Q_UNUSED( message ); Q_UNUSED( ackedMessageType ); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Not handling the unknown ack"; #endif // Derived class can implement some handling. return false; } /** * @brief Notify the derived class it can initiate the file transfer. * * It either calls userStarted3_UserPrepares() or contactStarted4_ContactConfirmsPreparation(), * depending who started the application. The timeout timer will also be stopped. */ 02467 void P2PApplication::initiateTransfer() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Signalling implementation class " << "to start the file transfer."; #endif // No longer waiting, we're sending. if( isWaitingState( P2P_WAIT_FOR_CONNECTION ) || isWaitingState( P2P_WAIT_FOR_CONNECTION2 ) ) { setWaitingState( P2P_WAIT_DEFAULT, 0 ); } // When we start sending information, it could take a while before // the contact returns messages which disable the waiting timer. // Avoid this by disabling it now, before we kill our own transfer. if( stopWaitingTimer() ) { kWarning() << "timeout detection timer was still running, stopped " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)."; } // Call the connect implementation based on who invoked this application. // The implementations of these methods can call sendData(), and // the actual transfer will be handled transparently here. if( isUserStartedApp() ) { userStarted3_UserPrepares(); } else { contactStarted4_ContactConfirmsPreparation(); } } /** * @brief Send a cancel message. * * The application will be terminated later, either directly or after the contact ACKed the error. * * The following cancelReason values are supported: * - <tt>CANCEL_INVITATION</tt> * The user declined the invitation. This sends an SLP <tt>603 Decline</tt> message. * * - <tt>CANCEL_ABORT</tt> / <tt>CANCEL_FAILED</tt> * The user aborted the session. * If called from contactStarted1_ContactInvitesUser() with <tt>CANCEL_FAILED</tt>, * this sends an SLP <tt>500 Internal Error</tt> message. * Otherwise it sends a SLP BYE or P2P error message. * * - <tt>CANCEL_NOT_INSTALLED</tt> * The requested service/application is not installed. * This sends an SLP <tt>500 Internal Error</tt> message. * * - <tt>CANCEL_TIMEOUT</tt> * There was a timeout waiting for the contact to accept or send certain P2P data. * This sends an P2P message with the "waiting flag" set (type 4) and terminates the application direclty. * This reason is used internally by certain timeout functions. * * - <tt>CANCEL_INVALID_SLP_CONTENT_TYPE</tt> * Sends an "MSNSLP 500 Internal Error" message back, but with a <tt>null</tt> Content-Type value set. * * The error messages which send an SLP message can only be used from * contactStarted1_ContactInvitesUser() and userStarted2_ContactAccepts(). * Once the MSNSLP error message is sent, P2PApplication waits for the * contact to ACK, and terminates the application afterwards. * Hence, you don't have to call endApplication() at all. * * When an MSNSLP error is sent at a different moment, this will likely result in * undefined responses from the other client (or the error message could be ignored). * If KMess is compiled in debug mode, an assertion in sendSlpError() detects this. * * The <tt>CANCEL_ABORT</tt> reason is the only valid response to reject * a data-preparation message in the userStarted3_UserPrepares() method. * If KMess is compiled in debug mode, an assertion will be triggered otherwise. * * @param cancelReason The reason why the application is cancelled. */ 02553 void P2PApplication::sendCancelMessage(const ApplicationCancelReason cancelReason) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "request to abort " << "(reason=" << cancelReason << ", state=" << (int) getWaitingState() << ")."; #endif // I don't care what the reason is if you had // to send the data-preparation ACK if( userShouldAcknowledge_ ) { #ifdef KMESSTEST KMESS_ASSERT( cancelReason == CANCEL_ABORT); #endif // Cancelled the data preparation message sendP2PAbort(); userShouldAcknowledge_ = false; // TODO: send bye here to? wait for bye? just terminate? kWarning() << "data-preparation stage failed " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)."; endApplicationLater(); return; } switch(cancelReason) { // Declined the invitation case CANCEL_INVITATION: #ifdef KMESSTEST KMESS_ASSERT( ! invitationContentType_.isEmpty() ); KMESS_ASSERT( invitationSessionID_ != 0 ); #endif // Use the cancel-reason to send the correct message: sendSlpError("603 Decline", invitationSessionID_, invitationContentType_); return; // Timeout waiting for some P2P data // ...or timeout waiting for contact to accept. case CANCEL_TIMEOUT: // Timeout handling occurs via slotCleanup(), not here. kWarning() << "CANCEL_TIMEOUT is not supported here " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x04,endapplicationlater)"; // Send the low-level error message that the session is waiting for something! sendP2PWaitingError(); endApplicationLater(); return; // The application type is not installed case CANCEL_NOT_INSTALLED: if( gotSlpMessage_) { // Usually this particular error is invoked from Application::contactStarted1_ContactInvitesUser(). sendSlpError("500 Internal Error", invitationSessionID_, "null"); } else { kWarning() << "CANCEL_NOT_INSTALLED is not supported here " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=return)"; } return; // User or application wants to abort case CANCEL_ABORT: case CANCEL_FAILED: // Make sure sending is aborted. abortDataSending(); // Determine which error message to send back. if( gotSlpMessage_ ) { // A: Special case for SLP messages. if( isWaitingState( P2P_WAIT_USER_ACCEPT ) && cancelReason == CANCEL_ABORT ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Cancelling after receiving an SLP INVITE, " << "sending BYE back."; #endif #ifdef KMESSTEST KMESS_ASSERT( isUserStartedApp() ); #endif // Send bye early, contact should detect this. sendSlpBye(); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Aborting after receiving an SLP message, " << "sending 500 Internal Error back."; #endif // Cancelled while in SLP negotiate mode. // Include the original content-type to tell the other // client that part of the message was not in error. sendSlpError("500 Internal Error", invitationSessionID_, invitationContentType_); } } else if( isUserStartedApp() ) { // B: User started application #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "P2PApplication::sendCancelMessage() Cancelling an application " << "we've started ourselves, sending BYE early."; #endif // Mark the logs if there is something weird going on. // It's possible the contact doesn't receive the INVITE anymore // while KMess can't detect yet it's closed. // This happens sometimes when KMess sent a file before which was aborted by WLM. if( hasUnAckedMessage( P2P_MSG_SESSION_INVITATION ) ) { kWarning() << "aborting while INVITE ack was not received " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue,sendslpbye)."; } // Send bye early, contact should detect this. sendSlpBye(); } else { // C: Contact started application #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "P2PApplication::sendCancelMessage() Cancelling an application " << "started by the other contact, sending BYE early."; #endif // Send bye early, contact should detect this. sendSlpBye(); // TODO: after the message is ACKed, the contact sends a 0x40 "I've sent the bye early" message. // KMess does not do this here. } return; // Invalid content type received case CANCEL_INVALID_SLP_CONTENT_TYPE: #ifdef KMESSTEST KMESS_ASSERT( gotSlpMessage_ ); #endif // Send an 500 error message without content-type set sendSlpError("500 Internal Error"); return; // Last option, unknown cancel message default: kWarning() << "unknown cancelReason used " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)."; // Initiate primitive abort. sendP2PAbort(); } } /** * @brief Send the data preparation ACK. * * You need to call this method from userStarted3_UserPrepares() * to accept the data-preparation message. * * This method will actually set a flag only, * which is inspected by gotDataPreparation() when userStarted3_UserPrepares() returns. */ 02742 void P2PApplication::sendDataPreparationAck() { if(userShouldAcknowledge_) { // This value is checked again after the // userStarted3_UserPrepares() method returns. userShouldAcknowledge_ = false; } else { kWarning() << "Call not expected here! " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)"; } } /** * @brief Send a SLP BYE message to close the session. * * This should only be called if we sent the SLP INVITE too, * otherwise the contact should sent it. * * Once the BYE message is sent, it waits for the last ACK to arrive. * The session is terminated afterwards. If the last ACK doesn't arrive * before a timeout occurs, the application is terminated as well to avoid. * * See gotSlpBye() for an example of a SLP BYE message. */ 02774 void P2PApplication::sendSlpBye() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending SLP BYE message."; #endif #ifdef KMESSTEST KMESS_ASSERT( sessionID_ != 0 ); KMESS_ASSERT( ! callID_.isEmpty() ); KMESS_ASSERT( ! branch_.isEmpty() ); #endif // Handle QString myHandle( CurrentAccount::instance()->getHandle() ); // Determine content. // In the last, the official client only had "\r\n\0" as content (length = 3). QString contentType ( "application/x-msnmsgr-sessionclosebody" ); QString content ( "SessionID: " + QString::number( sessionID_ ) + "\r\n\r\n\0" ); uint contentLength = content.length(); // Create the message QString slpMessage; slpMessage = "BYE MSNMSGR:" + getContactHandle() + " MSNSLP/1.0\r\n" "To: <msnmsgr:" + getContactHandle() + ">\r\n" "From: <msnmsgr:" + myHandle + ">\r\n" "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n" // from our INVITE "CSeq: 0\r\n" "Call-ID: " + callID_ + "\r\n" // from our INVITE "Max-Forwards: 0\r\n" "Content-Type: " + contentType + "\r\n" "Content-Length: " + QString::number(contentLength) + "\r\n" + (contentLength > 0 ? "\r\n" : "") + content; // Send the message sendSlpMessage(slpMessage, P2P_MSG_SESSION_BYE); setClosing(true); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for BYE ACK..."; #endif // Don't run endApplication() yet, there is one ACK we wait for... setWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK, P2PAPPLICATION_TIMEOUT_ACK ); } /** * @brief Send an SLP error response. * * This can be done to decline a file tranfer or tell the other client it sent bad SLP data. * * The session ID can be 0 to indicate there was an error in the Content-Type. Setting the * session ID and Content-Type to the values used in the INVITE message indicates * the invitation is cancelled. * * @param statusLine The status line, for example <tt>200 OK</tt>, or <tt>603 Decline</tt>. * @param sessionID The identifier of the current session (can be zero). * @param messageContentType Contact type of the message which is in error (can be empty) * @param messageType The message type. This is used later when an ACK is received. */ 02836 void P2PApplication::sendSlpError( const QString &statusLine, const ulong sessionID, const QString &messageContentType, P2PMessageType messageType ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending SLP error response: " << statusLine; #endif #ifdef KMESSTEST // Test, as other invocations likely result in undefined client behavour KMESS_ASSERT( gotSlpMessage_ ); KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) || isWaitingState( P2P_WAIT_USER_ACCEPT ) ); KMESS_ASSERT( ! callID_.isEmpty() ); KMESS_ASSERT( ! branch_.isEmpty() ); #endif QString myHandle( CurrentAccount::instance()->getHandle() ); // Determine Content-Type and Content-Length fields QString content; QString contentType; uint contentLength; if( callID_.isEmpty() || branch_.isEmpty() ) { kWarning() << "attempting to send SLP error back, " "but Call-ID or Branch is unknown! " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=send0x08)"; sendP2PAbort(); return; } // It's safe now to send the normal ACK. // Send it if this was not done by the caller yet. // The if(..) is not needed but avoids a NOTICE in the debug log. if( ! isP2PAckSent() ) { sendP2PAck(); } if(sessionID == 0) { // Used to indicate a general error content = QString::null; contentType = "null"; contentLength = 0; } else { #ifdef KMESSTEST KMESS_ASSERT(messageContentType != 0 && ! messageContentType.isEmpty()); #endif // Used to indicate an error in the Invite, or decline a session content = "SessionID: " + QString::number(sessionID) + "\r\n\r\n\0"; contentLength = content.length(); contentType = messageContentType; // TODO see when this should be application/x-msnmsgr-session-failure-respbody instead. } // Create the message QString slpMessage; slpMessage = "MSNSLP/1.0 " + statusLine + "\r\n" "To: <msnmsgr:" + getContactHandle() + ">\r\n" "From: <msnmsgr:" + myHandle + ">\r\n" "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n" // Indicates which INVITE was in error. "CSeq: 0\r\n" "Call-ID: " + callID_ + "\r\n" "Max-Forwards: 0\r\n" "Content-Type: " + contentType + "\r\n" "Content-Length: " + QString::number(contentLength) + "\r\n" + (contentLength > 0 ? "\r\n" : "") + content; // Send the message sendSlpMessage(slpMessage, messageType); setClosing(true); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for SLP ACK..."; #endif // Wait for the message to be ACK-ed. setWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK, P2PAPPLICATION_TIMEOUT_ACK ); } /** * @brief Utility function to send an SLP INVITE message. * * Derived classes use sendSlpSessionInvitation() and sendSlpTransferInvitation() instead. * Both functions use this method internally to send the invitation. * * This method is constructs the SLP headers for the INVITE message, and sends it using sendSlpMessage(). * The Branch-ID is generated by this method, and the Call-ID is generated with the first request. * After sending the invitation, it's waiting for the contact to accept or decline the message. * * @param message The SLP body fields, typically 'SessionID', 'EUF-GUID', 'AppId', and 'Context'. * @param contentType Content-Type of the message body. * @param messageType The message type. This is used later when an ACK is received. */ 02941 void P2PApplication::sendSlpInvitation(const MimeMessage &message, const QString &contentType, P2PMessageType messageType) { #ifdef KMESSTEST KMESS_ASSERT(! contentType.isEmpty() ); KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) ); #endif #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending INVITE message"; #endif // Mime message conversion QString mimeBody( message.getFields() ); // Content handles QString myHandle( CurrentAccount::instance()->getHandle() ); // Generate identifiers if( callID_.isNull() ) { // The callID remains the same between multiple invites // It identifies the session. callID_ = KMessShared::generateGUID(); } // BranchID identifies this INVITE request branch_ = KMessShared::generateGUID(); // Create the message (space after CSeq is added to emulate msn exactly) QString slpMessage; slpMessage = "INVITE MSNMSGR:" + getContactHandle() + " MSNSLP/1.0\r\n" "To: <msnmsgr:" + getContactHandle() + ">\r\n" "From: <msnmsgr:" + myHandle + ">\r\n" "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n" "CSeq: 0 \r\n" "Call-ID: " + callID_ + "\r\n" "Max-Forwards: 0\r\n" "Content-Type: " + contentType + "\r\n" // Starts MSNSLP Session. "Content-Length: " + QString::number(mimeBody.length() + 5) + "\r\n" "\r\n" + mimeBody + "\r\n"; sendSlpMessage(slpMessage, messageType); // Keep the used content type in the state variables. // For contact-started invitations, this is used to store their type. // In this case, it can be used to store ours. invitationContentType_ = contentType; #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send INVITE ACK, and accept later..."; #endif // Wait for the contact to accept // MSN 6 had a timeout of 30 sec to accept, WLM seams to have no timeout. setWaitingState( P2P_WAIT_FOR_INVITE_ACK, P2PAPPLICATION_TIMEOUT_ACCEPT ); } /** * @brief Send the 200 OK message to accept the invitation. * * The message should contain the 'SessionID' field. It's value can * be retreived with getInvitationSessionID(). * * Once the confirmation message has been sent, this class will automatically * use the correct Session ID for following messages (since it was already read from the SLP INVITE message). * * See gotSlpOk() for a message example. * Note that the 'message' parameter only needs to supply the SLP body fields. * The SLP header will be added by this method. * * @param message The SLP message body. */ 03017 void P2PApplication::sendSlpOkMessage(const MimeMessage &message) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Sending MSNSLP/1.0 200 OK message."; #endif #ifdef KMESSTEST KMESS_ASSERT(! branch_.isEmpty() ); KMESS_ASSERT(! invitationContentType_.isEmpty() ); // Waiting state is P2P_WAIT_USER_ACCEPT if contactStarted2_.. is called directly from contactStarted1_.. // TODO: handle WLM8 invitations sent before data preparation // SLP_WAIT_FOR_PREPARE happens when a confirmation needs to be sent when the session is still in prepare state KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) || isWaitingState( P2P_WAIT_USER_ACCEPT ) || isWaitingState( P2P_WAIT_FOR_CONNECTION ) || isWaitingState( P2P_WAIT_FOR_PREPARE ) ); #endif // Get message content and Content-Type bool gotTransferInvitation = (invitationContentType_ == "application/x-msnmsgr-transreqbody"); QString content ( message.getFields() ); QString contentType( (gotTransferInvitation ? "application/x-msnmsgr-transrespbody" : invitationContentType_) ); QString myHandle( CurrentAccount::instance()->getHandle() ); // Send the SLP OK message to tell the other client you'd like to start the transfer. QString slpMessage; slpMessage = "MSNSLP/1.0 200 OK\r\n" "To: <msnmsgr:" + getContactHandle() + ">\r\n" "From: <msnmsgr:" + myHandle + ">\r\n" "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n" "CSeq: " + QString::number(invitationCSeq_ + 1) + "\r\n" "Call-ID: " + callID_ + "\r\n" "Max-Forwards: 0\r\n" "Content-Type: " + contentType + "\r\n" "Content-Length: " + QString::number(content.length() + 3) + "\r\n" "\r\n" + content + "\r\n"; sendSlpMessage(slpMessage, ( gotTransferInvitation ? P2P_MSG_TRANSFER_OK : P2P_MSG_SESSION_OK ) ); // Initialize the session ID for the first OK message. // Note this method can be called again later to handle the transfer OK message. if( sessionID_ == 0 ) { // Now that we've confirmed the session, // the sessionID will be used the next messages. sessionID_ = invitationSessionID_; } if(gotTransferInvitation) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to ACK the 200 transfer OK message..."; #endif // Wait until our transfer OK message is ACK-ed setWaitingState( P2P_WAIT_FOR_TRANSFER_ACK, P2PAPPLICATION_TIMEOUT_ACK ); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to ACK the 200 OK message..."; #endif // Wait until our normal OK message is ACK-ed setWaitingState( P2P_WAIT_FOR_SLP_OK_ACK, P2PAPPLICATION_TIMEOUT_ACK ); } } /** * @brief Send the invitation for a normal session. * * It sends a normal <tt>application/x-msnmsgr-sessionreqbody</tt> message using sendSlpInvitation(). * The session ID is stored to handle incoming messages. It can be requested with getSessionID(). * * Internally the remaining message fields are appended using: * - sendSlpInvitation() * - sendSlpMessage() * - sendP2PMessage() * * See gotSlpSessionInvitation() for an example message. * * @param sessionID The generated session ID, can be generated with generateID(). * @param eufGuid The EUF-GUID value identifying the application type, from <tt>getAppId()</tt>. * @param appId The numeric app-Id value, which also identifies the application type. * @param context The context field. */ 03107 void P2PApplication::sendSlpSessionInvitation( quint32 sessionID, const QString &eufGuid, const int appId, const QString &context ) { // Create the message MimeMessage invitation; invitation.addField( "EUF-GUID", eufGuid ); invitation.addField( "SessionID", QString::number(sessionID) ); invitation.addField( "AppID", QString::number(appId) ); invitation.addField( "Context", context ); // Send the invitation sendSlpInvitation( invitation, "application/x-msnmsgr-sessionreqbody", P2P_MSG_SESSION_INVITATION); // If the client accepts our invitation, we // use the session ID in the next ACK message. sessionID_ = sessionID; setObjectName( QLatin1String( metaObject()->className() ) + "[" + QString::number( sessionID_ ) + "/" + getContactHandle() + "]" ); } /** * @brief Send the invitation for a direct connection. * * This sends a <tt>application/x-msnmsgr-transreqbody</tt> message using sendSlpInvitation(). * The contact responds with the ipaddresses/ports we can connect to. * * When the direct connection attempt completed, initiateTransfer() will be called. * It's possible the direct connection attempt failed, the clients will * exchange file data over the switchboard instead. * * See gotSlpTransferInvitation() for message examples. */ 03141 void P2PApplication::sendSlpTransferInvitation() { // Check if there is already an connection if( applicationList_->hasAuthorizedDirectConnection() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Connection is already setup, " << "re-using the current connection."; #endif initiateTransfer(); return; } // Prepare to transfer when a connection is made by this application, or another application. connect( applicationList_, SIGNAL( connectionAuthorized() ), // when authorized, send data this, SLOT ( slotConnectionAuthorized() )); connect( applicationList_, SIGNAL( connectionFailed() ), // when failed, revert to switchboard this, SLOT ( slotConnectionFailed() )); // Check if there are pending connections if( applicationList_->hasPendingConnections() || applicationList_->hasPendingConnectionInvitation() ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Another application is already inviting " << "for a direct connection, waiting for that attempt to succeed or fail..."; #endif setWaitingState( P2P_WAIT_FOR_CONNECTION2, 0 ); showTransferMessage( i18n("Waiting for connection") ); return; } // Only connect last slot if we're really going to make the connection ourselves. connect( applicationList_, SIGNAL( connectionEstablished() ), // when connected, send nonce this, SLOT ( slotConnectionEstablished() )); // Invite the contact to create a connection. // This message tells the other client it can't connect to this end-host. // Either the data needs to be sent over the switchboard, // or the other client should initiate a direct connection server. MimeMessage invitation; invitation.addField("Bridges", "TCPv1"); invitation.addField("NetID", QString::number( KMessShared::generateID() )); invitation.addField("Conn-Type", "IP-Restrict-NAT"); invitation.addField("UPnPNat", "false"); invitation.addField("ICF", "false"); // Send the invitation sendSlpInvitation( invitation, "application/x-msnmsgr-transreqbody", P2P_MSG_TRANSFER_INVITATION ); // Wait for the ack, the application list should // timeout if the connections can't be made. setWaitingState( P2P_WAIT_FOR_INVITE_TR_ACK, 0 ); // Mark invitation as pending. // This will fire an event when it's manually reset to false. applicationList_->setPendingConnectionInvitation( true ); // When we include a Hashed-Nonce field, the contact also // adds a Hashed-Nonce to it's 'transrespbody' response message. // This may resemble http-digest authentication, but is still not researched. // Supported Conn-Type values: // - Firewall // - Direct-Connect (Same IP, same port) // - Port-Restrict-NAT (Same IP, different port) // - IP-Restrict-NAT (Different IP, same port) // - Symmetric-NAT (Different IP, different port) // - Unknown-Connect (Different IP, different port) // The connection type can be discovered with STUN. // NetID is zero for Firewall/Direct-Connect, // otherwise it's a random number. // Inform the user about the file transfer progress. showTransferMessage( i18n("Negotiating options to connect") ); } /** * Assign a fixed session ID (used for p2p ink transfers) */ 03225 void P2PApplication::setDataCastSessionID( quint32 sessionID ) { #ifdef KMESSTEST KMESS_ASSERT( sessionID_ == 0 ); KMESS_ASSERT( sessionID < 600 ); KMESS_ASSERT( getMode() == APP_MODE_DATACAST ); #endif sessionID_ = sessionID; } /** * @brief Called when the direct connection is authorized by one of the P2P applications. * * This initiates the transfer by calling initiateTransfer(). */ 03243 void P2PApplication::slotConnectionAuthorized() { // Avoid posibility that this method is invoked again if // the direct connection is closed and re-connected later by another application. disconnect( applicationList_, 0, this, SLOT(slotConnectionAuthorized())); // The user who sent the INVITE, but the other side asked us to be the server. bool wasReverseInvite = ( isUserStartedApp() && applicationList_->getDirectConnection()->isServer() ); if( wasReverseInvite ) { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Direct connection authorized, " "reverse invitation complete (state=" << (int) getWaitingState() << ")."; #endif // Since we don't actively start reverse invitations, // assume for now that the contact should send something again. // TODO: when we send reverse invites too, we need to track who's turn it is. if( ! isTransferActive() ) { // Waiting for prepare message #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send some prepare message..."; #endif setWaitingState( P2P_WAIT_FOR_PREPARE, P2PAPPLICATION_TIMEOUT_ACK ); } // Do not call 'initiateTransfer()' here. For MsnObject transfers, // this will fire userStarted3 to confirm the data preparation. } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Direct connection authorized, " "starting transfer (state=" << (int) getWaitingState() << ")."; #endif // Protect against situations which could break our internal state. if( isTransferActive() ) { kWarning() << "Direct connection authorized, " "but data transfer was already started before. Won't notify derived classes " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " reverse=" << wasReverseInvite << " class=" << metaObject()->className() << " action=return)."; return; } // No longer wait for packets, got connection, so go for it. stopWaitingTimer(); initiateTransfer(); } } /** * @brief Called when the direct connection is established. * * This sends the direct connection handshake with sendDirectConnectionHandshake(), * or waits for the contact to send one. */ 03310 void P2PApplication::slotConnectionEstablished() { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Direct connection established."; #endif // Disconnect irrelevant signals. // Connection can still fail if the nonce is invalid. disconnect( applicationList_, 0, this, SLOT( slotConnectionEstablished() )); // Send the handshake if this is a new connection bool isClient = ( ! applicationList_->getDirectConnection()->isServer() ); if( isClient ) { #ifdef KMESSTEST KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_CONNECTION2 ) ); #endif // Send the handshake sendDirectConnectionHandshake( nonce_ ); #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for direct connection handshake response..."; #endif // Start the timeout timer again. setWaitingState( P2P_WAIT_FOR_HANDSHAKE_OK, P2PAPPLICATION_TIMEOUT_ACK ); } else { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Waiting for contact to send connection handshake..."; #endif // Wait for the handshake setWaitingState( P2P_WAIT_FOR_HANDSHAKE, P2PAPPLICATION_TIMEOUT_ACK ); } } /** * @brief Called when a direct connection could not be made. * * It initiates the transfer with initiateTransfer(). */ 03356 void P2PApplication::slotConnectionFailed() { // Make this a warning, so users can figure out stuff when running kmess from the console. kWarning() << "A direct connection could not be made, " "switching file transfer to slow indirect transfer " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)."; #ifdef KMESSTEST KMESS_ASSERT( ! applicationList_->hasDirectConnection() ); KMESS_ASSERT( ! applicationList_->hasPendingConnections() ); #endif // Disconnect all signals as they are not relevant anymore disconnect( applicationList_, 0, this, SLOT( slotConnectionAuthorized() )); disconnect( applicationList_, 0, this, SLOT( slotConnectionEstablished() )); disconnect( applicationList_, 0, this, SLOT( slotConnectionFailed() )); // Inform the user about the transfer progress showTransferMessage( i18n("Reverting to indirect file transfer (this could be slow).") ); // start the transfer over the switchboard stopWaitingTimer(); initiateTransfer(); } /** * @brief Show a timeout message because an expected message was not received. * * The contact is already notified at this point about this problem. * All this method needs to do, is displaying a reasonable message at the console or user interface. */ 03393 void P2PApplication::showTimeoutMessage( P2PWaitingState waitingState ) { // Important note: // Each case calling endApplication() contains a "return;" // All debug cases use "break;" so the final endApplication() is called. // Clear any active invitation links first modifyOfferMessage(); // Put a message at the console switch( waitingState ) { case P2P_WAIT_FOR_INVITE_ACK: { kWarning() << "Timeout waiting SLP INVITE ack message " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_CONTACT_ACCEPT: { #ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL kDebug() << "Contact didn't accept the invitation, terminating manually."; #endif // NOTE: WLM8 has no timeout abort P2P (file transfer) invitations. // It keeps the invitation open even if the switchboard closes. showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for the contact to accept."), ChatMessage::CONTENT_APP_CANCELED, false ); sendCancelMessage( CANCEL_ABORT ); // Should call sendSlpBye(). // Can't send 0x04 timeout message because there is no session yet, and previous message is already ACKed. // NOTE: it seams Windows Live Messenger 8.0 doesn't send a BYE ACK for this message. return; } case P2P_WAIT_FOR_CONNECTION: { showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for a connection to succeed or fail."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() ); endApplication(); break; } case P2P_WAIT_FOR_CONNECTION2: { // Still keep waiting, DirectConnectionBase should trigger a timeout. kWarning() << "Time-out waiting to connect to the remote system " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=return)."; return; } case P2P_WAIT_FOR_SLP_OK_ACK: { kWarning() << "Timeout waiting for SLP OK ACK " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_PREPARE: { kWarning() << "Timeout waiting for data-preparation message " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_PREPARE_ACK: { kWarning() << "Timeout waiting for data-preparation ACK " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_TRANSFER_ACK: { kWarning() << "Timeout waiting for SLP transfer OK ACK " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_FILE_DATA: { kWarning() << "Timeout waiting for file data " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; // The 0x80 mesage should be sent now. // Set "incoming" parameter to display messages like "the file you're sending.." in the future. showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for data."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() ); sendCancelMessage( CANCEL_FAILED ); // Should call sendSlpBye(). return; } case P2P_WAIT_FOR_DATA_ACK: { kWarning() << "Timeout waiting for file data received ack " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_HANDSHAKE: { // NOTE: we could start an initiateTransfer() here, but this is clearly a client bug. kWarning() << "Timeout waiting for DC handshake " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_HANDSHAKE_OK: { // NOTE: we could start an initiateTransfer() here, but this is clearly a client bug. kWarning() << "Timeout waiting for DC handshake response " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; break; } case P2P_WAIT_FOR_SLP_BYE: { kWarning() << "Timeout waiting for BYE message " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; // No message required, already got what we wanted endApplication(); return; } case P2P_WAIT_FOR_SLP_BYE_ACK: { kWarning() << "Timeout waiting for BYE-ACK " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; // No message required, already got what we wanted endApplication(); return; } case P2P_WAIT_FOR_SLP_ERR_ACK: { kWarning() << "Timeout waiting for SLP Error-ACK " "(contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplication)."; // No message required, already got what we wanted endApplication(); return; } default: { kWarning() << "Timeout waiting " "(state=" << (int) getWaitingState() << " session=" << sessionID_ << " class=" << metaObject()->className() << " contact=" << getContactHandle() << ")"; } } // Default implementation quits showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for data."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() ); endApplication(); } /** * @brief Called when the user aborted the application. * * Displays a message using getUserAbortMessage() and notifies the contact. * Unlike the base class, it won't terminate the application since the contact should return an ACK message first. * * This method may be overwritten to add additional event handling. */ 03596 void P2PApplication::userAborted() { #ifdef KMESSDEBUG_APPLICATION kDebug() << "user requests to abort the session. state=" << (int) getWaitingState(); #endif // Make sure a second message is not displayed here. if( isClosing() ) { kWarning() << "Attempted to close application twice " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)"; // Make sure the timer is started again. endApplicationLater(); return; } else if( isWaitingState( P2P_WAIT_FOR_SLP_BYE ) ) { // This is a problem when a client does not send a SLP BYE (as seen with amsn 0.97). #ifdef KMESSDEBUG_APPLICATION kDebug() << "Ignoring request while waiting for SLP BYE."; #endif return; } if( isTransferComplete() ) { // Make sure the user won't get a "transfer complete" message // followed by a "contact aborted" message. // // Currently happens when a contact restarts a switchboard (aMsn 0.97 does). // The old switchboard is removed, and all applications abort. // TODO: The switchboard closeConnection() call assumes it's always user initiated. kWarning() << "not displaying message because data transfer is complete. " "The switchboard was likely closed after receiving all data. " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=continue)"; } else { // Display abort message modifyOfferMessage(); showEventMessage( getUserAbortMessage(), ChatMessage::CONTENT_APP_CANCELED, false ); } // Send cancel message. // Prepares the application to abort. sendCancelMessage( CANCEL_ABORT ); // Set the state to avoid crashes. setUserAborted( true ); setClosing( true ); // Don't end the application, instead wait for contact's client to respond too } /** * @brief Called when the user rejected (declined) the application. * * Displays a message using getUserRejectMessage() and notifies the contact. * Unlike the base class, it won't terminate the application since the contact should return an ACK message first. */ 03669 void P2PApplication::userRejected() { if( isClosing() ) { #ifdef KMESSDEBUG_APPLICATION kWarning() << "Attempted to close application twice " "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << " session=" << sessionID_ << " class=" << metaObject()->className() << " action=endapplicationlater)"; #endif // Make sure the timer is started again. endApplicationLater(); return; } else { #ifdef KMESSDEBUG_APPLICATION kDebug() << "user requests to reject the invitation."; #endif // Send abort messages modifyOfferMessage(); showEventMessage( getUserRejectMessage(), ChatMessage::CONTENT_APP_CANCELED, false ); sendCancelMessage( CANCEL_INVITATION ); // Set the state to avoid crashes. setUserAborted( true ); setClosing( true ); // Don't end the application, instead wait for contact's client to respond too } } #include "p2papplication.moc"