Files
kmess/src/dialogs/networkwindow.cpp
T
2026-07-06 09:37:59 +02:00

1435 lines
50 KiB
C++

/***************************************************************************
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 <QFile>
#include <QRegExp>
#include <QSize>
#include <QDateTime>
#include <QTabWidget>
#include <QTextCodec>
#include <QTextStream>
#include <QFileDialog>
#include <KLocalizedString>
#include <KMessageBox>
#include <KTextBrowser>
// 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 <code>KMESS_NET_CLOSE</code>.
* @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 <code>KMESS_NET_INIT</code>.
* @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 <code>KMESS_NET_RECEIVED</code>.
* @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 <code>KMESS_NET_SENT</code>.
* @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( " ", " &nbsp;")
.replace( QRegExp("[\r\n] "), "\n&nbsp;");
// Add spacer if previous message was some time ago.
QDateTime now = QDateTime::currentDateTime();
if( entry->lastMessage.secsTo( now ) > 20 )
{
entry->logView->append( "<p style='margin: 0; font-size: 8pt'>&nbsp;<!-- time spacer --></p>" );
}
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( "<p style='margin: 0'><font color='#333333'>&nbsp; " + message + "</font></p>\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( "<p style='margin: 0; color: #0033FF'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, true, message) + "</p>\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( "<p style='margin: 0; color: #FF3300'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, false, message) + "</p>\n" );
addMessage(entry, html);
}
// Inform the connection was closed
void NetworkWindow::connectionClosed(QObject *connection)
{
// Add the message
ConnectionEntry *entry = getConnectionEntry(connection);
entry->logView->append("<p style='margin:0; color: #C2C2C2'>the connection was closed<br>" + getTime() + "</p>\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 "<br>" + getTime() + "<font color='#333333'>&nbsp; " + description + "</font>\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(), "<?xml", 5) == 0)
{
entry->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 += "<font color=gray>(" + QString::number(message.size()) + " bytes of binary data)</font>";
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("<wsse:Password>");
if( passStart != -1 )
{
passStart += 15; // size of xml tag
int passEnd = utf8Message.indexOf("</wsse:Password>", passStart);
int passLen = ( passEnd - passStart );
utf8Message = utf8Message.replace( passStart, passLen, QString( passLen, '*' ) );
}
}
// Convert to HTML.
// Remove last <br> because KTextBrowser::append() already adds it.
logMessage = formatString( utf8Message );
logMessage = logMessage.replace(QRegExp("<br>$"), 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 "<font color=gray>(" + QString::number( message.size() ) + " bytes of binary data, flushed afterwards, can't parse)</font>";
}
// Show header information
logMessage += formatRawData(incoming, message, headerStart, 48, 4, 16);
// Add header debug details
logMessage += "<font color='#333333'>";
logMessage += "\n<br>session id: " + QString::number(sessionId);
logMessage += "\n<br>message id: " + QString::number(messageId);
logMessage += "\n<br>offset: " + QString::number(dataOffset);
logMessage += "\n<br>total: " + QString::number(totalSize);
logMessage += "\n<br>size: " + QString::number(dataSize);
logMessage += "\n<br>flags: 0x" + QString::number(flags, 16);
if( flags == 0x100 )
{
// For the handshake message, the last three fields are replaced with the nonce.
logMessage += "\n<br>nonce: " + P2PMessage::extractNonce( message.data(), headerStart + 32 );
}
else
{
logMessage += "\n<br>ackSid: " + QString::number(ackSessionId);
logMessage += "\n<br>ackUid: " + QString::number(ackUniqueId);
logMessage += "\n<br>ackSize: " + QString::number(ackDataSize);
}
logMessage += "</font>";
// 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<br>" + 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<const char16_t *>( dataPtr + 10 ),
( safeSize - 10 - 1 ) / int( sizeof( char16_t ) ) ) );
logMessage += "\n<br>" + formatRawData( incoming, message, p2pDataStart, 10, 1 ) // 1 per col.
+ "\n<br>" + formatString( camMessage )
+ "\n<br>" + formatRawData( incoming, message, safeEnd - 2, 2 ); // final null (in utf16).
}
else
{
// Don't display the data.
logMessage += "\n<br><font color=gray>(" + QString::number(dataSize) + " bytes of binary data)</font>";
}
// Display the remaining bytes, if any.
// This is the footer when the p2p message is sent over the SB.
if( safeEnd < message.size() )
{
logMessage += "<br>" + 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 += "<tt><font color=";
logMessage += (incoming? "#4747C2" : "#CC334D");
logMessage += ">";
// If length field is corrupt (e.g. 432712149, avoid looping though 4GB of memory..
int safeLength = qMin( length, message.size() - start );
for(int i = 0; i < safeLength; i++)
{
// Add spacers between ints, make it span multiple rows.
if( i > 0 )
{
if( i % bytesPerRow == 0 )
{
logMessage += "<br>";
}
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 += "</font></tt>";
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", "<tt><font color=gray>\\r\\n</font></tt><br>");
logMessage = logMessage.replace("\n", "<tt><font color=gray>\\n</font></tt><br>");
logMessage = logMessage.replace("\r", "<tt><font color=gray>\\r</font></tt><br>");
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 "<font color=black size=1>" + QTime::currentTime().toString( "hh:mm:ss:zzz" ) + "</font>";
}
// 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("<p><font color='#C2C2C2'>Log started by " + QString(connection->objectName()) + "</font></p>");
}
else
{
entry->logView->append("<p><font color='#C2C2C2'>re-initialized as: " + title + "</font></p>");
}
}
// 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<KTextBrowser *>( 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<KTextBrowser *>( 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
QHashIterator<void*,ConnectionEntry*>it( 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<void*,ConnectionEntry*> 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.<br />"
"If you do not know how to exactly do it, "
"you could be lucky and just get disconnected, or <i>you may "
"incur in more serious consequences</i>.<br />"
"You have been warned!<br />"
"<b>Do you want to continue sending this message?</b>" ),
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')!<br />"
"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<void*,ConnectionEntry*> 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<MsnNotificationConnection*>( 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;
}
}