/*************************************************************************** networkwindow.cpp - description ------------------- begin : Wed Jan 28 2005 copyright : (C) 2005 by Diederik van der Boor email : "vdboor" --at-- "codingdomain.com" ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "../kmessdebug.h" #include "networkwindow.h" #include "../network/mimemessage.h" #include "../network/msnnotificationconnection.h" #include "../network/p2pmessage.h" #include "../utils/kmessconfig.h" #include #include #include #include #include #include #include #include #include #include #include // Declare the instance NetworkWindow * NetworkWindow::instance_(0); #ifdef KMESS_NETWORK_WINDOW // The network window can optionally be compiled. // The macro's from kmessdebug.h make it easy to handle this // without optionally depending on networkwindow.h everywhere. /** * @brief Inform the network window the connection was closed, used by KMESS_NET_CLOSE. * @param connection Reference to the caller object, to distinguish between connections. * @ingroup Debug */ void _kmessNetClose(QObject *connection) { NetworkWindow::instance()->connectionClosed(connection); } /** * @brief Inform the network window about a new connection, used by KMESS_NET_INIT. * @param connection Reference to the caller object, to distinguish between connections. * @param title The title for the connection in the network window. * @ingroup Debug */ void _kmessNetInit(QObject *connection, const QString &title) { NetworkWindow::instance()->setConnectionTitle(connection, title); } /** * @brief Add the received data to the network window, used by KMESS_NET_RECEIVED. * @param connection Reference to the caller object, to distinguish between connections. * @param message The data received from the connection, should be a complete message. * @ingroup Debug */ void _kmessNetReceived(QObject *connection, const QByteArray &message) { NetworkWindow::instance()->addIncomingMessage(connection, message); } /** * @brief Add the sent data to the network window, used by KMESS_NET_SENT. * @param connection Reference to the caller object, to distinguish between connections. * @param message The data sent to the connection, should be a complete message. * @ingroup Debug */ void _kmessNetSent(QObject *connection, const QByteArray &message) { NetworkWindow::instance()->addOutgoingMessage(connection, message); } #endif // Constructor NetworkWindow::NetworkWindow( QWidget *parent ) : KMessDialog( parent, Qt::Dialog ) // Explicitly set the window type to disable the destructive close behavior , Ui::NetworkWindow() { // Set up the dialog and its buttons setObjectName( "Network" ); setButtons( Close | User1 | User2 | User3 ); setDefaultButton( Close ); setCaption( i18n("Network Window") ); restoreDialogSize( KMessConfig::instance()->getGlobalConfig( "NetworkWindow" ) ); // Load the ui file in a main qwidget QWidget *mainWidget = new QWidget(this); setupUi( mainWidget ); setMainWidget( mainWidget ); // Set up the behavior of the tab widget connectionTabs_->setTabsClosable( true ); connect( connectionTabs_, &QTabWidget::tabCloseRequested, this, [this]( int index ) { closeTab( connectionTabs_->widget( index ) ); } ); connect( connectionTabs_, SIGNAL( currentChanged(int) ), this, SLOT( slotUpdateWidgets(int) ) ); // Set up the behavior of the Send button connect( sendCommandButton_, SIGNAL( released() ), this, SLOT( slotSendCommand() ) ); // Name our custom buttons and setup their behavior setButtonText( User1, i18n("S&ave Tab") ); setButtonText( User2, i18n("C&lear Tab") ); setButtonText( User3, i18n("C&lose All Tabs") ); setButtonIcon( User1, QIcon::fromTheme( "document-save" ) ); setButtonIcon( User2, QIcon::fromTheme( "edit-clear-list" ) ); setButtonIcon( User3, QIcon::fromTheme( "edit-clear" ) ); connect( this, SIGNAL( user1Clicked() ), this, SLOT( saveCurrentTab() ) ); connect( this, SIGNAL( user2Clicked() ), this, SLOT( clearCurrentTab() ) ); connect( this, SIGNAL( user3Clicked() ), this, SLOT( closeAllTabs() ) ); // Set the default state of the ui widgets sendStandardCmdRadio_->setChecked( true ); } // Destructor NetworkWindow::~NetworkWindow() { KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "NetworkWindow" ); saveDialogSize( group ); // Disconnect all signals, prevent i.e. slotUpdateWidgets from being called disconnect( connectionTabs_, 0, this, 0 ); disconnect( sendCommandButton_, 0, this, 0 ); disconnect( this, 0, this, 0 ); foreach( ConnectionEntry *entry, connectionViews_ ) { delete entry->logView; delete entry; } connectionViews_.clear(); } // The internal function to add a message void NetworkWindow::addMessage(ConnectionEntry *entry, QString html) { html = html.replace( " ", "  ") .replace( QRegExp("[\r\n] "), "\n "); // Add spacer if previous message was some time ago. QDateTime now = QDateTime::currentDateTime(); if( entry->lastMessage.secsTo( now ) > 20 ) { entry->logView->append( "

 

" ); } entry->logView->append( html.toUtf8() ); entry->lastMessage = now; // Update the widgets for the currently visible tab slotUpdateWidgets(); } // Add a log message to the last log entry void NetworkWindow::addLogMessage(QObject *connection, const QString &message) { ConnectionEntry *entry = getConnectionEntry(connection); QString html( "

  " + message + "

\n" ); addMessage( entry, html ); } // Add an incoming message to the log. void NetworkWindow::addIncomingMessage(QObject *connection, const QByteArray &message) { ConnectionEntry *entry = getConnectionEntry(connection); QString html( "


" + formatMessage(entry, true, message) + "

\n" ); addMessage(entry, html); } // Add an outgoing message to the log. void NetworkWindow::addOutgoingMessage(QObject *connection, const QByteArray &message) { ConnectionEntry *entry = getConnectionEntry(connection); QString html( "


" + formatMessage(entry, false, message) + "

\n" ); addMessage(entry, html); } // Inform the connection was closed void NetworkWindow::connectionClosed(QObject *connection) { // Add the message ConnectionEntry *entry = getConnectionEntry(connection); entry->logView->append("

the connection was closed
" + getTime() + "

\n"); entry->lastMessage = QDateTime::currentDateTime(); entry->isClosed = true; // // Unregister the connection, so new attempts from the same class generate a new object. // connectionViews_.remove(browser); } // Return a log description for a standard command. QString NetworkWindow::describeCommand(const QString &command) { QString firstPart( command.left(3) ); // command[0] in normal code. int errorCode = firstPart.toInt(); if( errorCode != 0 ) { switch( errorCode ) { // Command is a 3 digit error code. case 200: return formatDescription("Error: Invalid syntax"); case 201: return formatDescription("Error: Invalid parameter"); case 205: return formatDescription("Error: Invalid principal; not an MSN account"); case 206: return formatDescription("Error: Domain name missing"); case 207: return formatDescription("Error: Already logged in"); case 208: return formatDescription("Error: Invalid principal; not an MSN account"); case 209: return formatDescription("Error: Nickname change illegal"); case 210: return formatDescription("Error: Principal list full"); case 213: return formatDescription("Error: Invalid rename request"); case 215: return formatDescription("Error: Principal already on list"); case 216: return formatDescription("Error: Principal not on list"); case 217: return formatDescription("Error: Principal not online"); case 218: return formatDescription("Error: Already in this mode"); case 219: return formatDescription("Error: Principal is in the opposite list"); case 223: return formatDescription("Error: Too many groups"); case 224: return formatDescription("Error: Invalid group"); case 225: return formatDescription("Error: Principal not in group"); case 227: return formatDescription("Error: Group not empty"); case 228: return formatDescription("Error: Group with same name already exists"); case 229: return formatDescription("Error: Group name too long"); case 230: return formatDescription("Error: Cannot remove group zero"); case 231: return formatDescription("Error: Invalid group"); case 240: return formatDescription("Error: Empty domain"); case 280: return formatDescription("Error: Switchboard server failed"); case 281: return formatDescription("Error: Transfer to switchboard failed"); case 282: return formatDescription("Error: P2P header error"); case 300: return formatDescription("Error: Required field missing"); case 302: return formatDescription("Error: Not logged in"); case 402: // same as 403 case 403: return formatDescription("Error: Error accessing contact list"); case 420: return formatDescription("Error: Invalid account permissions"); // CVR with unofficial client, non beta service tester case 500: return formatDescription("Error: Service Temporarily Unavailable"); case 501: return formatDescription("Error: Database server error"); case 502: return formatDescription("Error: Protocol command is disabled"); case 510: return formatDescription("Error: File operation failed"); case 511: return formatDescription("Error: Account is banned"); case 520: return formatDescription("Error: Memory allocation failed"); case 540: return formatDescription("Error: Challenge response failed"); case 600: return formatDescription("Error: Server is busy"); case 601: return formatDescription("Error: Server is unavailable"); case 602: return formatDescription("Error: Peer nameserver is down"); case 603: return formatDescription("Error: Database connection failed"); case 604: return formatDescription("Error: Server is going down"); case 605: return formatDescription("Error: Server unavailable"); case 700: return formatDescription("Error: Connection failed"); case 710: return formatDescription("Error: Bad command parameter"); case 711: return formatDescription("Error: Write is blocking"); case 712: return formatDescription("Error: Session is overloaded"); case 713: return formatDescription("Error: Opening chat sessions too fast"); case 714: return formatDescription("Error: Too many chat sessions open"); case 715: return formatDescription("Error: Unexpected command value"); case 717: return formatDescription("Error: Bad friend file"); case 731: return formatDescription("Error: Not expected"); case 800: return formatDescription("Error: Changing names too fast"); case 911: return formatDescription("Error: Authentication ticket was incorrect"); case 913: return formatDescription("Error: not allowed when hidden"); case 923: return formatDescription("Error: Kids' Passport without parental consent"); case 924: return formatDescription("Error: passport account not verified yet"); case 928: return formatDescription("Error: bad ticket"); case 931: return formatDescription("Error: account not on this server"); // if using a cached NS instead of the main dispatch point. case 900: case 912: case 918: case 919: case 921: case 922: return formatDescription("Error: Server too busy"); case 914: case 915: case 916: return formatDescription("Error: Server unavailable"); case 917: return formatDescription("Error: authentication failed"); default: return formatDescription("Error, type is unknown!"); } } else { if( firstPart == "VER" ) { return formatDescription("Protocol version negotiation"); } else if ( firstPart == "CVR" ) { return formatDescription("Application version exchange"); } else if ( firstPart == "XFR" ) { return formatDescription("Server transfer request"); } else if ( firstPart == "USR" ) { return formatDescription("User login information"); } else if ( firstPart == "SYN" ) { return formatDescription("Contactlist sync request"); } else if ( firstPart == "CHG" ) { return formatDescription("Client status message (change of online state, client capabilities or msn object)"); } else if ( firstPart == "CHL" ) { return formatDescription("Challenge query"); } else if ( firstPart == "IRO" ) { return formatDescription("Contact already in chat"); } else if ( firstPart == "CAL" ) { return formatDescription("Calling a contact to join chat"); } else if ( firstPart == "RNG" ) { return formatDescription("Invitation for a chat"); } else if ( firstPart == "ANS" ) { return formatDescription("Chat invitation answer with user login information"); } else if ( firstPart == "JOI" ) { return formatDescription("Contact joins chat"); } else if ( firstPart == "FIL" ) { return formatDescription("Indicates the size of the entire file"); } else if ( firstPart == "TFR" ) { return formatDescription("Start of file transfer"); } } // Default // return QString(); return formatDescription( QString() ); // always add time. } // Return a log description for the direct connection preamble QString NetworkWindow::describeDirectConnectionPreamble(const QByteArray &message) { if( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0", 4) == 0 ) { return formatDescription("Indicates the size of the next block"); } else if( message.size() == 4 && memcmp(message.data(), "foo\0", 4) == 0 ) { return formatDescription("The preamble of the direct connection: foo\\0"); } else if( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 ) { return formatDescription("The block size followed by the preamble of the direct connection: foo\\0"); } else { // return QString(); return formatDescription( QString() ); // always add time. } } // Return a log description for the mime message QString NetworkWindow::describeMimeMessage(const QString &mimeMessage) { QRegExp rx("Content-Type: ([^\r\n; ]+)"); if( rx.indexIn(mimeMessage) == -1 ) { return formatDescription("Message could not be parsed!"); } // Return a description based on the content type QString contentType( rx.cap(1) ); // Notification server messages if( contentType == "text/x-msmsgsprofile" ) { // Message with all meta-data of the profile return formatDescription("Profile data message"); } else if( contentType == "text/x-msmsgsinitialemailnotification" ) { // The initial status of the mailbox return formatDescription("Initial email status message"); } else if( contentType == "text/x-msmsgsemailnotification" ) { // A new email was received return formatDescription("New email notification message"); } else if( contentType == "text/x-msmsgsactivemailnotification" ) { // The number of unread emails has changed return formatDescription("Email activity message"); } else if( contentType == "application/x-msmsgssystemmessage" ) { // The server is going down for maintenance return formatDescription("System maintenance notification"); } else if( contentType == "text/x-msmsgsinitialmdatanotification" ) { // Another contact sent an Offline-IM, which is received upon logging-in return formatDescription("Offline-IM notification or Hotmail email status"); } else if( contentType == "text/x-msmsgsoimnotification" ) { // Another contact sent an Offline-IM while we're in invisible mode. return formatDescription("Offline-IM notification"); } // Switchboard server messages if( contentType == "text/plain" ) { // A standard chat message return formatDescription("Chat text message"); } else if( contentType == "text/x-msmsgscontrol" ) { // The contact is typing a message return formatDescription("Typing notification message"); } else if( contentType == "text/x-mms-emoticon" ) { // List of custom emoticons return formatDescription("Custom emoticon list"); } else if( contentType == "text/x-mms-animemoticon" ) { // List of animated custom emoticons return formatDescription("Custom animated emoticon list"); } else if( contentType == "image/gif" || contentType == "application/x-ms-ink" ) { // Ink (drawing) message return formatDescription("Ink drawing message"); } else if( contentType == "text/x-clientcaps" ) { // Third party client-capabilities message return formatDescription("Third-party client capabilities message"); } else if( contentType == "text/x-keepalive" ) { // Third party keep-alive message return formatDescription("Third-party keep-alive message"); } else if( contentType == "text/x-msmsgsinvite" ) { // Old invitation method return formatDescription("Mime-style invitatation message"); } else if( contentType == "application/x-msnmsgrp2p" ) { // New invitation method return formatDescription("P2P-style invitation message"); } else if( contentType == "text/x-msnmsgr-datacast" ) { // Datacast (nudge, voice-clip) return formatDescription("Datacase message, nudge, wink or voice-clip"); } // Unknown message type return formatDescription("Unknown content-type: " + contentType + "!"); } // Return a log description for the message QString NetworkWindow::describeP2PMessage(const QByteArray &message, const int headerStart) { // Prepare to extract certain P2P header fields QDataStream binaryStream( message ); binaryStream.setByteOrder( QDataStream::LittleEndian ); // 0 4 8 16 24 28 32 36 40 48 // |....|....|....|....|....|....|....|....|....|....|....|....| // |sid |mid |offset |totalsize|size|flag|asid|auid|a-datasz | quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId; quint64 dataOffset, totalSize, ackDataSize; // Get header fields bool posFound = binaryStream.device()->seek( headerStart ); if( ! posFound ) { return formatDescription("Message could not be parsed!"); } binaryStream >> sessionId; binaryStream >> messageId; binaryStream >> dataOffset; binaryStream >> totalSize; binaryStream >> dataSize; binaryStream >> flags; binaryStream >> ackSessionId; binaryStream >> ackUniqueId; binaryStream >> ackDataSize; switch( flags ) { case 0: case 0x1000000: // since WLM 2009 // Check for abuse of the P2P system (datacast emulation) if( sessionId == 64 ) { if( dataOffset > 0 ) { QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" ); return formatDescription("Ink message over P2P, " + partName); } else { return formatDescription("Ink message over P2P"); } } // Another awkward data message (webcam setup) if( sessionId != 0 && flags == 0 && dataSize >= 18 && message[ headerStart + 48 ] == '\x80' ) { return formatDescription("Webcam setup message"); } // No flags means the contents is a string with SLP-like mime fields, used to negotiate the session. // datasize of 0 is not handed here, is an invalid packet. // KMess does parse it as ACK message for compatibility with broken clients. if( dataSize == 4 && dataOffset == 0 ) { return formatDescription("P2P Data preparation message"); } else if( dataOffset > 0 ) { QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" ); return formatDescription("P2P Session negotiation message, " + partName); } else if( sessionId == 0 && dataSize > 4 ) { int preambleStart = headerStart + 48; QString slpMessage ( QString::fromUtf8( message.data() + preambleStart, message.size() - preambleStart - 5 ) ); // last 5 is for the footer code. bool isTransfer = slpMessage.contains("Content-Type: application/x-msnmsgr-trans"); if( slpMessage.startsWith("INVITE ") ) { return isTransfer ? formatDescription("P2P Transfer invitation message") : formatDescription("P2P Session invititation message"); } else if( slpMessage.startsWith("MSNSLP/") ) { int statusCode = slpMessage.section(' ', 1, 1).toInt(); if( statusCode == 200 ) { return isTransfer ? formatDescription("P2P Transfer accept message") : formatDescription("P2P Session accept message"); } else if( statusCode == 404 ) { return formatDescription("P2P Session error message"); } else if( statusCode == 481 ) { return formatDescription("P2P Session error message"); } else if( statusCode == 500 ) { return formatDescription("P2P Session error message"); } else if( statusCode == 603 ) { return isTransfer ? formatDescription("P2P Transfer decline message") : formatDescription("P2P Session decline message"); } else { formatDescription("P2P Session status message, type is unknown!"); } } else if( slpMessage.startsWith("ACK ") ) { return formatDescription("P2P Session acknowledgement message (meaning unknown!)"); } else if( slpMessage.startsWith("BYE ") ) { return formatDescription("P2P Session bye message"); } else { return formatDescription("P2P Session negotiation message, type is unknown!"); } } break; case 0x01: return formatDescription("P2P Negative ack, message with ID " + QString::number(ackSessionId) + " failed at byte " + QString::number(ackDataSize)); case 0x02: if( dataSize == 4 ) { return formatDescription("P2P Data preparation message (invalid flag!)"); } else if ( ackDataSize == 4 ) { return formatDescription("P2P Data preparation ack"); } else { return formatDescription("P2P Ack message for message with" " ID " + QString::number(ackSessionId) + " and ackSid " + QString::number(ackUniqueId)); } case 0x04: return formatDescription("P2P Timeout message, session should terminate"); case 0x06: return formatDescription("P2P Timeout message, still waiting for ACK message"); case 0x08: // The logs of the official client call this a "RST" message. return formatDescription("P2P Reset message for message with " "ackSid " + QString::number(ackSessionId) + ", session should terminate"); case 0x20: case 0x1000020: // since WLM 2009 return formatDescription("P2P MsnObject data," " bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) + " of " + QString::number(totalSize)); case 0x40: return formatDescription("P2P Close ack, sender of message with ID " + QString::number(ackSessionId) + " aborted it's own data stream"); case 0x80: return formatDescription("P2P Close ack, receiver of message with ID " + QString::number(ackSessionId) + " aborted the data stream"); case 0x100: return formatDescription("P2P Direct-connection handshake"); case 0x1000030: return formatDescription("P2P File data," " bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) + " of " + QString::number(totalSize)); } return formatDescription("P2P Message, type is unknown!"); } // Destroy the singleton. void NetworkWindow::destroy() { if( ! instance_ ) { // The singleton has already been destroyed, or was never used return; } delete instance_; instance_ = 0; } // Format the description to be displayed QString NetworkWindow::formatDescription(const QString &description) { return "
" + getTime() + "  " + description + "\n"; } // Format the message to be displayed QString NetworkWindow::formatMessage(ConnectionEntry *entry, bool incoming, const QByteArray &message) { // If the connection type is unknown, auto-detect it. if( entry->type == TYPE_UNKNOWN ) { // Try to detemine which type of connection this is. // TODO: As of Qt4 use QByteArray functions instead of memcmp(). if( ( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0", 4) == 0 ) || ( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 ) ) { // Start of direct connection: size field of 4 + foo\0 entry->type = TYPE_DC; } else if( message.size() >= 10 ) { if( memcmp(message.data(), "VER MSNFTP", 10) == 0 ) { entry->type = TYPE_FTP; } else if( memcmp(message.data(), "VER 0 MSNP", 10) == 0 || memcmp(message.data(), "VER 1 MSNP", 10) == 0 ) { entry->type = TYPE_NS; } else if( memcmp(message.data(), "USR ", 4) == 0 || memcmp(message.data(), "ANS ", 4) == 0) { entry->type = TYPE_SB; } else if( memcmp(message.data(), "GET ", 4) == 0 || memcmp(message.data(), "POST ", 5) == 0 || memcmp(message.data(), "type = TYPE_HTTP; } } // If no type is found, assume it's a standard MSN connection. if( entry->type == TYPE_UNKNOWN ) { kmWarning() << "Could not determine connection type for message: " << message.data(); entry->type = TYPE_NS; } } // Format the message. QString logMessage; switch( entry->type ) { // Messages from a DC are always sent as binary case TYPE_DC: if( message.size() < 48 ) { // Not a p2p message, but the length field sent in advance. logMessage += formatRawData(incoming, message, 0, message.size()); logMessage += describeDirectConnectionPreamble(message); } else { // A p2p message from the direct connection logMessage += formatP2PMessage(incoming, message, 0); logMessage += describeP2PMessage(message, 0); } break; // After the TFR is received, the FTP connection sends raw data. case TYPE_FTP_RAW: if( message.size() == 3 ) { // This is a control message logMessage += formatRawData(incoming, message, 0, message.size()); if( message[0] == 0 ) { logMessage += formatDescription("Indicates the size of the next block"); } else if( memcmp(message.data(), "\x01" "\0\0", 3) == 0 ) { logMessage += formatDescription("Cancel message"); } break; } else if( message.size() == 14 && memcmp(message.data(), "BYE 16777989\r\n", 14) == 0 ) { // This is the last BYE message entry->type = TYPE_FTP; // fallthrough } else { logMessage += "(" + QString::number(message.size()) + " bytes of binary data)"; break; } // Assume it's a standard connection. default: // Convert the message to UTF-8 QString utf8Message( QString::fromUtf8( message.data(), message.size() ) ); // Treat Mime messages with P2P content different. if( utf8Message.contains("Content-Type: application/x-msnmsgrp2p") ) { // Find end of Mime, start of binary P2P int mimeEnd = utf8Message.indexOf("\r\n\r\n"); if( mimeEnd != -1 ) { // Extract the first utf-8 mime part, locate start of p2p header QString p2pMimePart( utf8Message.left(mimeEnd + 4) ); int headerStart = p2pMimePart.toUtf8().size(); // convert mimeEnd index, utf-8 may have double characters // Add the mime part and binary p2p part logMessage += formatString( p2pMimePart ); logMessage += formatP2PMessage(incoming, message, headerStart); logMessage += describeP2PMessage(message, headerStart); } } else { // Whole message is in utf-8 // First hide things we don't want to log. if( entry->type == TYPE_HTTP ) { // Hide passwords from the passport login, if they are included in the message int passStart = utf8Message.indexOf(""); if( passStart != -1 ) { passStart += 15; // size of xml tag int passEnd = utf8Message.indexOf("", passStart); int passLen = ( passEnd - passStart ); utf8Message = utf8Message.replace( passStart, passLen, QString( passLen, '*' ) ); } } // Convert to HTML. // Remove last
because KTextBrowser::append() already adds it. logMessage = formatString( utf8Message ); logMessage = logMessage.replace(QRegExp("
$"), QString()); if( entry->type == TYPE_FTP || entry->type == TYPE_NS || entry->type == TYPE_SB ) { if( utf8Message.startsWith("MIME-Version:") ) { logMessage += describeMimeMessage( utf8Message ); } else { logMessage += describeCommand( utf8Message ); } } } } // At a certain point, a MSNFTP connection starts sending raw data. // Detect this so the next formatMessage() invocation handles this. if( entry->type == TYPE_FTP ) { if( message.size() >= 3 && memcmp(message.data(), "TFR", 3) == 0 ) { entry->type = TYPE_FTP_RAW; } } return logMessage; } // Format a msnp2p message to be displayed. QString NetworkWindow::formatP2PMessage(bool incoming, const QByteArray &message, const int headerStart) { QString logMessage; // Prepare to extract certain P2P header fields QDataStream binaryStream( message ); binaryStream.setByteOrder( QDataStream::LittleEndian ); int p2pDataStart = headerStart + 48; bool posFound; // 0 4 8 16 24 28 32 36 40 48 // |....|....|....|....|....|....|....|....|....|....|....|....| // |sid |mid |offset |totalsize|size|flag|asid|auid|a-datasz | quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId; quint64 dataOffset, totalSize, ackDataSize; // Get header fields posFound = binaryStream.device()->seek( headerStart ); // return value ignored. binaryStream >> sessionId; binaryStream >> messageId; binaryStream >> dataOffset; binaryStream >> totalSize; binaryStream >> dataSize; binaryStream >> flags; binaryStream >> ackSessionId; binaryStream >> ackUniqueId; binaryStream >> ackDataSize; #ifdef KMESSTEST KMESS_ASSERT( posFound ); #endif // If the data size is completely wrong, we're dealing with partially sent data here, // caused by DirectConnectionBase::slotSocketReadyWrite(). if( dataSize > 3000 ) { return "(" + QString::number( message.size() ) + " bytes of binary data, flushed afterwards, can't parse)"; } // Show header information logMessage += formatRawData(incoming, message, headerStart, 48, 4, 16); // Add header debug details logMessage += ""; logMessage += "\n
session id: " + QString::number(sessionId); logMessage += "\n
message id: " + QString::number(messageId); logMessage += "\n
offset: " + QString::number(dataOffset); logMessage += "\n
total: " + QString::number(totalSize); logMessage += "\n
size: " + QString::number(dataSize); logMessage += "\n
flags: 0x" + QString::number(flags, 16); if( flags == 0x100 ) { // For the handshake message, the last three fields are replaced with the nonce. logMessage += "\n
nonce: " + P2PMessage::extractNonce( message.data(), headerStart + 32 ); } else { logMessage += "\n
ackSid: " + QString::number(ackSessionId); logMessage += "\n
ackUid: " + QString::number(ackUniqueId); logMessage += "\n
ackSize: " + QString::number(ackDataSize); } logMessage += "
"; // The message a utf-8 (MSNSLP) payload if the session id is zero. const char *dataPtr = ( message.data() + p2pDataStart ); bool hasSlpPayload = ( sessionId == 0 && dataSize > 0 ); int safeEnd = qMin( message.size(), (int) ( p2pDataStart + dataSize ) ); int safeSize = qMin( (int) dataSize, ( message.size() - p2pDataStart ) ); if( hasSlpPayload ) { // See if the SLP body is terminated with a final null character (should be). int slpLength = safeSize; bool hasSlpNullEnd = ( message[ p2pDataStart + slpLength - 1 ] == 0x00 ); if( hasSlpNullEnd ) { slpLength--; } // Display the SLP body QString slpMessage( QString::fromUtf8( dataPtr, slpLength ) ); logMessage += "\n
" + formatString( slpMessage ); // Add message footer if( hasSlpNullEnd ) { // Display the \0 that was dropped in the QString::fromUtf8() call. logMessage += formatRawData( incoming, message, slpLength, 1 ); } } else if( dataSize >= 18 && message.size() > p2pDataStart && message[ p2pDataStart ] == '\x80' ) { // Webcam invitation QString camMessage( QString::fromUtf16( reinterpret_cast( dataPtr + 10 ), ( safeSize - 10 - 1 ) / int( sizeof( char16_t ) ) ) ); logMessage += "\n
" + formatRawData( incoming, message, p2pDataStart, 10, 1 ) // 1 per col. + "\n
" + formatString( camMessage ) + "\n
" + formatRawData( incoming, message, safeEnd - 2, 2 ); // final null (in utf16). } else { // Don't display the data. logMessage += "\n
(" + QString::number(dataSize) + " bytes of binary data)"; } // Display the remaining bytes, if any. // This is the footer when the p2p message is sent over the SB. if( safeEnd < message.size() ) { logMessage += "
" + formatRawData( incoming, message, safeEnd, message.size() - safeEnd ); } return logMessage; } // Format a utf-8 string to be displayed QString NetworkWindow::formatRawData(bool incoming, const QByteArray &message, const int start, const int length, const int bytesPerCol, const int bytesPerRow) { QString logMessage; logMessage += " 0 ) { if( i % bytesPerRow == 0 ) { logMessage += "
"; } else if( i % bytesPerCol == 0 ) { logMessage += " "; } } // Convert to hex string const char *hexMap = "0123456789abcdef"; char byte = message[start + i]; int upper = (byte & 0xf0) >> 4; int lower = (byte & 0x0f); logMessage += hexMap[upper]; logMessage += hexMap[lower]; } logMessage += "
"; return logMessage; } // Format a utf-8 string to be displayed QString NetworkWindow::formatString(const QString &message) { // Parse any HTML characters. QString logMessage( Qt::escape( message ) ); // Format newline characters logMessage = logMessage.replace("\r\n", "\\r\\n
"); logMessage = logMessage.replace("\n", "\\n
"); logMessage = logMessage.replace("\r", "\\r
"); return logMessage; } // Find the tab for the connection, or create a new one. NetworkWindow::ConnectionEntry * NetworkWindow::getConnectionEntry(QObject *connection) { #ifdef KMESSTEST KMESS_ASSERT( connection != 0 ); // no KMESS_NULL(), this is debug code already. #endif // Find the entry ConnectionEntry *entry = connectionViews_.value( connection, 0 ); // If the view didn't exist before, create a new one. if( entry == 0 ) { // Create new entry entry = new ConnectionEntry; entry->logView = new KTextBrowser( connectionTabs_ ); entry->logView->setObjectName( connection->objectName() + "_log" ); entry->type = TYPE_UNKNOWN; entry->lastMessage = QDateTime::currentDateTime(); connectionViews_.insert( connection, entry ); // Add new tab connectionTabs_->addTab( entry->logView, QIcon::fromTheme( "edit-select-all" ), connection->objectName() ); // Enable the Close All Tabs button enableButton( User3, ( connectionViews_.count() > 1 ) ); } // Initialize new connections and reinitialize old ones as open entry->isClosed = false; return entry; } // Return the current time. QString NetworkWindow::getTime() const { return "" + QTime::currentTime().toString( "hh:mm:ss:zzz" ) + ""; } // Set the title of the connection in the display void NetworkWindow::setConnectionTitle(QObject *connection, const QString &title) { ConnectionEntry *entry = getConnectionEntry(connection); // If empty, set the initial label. Otherwise add a IRC-like rename message if( entry->type == TYPE_UNKNOWN ) { connectionTabs_->setTabText( connectionTabs_->indexOf( entry->logView ), title ); entry->logView->append("

Log started by " + QString(connection->objectName()) + "

"); } else { entry->logView->append("

re-initialized as: " + title + "

"); } } // Show the window if there are connection logs to show void NetworkWindow::show() { if( connectionTabs_->count() < 1 ) { KMessageBox::error( this, i18n("No connections are present.\nCannot open the Network Window.") ); return; } KMessDialog::show(); } // The 'save tab' button was pressed. void NetworkWindow::saveCurrentTab() { if( connectionTabs_->currentWidget() == 0 ) return; QString path; QString name( connectionTabs_->tabText( connectionTabs_->currentIndex() ) ); path = QFileDialog::getSaveFileName( this, QString(), QString(), "*.html" ); if( path.isEmpty() ) return; kmDebug() << "NetworkWindow - saving network log for window '" << name << "' to '" << path << "'."; bool failed = false; // Create and open the file. QFile file( path ); if( ! file.open( QIODevice::WriteOnly ) ) { failed = true; kmWarning() << "File save failed - couldn't open file."; } // Output the HTML with the right text encoding QString text( static_cast( connectionTabs_->currentWidget() )->toHtml() ); QTextStream textStream( &file ); textStream.setCodec( QTextCodec::codecForLocale() ); textStream << text; file.close(); file.flush(); if( failed || ! file.exists() ) { KMessageBox::error( this, i18n("Could not save the Network Window log. Make sure you have permission to write in the folder where it is being saved.") ); } } // The 'clear tab' button was pressed. void NetworkWindow::clearCurrentTab() { if( connectionTabs_->currentWidget() == 0 ) return; static_cast( connectionTabs_->currentWidget() )->clear(); } // The 'close all tabs' button was pressed. void NetworkWindow::closeAllTabs() { // Activate the tab of the main connection connectionTabs_->setCurrentIndex( 0 ); // Get the main connection widget QWidget *mainConnectionWidget = connectionTabs_->widget( 0 ); // Loop through all tabs foreach( ConnectionEntry *entry, connectionViews_ ) { // Don't close the main connection tab if( entry->logView == mainConnectionWidget ) { continue; } connectionTabs_->removeTab( connectionTabs_->indexOf( entry->logView ) ); closeTab( entry->logView ); } } // A 'close tab' button was pressed. void NetworkWindow::closeTab( QWidget *removedWidget ) { if( connectionTabs_->indexOf( removedWidget ) == 0 ) { KMessageBox::information( this, i18n("Cannot close the main connection tab.") ); return; } // Remove the widget from the list of views QHashIteratorit( connectionViews_ ); while( it.hasNext() ) { it.next(); if( it.value()->logView == removedWidget ) { connectionViews_.remove( it.key() ); delete it.value(); break; } } // Delete the page of the removed item delete removedWidget; // Disable the Close All Tabs button when there are no tabs enableButton( User3, ( connectionViews_.count() > 1 ) ); } // Return the primary instance of the network window. NetworkWindow* NetworkWindow::instance() { // Create on demand if( instance_ == 0 ) { instance_ = new NetworkWindow(); } // Return active instance return instance_; } // A tab was selected. void NetworkWindow::slotUpdateWidgets( int selectedTab ) { bool enable = false; // Automatically obtain the tab index instead of getting it as a parameter if( selectedTab < -1 ) { selectedTab = connectionTabs_->currentIndex(); } // If there are open tabs, find out which one is selected. // If there are none, just disable the command sending group if( selectedTab != -1 ) { QWidget *currentWidget = connectionTabs_->currentWidget(); QHashIterator it( connectionViews_ ); // Iterate through all connections to find whether the current one is connected or not while( it.hasNext() ) { it.next(); if( it.value()->logView == currentWidget ) { // The active tab's connection is active, enable the widgets if( ! it.value()->isClosed ) { enable = true; } break; } } } commandSendingGroup_->setEnabled( enable ); } // Send a command void NetworkWindow::slotSendCommand() { // Get the text fields' text QString command( commandEdit_->text().toUpper() ); QString payload( payloadEdit_->toPlainText() ); // Empty commands can't be sent if( command.isEmpty() || connectionTabs_->currentIndex() == -1 ) { commandEdit_->setFocus(); return; } // Warn the developer about what he or she is doing. int result = KMessageBox::warningContinueCancel( this, i18n( "Sending commands to the server is a risky operation.
" "If you do not know how to exactly do it, " "you could be lucky and just get disconnected, or you may " "incur in more serious consequences.
" "You have been warned!
" "Do you want to continue sending this message?" ), i18n( "Network Window" ), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "sendServerCommandWarning" ); // Bail out, just in time if( result != KMessageBox::Continue ) { return; } // The developer knows what he or she is doing, send the command // Replace all newlines with the kind used by the server payload.replace( QRegExp( "\r?\n" ), "\r\n" ); // Empty payloads can, and will be replaced with a single newline if( payload.isEmpty() ) { payload = "\r\n"; } else if( ! payload.endsWith( "\r\n" ) ) { // Warn the developer that the payloads must end with a newline int result = KMessageBox::warningTwoActions( this, i18n( "The payload you are trying to send does not end with the " "required newline ('\\r\\n')!
" "Do you want KMess to add it for you?" ), i18n( "Network Window" ), KGuiItem( i18n( "Yes" ) ), KGuiItem( i18n( "No" ) ), "noPayloadWarning" ); // Append the missing newline to the payload if( result == KMessageBox::PrimaryAction ) { payload.append( "\r\n" ); } } // Find the currently active tab QWidget *currentWidget = connectionTabs_->currentWidget(); QHashIterator it( connectionViews_ ); void *connection; const ConnectionEntry *entry; // Iterate through all connections to find out which one's tab is selected in the UI while( it.hasNext() ) { it.next(); // Save the entry, we'll need it later entry = it.value(); // If the current tab is for this connection.. if( entry->logView == currentWidget ) { // .. disallow sending commands to disconnected connections if( entry->isClosed ) { commandEdit_->setFocus(); return; } // ..save the tab's connection to send commands there connection = it.key(); break; } } // Send the command to the right type of connection MsnNotificationConnection *nsConnection; switch( entry->type ) { case TYPE_NS: case TYPE_SB: nsConnection = static_cast( connection ); // Disallow sending commands to disconnected connections if( ! nsConnection->isConnected() ) { return; } // Send the type of command the developer wants if( sendStandardCmdRadio_->isChecked() ) { nsConnection->sendCommand( command, payload ); } else { nsConnection->sendMimeMessage( MsnNotificationConnection::ACK_ALWAYS, MimeMessage( payload ) ); } return; // TODO: Enable sending commands to these types of connections. case TYPE_UNKNOWN: case TYPE_FTP: case TYPE_FTP_RAW: case TYPE_DC: case TYPE_HTTP: default: KMessageBox::error( 0, i18n( "Cannot send commands to this kind of connection!" ) ); return; } }