1
/***************************************************************************
2
msnsocketnull.cpp - description
4
begin : Sat May 10 2008
5
copyright : (C) 2008 by Valerio Pilo
6
email : valerio@kmess.org
7
***************************************************************************/
9
/***************************************************************************
11
* This program is free software; you can redistribute it and/or modify *
12
* it under the terms of the GNU General Public License as published by *
13
* the Free Software Foundation; either version 2 of the License, or *
14
* (at your option) any later version. *
16
***************************************************************************/
18
#include "msnsocketnull.h"
22
#include <QHostAddress>
26
#include <KInputDialog>
29
#include <config-kmess.h>
30
#include "../kmessdebug.h"
31
#include "../contact/contact.h"
32
#include "../currentaccount.h"
35
// Initialize static class members
36
QMap<QString,MsnSocketNull::FakeContact> MsnSocketNull::contactList_;
37
QMap<QString,QString> MsnSocketNull::groupList_;
38
QMap<QString,MsnSocketNull::OnlineDetails> MsnSocketNull::onlineList_;
41
#ifdef KMESSDEBUG_CONNECTION // Area-specific debug statements
42
#define KMESSDEBUG_CONNECTION_SOCKET_NULL
46
// Change this to the position where you want KMess to search for a debug log
48
#define DEBUG_LOG_FILE QDir::homePath() + "/kmessdebug.log"
53
* @brief The constructor
57
MsnSocketNull::MsnSocketNull( ServerType serverType )
58
: MsnSocketBase( serverType )
61
setObjectName( "MsnSocketNull" );
63
// Connect the server request timer
64
connect( &responseTimer_, SIGNAL( timeout() ),
65
this, SLOT ( sendResponse() ) );
67
// If the lists are already filled, we're done
68
if( ! contactList_.isEmpty() || ! groupList_.isEmpty() )
73
// If that file exists, parse it
74
kDebug() << "File" << DEBUG_LOG_FILE << "file exists?" << QFile::exists( DEBUG_LOG_FILE );
75
if( QFile::exists( DEBUG_LOG_FILE ) )
77
QFile file( DEBUG_LOG_FILE );
78
file.open( QIODevice::ReadOnly );
79
QStringList fileContents( QString( file.readAll() ).split( QString( "\n" ) ) );
83
QList<int> connections;
84
QStringList fileCommands;
85
QRegExp commandRegexp( "\"\\s+-\\s+<<<\\s+\\((.+)\\)$" );
87
foreach( const QString &row, fileContents )
89
index = commandRegexp.indexIn( row );
93
QString command( commandRegexp.cap( 1 ) );
95
// Every connection starts with a good SYN
96
if( command.startsWith( "\"SYN\"" ) )
98
connections.append( fileCommands.size() );
102
// The only messages we care of are the list retrieval ones
103
if( ! command.startsWith( "\"LSG\"" ) &&
104
! command.startsWith( "\"LST\"" ) &&
105
! command.startsWith( "\"ILN\"" ) )
110
// Extract the QStringList of commands from the row
111
fileCommands.append( command );
115
kDebug() << "###########################################################################";
116
kDebug() << connections;
117
kDebug() << "###########################################################################";
118
for( int i = 0; i < fileCommands.size(); i++ )
120
if( connections.contains( i ) )
122
kDebug() << "###########################################################################";
123
kDebug() << "Connection" << connections.indexOf( i );
124
kDebug() << "**************";
127
kDebug() << fileCommands[ i ];
130
kDebug() << "###########################################################################";
134
if( connections.size() > 1 )
137
number = KInputDialog::getInteger( "KMess log mimicker", "Choose a connection from the log file \""
138
+ DEBUG_LOG_FILE + "\" to replicate:",
149
number--; // reindex from 0
153
int end = ( number == connections.size() - 1 ) ? fileCommands.size() - 1 : connections[ number ];
155
for( int idx = connections[ number ]; idx < end; idx++ )
157
if( fileCommands[idx].isEmpty() )
160
QStringList parts( fileCommands[idx].split( ", " ) );
161
for( int i=0; i < parts.size(); i++ )
163
parts[ i ] = parts[ i ].mid( 1, parts[i].size() - 2 );
166
if( parts[0] == "LSG" )
168
//("LSG", "KMess", "b07456fe-a6fd-4746-b63a-62f363a923ef")
169
groupList_[ parts[2] ] = QUrl::fromPercentEncoding( parts[1].toUtf8() );
171
else if( parts[0] == "LST" )
173
// ("LST", "N=friend@site.dom", "F=[a=1][c=4][b]-%20Emet%20-[/b][/c][/a]", "C=aef50adb-434d-4e85-bcfd-0d14c3baf529", "11", "1", "7ed2567a-71f0-4bf1-8ab6-9549f616c093")
174
// ("LST", "N=non-friend@site.dom", "10", "1")
175
FakeContact newContact;
177
if( parts.size() == 4 )
179
newContact.handle = parts[1].mid(2);
180
newContact.friendlyName = parts[1].mid(2);
181
newContact.guId = parts[1].mid(2);
182
newContact.lists = parts[2].toInt();
183
newContact.groupIds = QStringList();
187
newContact.handle = parts[1].mid(2);
188
newContact.friendlyName = QUrl::fromPercentEncoding( parts[2].mid(2).toUtf8() );
189
newContact.guId = parts[3].mid(2);
190
newContact.lists = parts[4].toInt();
191
newContact.groupIds = ( parts.size() > 6 ) ? parts[6].split( "," ) : QStringList();
194
contactList_[ newContact.handle ] = newContact;
196
else if( parts[0] == "ILN" )
198
// ("ILN", "10", "BSY", "contact@site.com", "Nickname%20encoded", "1342210100", "%3Cmsnobj%20Creator%3D%22contact%40site.com%22%20Size%3D%229333%22%20Type%3D%223%22%20Location%3D%22KMess.tmp%22%20Friendly%3D%22AA%3D%3D%22%20SHA1D%3D%22ysV4cHpd04lOhjHYB%2BTnggTwjdA%3D%22%20SHA1C%3D%226jkVICIHHIgJvSZAzgJ6FlNK10c%3D%22%2F%3E")
199
contactList_[ parts[3] ].friendlyName = QUrl::fromPercentEncoding( parts[4].toUtf8() );
201
OnlineDetails details;
202
details.msnObject = parts[6];
203
details.capabilities = parts[5].toUInt();
204
details.status = parts[2];
205
onlineList_[ parts[3] ] = details;
209
else // Generate a standard contact list
211
groupList_[ "0000-family" ] = "Family";
212
groupList_[ "000-friends" ] = "Friends";
213
groupList_[ "0-workmates" ] = "Workmates";
214
groupList_[ "00000-empty" ] = "Empty Group";
217
int isFriend = Contact::MSN_LIST_ALLOWED | Contact::MSN_LIST_FRIEND | Contact::MSN_LIST_REVERSE;
218
// NOTE: Add some invalid characters to the email addresses, so they will never resolve to possibly valid mailboxes.
219
contactList_.insert( "mum@kmess.orgX", createFakeContact
220
( "mum@kmess.orgX", "Mom", "c1mum", QStringList() << "0000-family", isFriend ) );
221
contactList_.insert( "sister@kmess.orgX", createFakeContact
222
( "sister@kmess.orgX", "Sis", "c2sister", QStringList() << "0000-family", isFriend ) );
223
contactList_.insert( "frank@kmess.com" , createFakeContact
224
( "frank@kmess.comX", "[u]Frankie[/u]", "c3frank", QStringList() << "000-friends", isFriend ) );
225
contactList_.insert( "elaine@kmess.comX", createFakeContact
226
( "elaine@kmess.com", "[c=orange]Ely[/c=yellow]", "c4elaine", QStringList() << "0-workmates", isFriend ) );
227
contactList_.insert( "badguy@bothering.comX", createFakeContact
228
( "badguy@bothering.comX", "[b]Bad Guy[/b]", "c5badguy", QStringList(), Contact::MSN_LIST_BLOCKED ) );
229
contactList_.insert( "idiot@friend.comX", createFakeContact
230
( "jack@idiotry.comX", "Jackie - OLOLOLO", "c6jack", QStringList() << "000-friends", isFriend | Contact::MSN_LIST_BLOCKED ) );
231
contactList_.insert( "sylvie@hotmail.comX", createFakeContact
232
( "sylvie@hotmail.comX", "(*)Sylvie(*) I'm working! (l)", "c7sylvie", QStringList() << "0-workmates", isFriend ) );
233
contactList_.insert( "evan123@hotmail.usX", createFakeContact
234
( "evan123@hotmail.usX", "€v1³", "c8evan", QStringList() << "0-workmates", isFriend ) );
235
contactList_.insert( "richard@multinational.comX", createFakeContact
236
( "richard@multinational.comX", "Richard ~ next week: vacation! :D", "c9richard", QStringList() << "0-workmates", isFriend ) );
237
contactList_.insert( "awesome_girl222@hotmail.comX", createFakeContact
238
( "awesome_girl222@hotmail.comX", "[b][c=12]Katie (L)[/c=21] [c=21]I love you...[/c=12][/b]", "c10katie", QStringList() << "0000-family", isFriend ) );
239
contactList_.insert( "unknown@unknown.wtfX", createFakeContact
240
( "unknown@unknown.wtfX", "Unknown Faceless Guy", "c11unknown", QStringList(), Contact::MSN_LIST_ALLOWED | Contact::MSN_LIST_REVERSE ) );
242
// Same seed everytime, so we get the same results when starting multiple times KMess
245
QMap<QString,FakeContact>::ConstIterator cIt;
246
for( cIt = contactList_.constBegin(); cIt != contactList_.constEnd(); ++cIt )
248
// Don't change to online all contacts
249
if( ( rand() % 4 ) == 0 || ( rand() % 3 ) == 0 )
252
OnlineDetails details;
253
details.msnObject = ""; // no pic atm
254
details.capabilities = 1985855532;
255
details.status = MsnStatus::getCode( (Status)(rand() % 8) );
256
onlineList_[ cIt.key() ] = details;
264
* @brief The destructor
268
MsnSocketNull::~MsnSocketNull()
270
// Disconnect from the server on exit
273
disconnectFromServer();
277
responseTimer_.stop();
280
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
281
kDebug() << "DESTROYED";
286
MsnSocketNull::FakeContact MsnSocketNull::createFakeContact( const QString &handle, const QString &friendlyName, const QString &guId, const QStringList &groupIds, int lists ) const
288
MsnSocketNull::FakeContact c;
290
c.friendlyName = friendlyName;
292
c.groupIds = groupIds;
299
* @brief Connect to the given server
301
* The initial connection is always made through the main Gateway.
302
* The first server response usually carries the new gateway IP
303
* which will be used for the rest of the connection.
305
* @param server The server hostname or IP address.
306
* @param port The port to connect to. Not used - all of our
307
* traffic goes through the default port.
309
void MsnSocketNull::connectToServer( const QString& server, const quint16 port )
311
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
312
if( server.isEmpty() && port == 0 )
314
kDebug() << "Faking connection to the default server.";
318
kDebug() << "Faking connection to server at " << server << ":" << port << ".";
322
// Set our internal state variables
324
setSendPings( true );
325
responseTime_.start();
327
// Start answering to the queued messages
330
// Signal we're ready
332
emit statusMessage( i18n( "Connected" ), false );
338
* @brief Send to the client the fake contact list
340
* This method sends the entire groups and contacts list back to KMess.
342
void MsnSocketNull::deliverContactList()
344
QMap<QString,QString>::ConstIterator gIt;
345
for( gIt = groupList_.constBegin(); gIt != groupList_.constEnd(); ++gIt )
347
emit dataReceived( QStringList() << "LSG"
348
<< QUrl::toPercentEncoding( gIt.value() )
349
<< gIt.key(), QByteArray() );
352
QMap<QString,FakeContact>::ConstIterator cIt;
353
for( cIt = contactList_.constBegin(); cIt != contactList_.constEnd(); ++cIt )
355
const FakeContact contact = cIt.value();
356
emit dataReceived( QStringList() << "LST"
357
<< "N=" + contact.handle
358
<< "F=" + QUrl::toPercentEncoding( contact.friendlyName )
359
<< "C=" + contact.guId
360
<< QString::number( contact.lists )
361
<< "1" // Meaning unknown
362
<< contact.groupIds.join(","),
366
// Same seed everytime, so we get the same results when starting multiple times KMess
369
QMap<QString,OnlineDetails>::ConstIterator oIt;
370
for( oIt = onlineList_.constBegin(); oIt != onlineList_.constEnd(); ++oIt )
372
const OnlineDetails details = oIt.value();
373
emit dataReceived( QStringList() << "ILN"
374
<< "999" // TODO: what is this? the ack number of the initial CHG? We don't use it.
376
<< contactList_[ oIt.key() ].handle
377
<< contactList_[ oIt.key() ].friendlyName
378
<< QString::number( details.capabilities )
387
* @brief Disconnect from the server
389
* This method closes the connection to the server and does some cleaning up.
390
* It empties all buffers, and resets the internal state.
391
* To avoid losing data, all pending requests are merged into a single huge
392
* request and sent altogether, before closing the connection. We won't
393
* receive responses for them; but at least we lose no outgoing data.
395
* @param isTransfer When set, a disconnected() signal won't be fired.
396
* This is used to handle transfers to another server
397
* without noticing a disconnect.
399
void MsnSocketNull::disconnectFromServer( bool isTransfer )
402
setSendPings( false );
404
// Just clean up if we are disconnected already
411
messageQueue_.clear();
412
responseTimer_.stop();
414
#ifdef KMESS_NETWORK_WINDOW
415
KMESS_NET_CLOSE( this );
418
// Do not signal a disconnection when we are transferring to another "server"
424
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
425
kDebug() << "Emitting disconnection signal.";
434
* @brief Return the local IP address
436
* Just returns "127.0.0.1". We're not going anywhere.
438
* @returns The IP address the socket uses at the system.
440
QString MsnSocketNull::getLocalIp() const
442
// We don't care about our own IP, return a generic one
443
return QHostAddress( QHostAddress::LocalHost ).toString();
447
* @brief Schedule the next response
449
* Restarts the internal timer to determine when the next command
450
* will be sent back to KMess
452
void MsnSocketNull::scheduleNext()
454
// Also send the ping if needed
455
if( sendPings_ && responseTime_.restart() > 30000 )
457
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
458
kDebug() << "Emitting ping signal.";
464
int nextResponseInterval = SOCKET_NULL_LAGGINESS_MIN + ( rand() % ( SOCKET_NULL_LAGGINESS_MAX + 1 ) );
465
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
466
kDebug() << "Next response will happen in" << nextResponseInterval << "milliseconds.";
469
responseTimer_.start( nextResponseInterval );
475
* @brief Answer a queued command
477
* This slot sends a response to a command sent by kmess
479
void MsnSocketNull::sendResponse()
481
// When the queue is empty, just try again every now and then
482
if( messageQueue_.isEmpty() )
484
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
485
kDebug() << "Queue is empty, idling.";
491
// Extract the oldest message from the queue
493
QByteArray lastMessage( messageQueue_.takeFirst() );
494
QString commandLine( QString::fromUtf8( lastMessage.data(), lastMessage.size() ) );
495
QStringList command( commandLine.trimmed().split(" ") );
497
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
498
kDebug() << "Parsing oldest queue message:" << command;
501
// Each server type has a different message set with different responses; so we implement them separately
502
if( serverType_ == MsnSocketBase::SERVER_NOTIFICATION )
504
success = sendNotificationServerResponse( command );
508
success = sendSwitchboardServerResponse( command );
511
// Answer the next message
512
if( success && ! messageQueue_.isEmpty() )
518
// Stop parsing stuff if there's an error or there's no messages to parse
519
responseTimer_.stop();
525
// Respond to a command when acting as a Notification Server
526
bool MsnSocketNull::sendNotificationServerResponse( const QStringList& command )
528
QStringList replyCommand;
529
QByteArray payloadData;
531
// Set the command for the reply to the name and ack id, so we can fill it with some other stuff
532
for( int i = 0; i < 2; i++ )
534
if( command.size() <= i )
538
replyCommand.append( command[ i ] );
541
// Reply to the message, if it contains a known command
543
if( command[0] == "VER" )
545
// Initial version info
547
// Only reply with "VER <ackID> <MSNP version>"
549
emit dataReceived( replyCommand << command[2], payloadData );
551
else if( command[0] == "CVR" )
553
emit dataReceived( replyCommand << KMESS_VERSION << KMESS_VERSION << KMESS_VERSION
554
<< "http://www.kmess.org/download/" << "http://www.kmess.org/",
557
else if( command[0] == "USR" )
559
// Test once the server redirection capabilities of KMess
560
if( ! isTransferDone_ )
562
isTransferDone_ = true;
564
// TODO: WTF are the other two arguments? We don't use them..
565
messageQueue_ << QString( "XFR %1 NS fakeServerRedirection::1234 0 fakeServerRedirection2::4321" )
566
.arg( command[1] ).toUtf8();
570
// We can't easily proceed with faking authentication, since it involves HTTP requests external to
571
// the scope of this class. Therefore, we just 'confirm' authentication
572
emit dataReceived( replyCommand << "OK"
573
<< CurrentAccount::instance()->getHandle()
574
<< SOCKET_NULL_IS_ACCOUNT_VERIFIED
579
else if( command[0] == "XFR" )
581
// Redirection request: we emit it as is
582
emit dataReceived( command, payloadData );
584
else if( command[0] == "OUT" )
586
// Quit/disconnection request
588
// If needed, fake having been disconnected from this server due to connection from another place/client
589
if( SOCKET_NULL_FAKE_OTH )
591
emit dataReceived( replyCommand << "OTH", payloadData );
594
disconnectFromServer();
596
else if( command[0] == "SYN" )
598
// Contact list synchronization
599
QDateTime now( QDateTime::currentDateTime() );
601
// TODO: When we'll start caching contact list contents, this could need to be updated
602
// to avoid sync issues with old data
603
QString listDate( now.toString( Qt::ISODate ) + ".00-00:00" );
605
emit dataReceived( replyCommand << listDate << listDate
606
<< QString::number( contactList_.count() )
607
<< QString::number( groupList_.count() ),
610
// Then start sending out to the client our Original Fake(tm) of the contact list
611
deliverContactList();
613
else if( command[0] == "GCF" )
616
// TODO: It should send something back so check what and replicate here
618
else if( command[0] == "CHG" )
620
// Status change. Reply as is.
621
emit dataReceived( command, payloadData );
623
else if( command[0] == "UUX" )
625
// Response to an UUX command: reply without the original payload (if any)
627
emit dataReceived( replyCommand, payloadData );
629
else if( command[0] == "URL" )
631
if( command[2] == "INBOX" )
633
replyCommand << "inbox"; // TODO: Fix URL
634
replyCommand << "http://mail.live.com/"; // TODO: Fix URL
635
replyCommand << "wtf"; // TODO: Fix URL
637
else if( command[2] == "COMPOSE" )
639
replyCommand << "compose"; // TODO: Fix URL
640
replyCommand << "http://mail.live.com/"; // TODO: Fix URL
641
replyCommand << "wtf"; // TODO: Fix URL
645
replyCommand << "http://www.kmess.org/";
648
emit dataReceived( replyCommand, payloadData );
650
else if( command[0] == "ADC" )
652
QString arg( command[3] );
655
if( arg.startsWith("N=") )
657
handle = arg.mid(2).toLower();
659
else if( arg.startsWith("F=") )
661
str = QUrl::fromPercentEncoding( arg.mid(2).toUtf8() );
662
foreach( const FakeContact &contact, contactList_ )
664
if( contact.friendlyName == str )
666
handle = contact.handle;
671
else if( arg.startsWith("C=") )
674
foreach( const FakeContact &contact, contactList_ )
676
if( contact.guId == str )
678
handle = contact.handle;
684
if( ! handle.isEmpty() && ! contactList_.contains( handle ) )
687
item.handle = handle;
688
item.guId = handle + "-guid";
689
item.friendlyName = handle + "'s Friendly Name";
690
item.groupIds = QStringList();
691
item.lists = 3; // Allowed + Friend
693
contactList_[ handle ] = item;
696
// Contact adding or list change, reply as is
697
emit dataReceived( command, payloadData );
699
else if( command[0] == "REM" )
703
emit dataReceived( command, payloadData );
705
else if( command[0] == "ADG" )
707
// Group adding, append the group ID
709
QString id( replyCommand.last().toLower() );
710
id.replace( QRegExp( "[^a-z0-9]" ), "-" );
711
id.append( "-" + QString::number( rand() ) );
713
groupList_[ id ] = QUrl::fromPercentEncoding( replyCommand.last().toUtf8() );
715
replyCommand = command;
717
emit dataReceived( replyCommand, payloadData );
719
else if( command[0] == "REG" )
722
groupList_[ command[2] ] = QUrl::fromPercentEncoding( command[3].toUtf8() );
724
emit dataReceived( command, payloadData );
726
else if( command[0] == "RMG" )
729
groupList_.remove( command[2] );
730
emit dataReceived( command, payloadData );
734
else if( command[0] == "WTF" )
736
// Template for other commands :)
737
replyCommand << QString();
738
emit dataReceived( replyCommand, payloadData );
742
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
743
kDebug() << "Unknown notification message:" << command;
746
disconnectFromServer();
747
emit error( "Connection closed by peer", ERROR_DROP );
756
// Respond to a command when acting as a Switchboard Server
757
bool MsnSocketNull::sendSwitchboardServerResponse( const QStringList& command )
759
QStringList replyCommand;
760
QByteArray payloadData;
762
// Set the command for the reply to the name and ack id, so we can fill it with some other stuff
763
for( int i = 0; i < 2; i++ )
765
if( command.size() <= i )
769
replyCommand.append( command[ i ] );
772
// Reply to the message, if it contains a known command
774
// TODO: Everything here
777
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
778
kDebug() << "Unknown switchboard message:" << command;
781
disconnectFromServer();
782
emit error( "Connection closed by peer", ERROR_DROP );
792
* @brief Set whether to send pings or not
794
* When sending pings, the application receives a periodic signal
795
* which is used to perform timed updates (like receiving display pics).
797
* @param sendPings True to send pings.
799
void MsnSocketNull::setSendPings( bool sendPings )
801
sendPings_ = sendPings;
807
* @brief Write data to the gateway without any conversion
809
* This method creates a new request and queues it to be sent when possible.
810
* See sendNextRequest() for more info on sending.
812
* @param data Contents of the message which will be sent
813
* @return -1 on error, or else always the exact size of the sent data.
815
qint64 MsnSocketNull::writeBinaryData( const QByteArray &data )
819
kWarning() << "Attempted to write data to a disconnected interface, aborting.";
823
#ifdef KMESSDEBUG_CONNECTION_SOCKET_NULL
824
kDebug() << "Appending message:" << data;
825
kDebug() << "To current list:" << messageQueue_;
828
// Reactivate the timer if it's been stopped due to an empty queue
829
if( ! responseTimer_.isActive() )
834
messageQueue_.append( data.trimmed() );
842
* @brief Write a string to the gateway
844
* This is a convenience method for writeBinaryData, which can be used to directly send a QString.
846
* @param data The message which will be sent
847
* @return -1 on error, or else always the exact size of the sent data.
849
qint64 MsnSocketNull::writeData( const QString &data )
851
return writeBinaryData( data.toUtf8() );
856
#include "msnsocketnull.moc"