/*
msnp2p.cpp - msn p2p protocol
Copyright (c) 2003-2004 by Olivier Goffart <ogoffart@tiscalinet.be>
*************************************************************************
* *
* 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 "msnp2p.h"
#include <stdlib.h>
// qt
#include <qregexp.h>
#include <qfile.h>
#include <qtextcodec.h>
// kde
#include <kdebug.h>
#include <kmdcodec.h>
#include <ktempfile.h>
#include <krun.h>
#include <klocale.h>
#include <kglobal.h>
#include <kdeversion.h>
#include <kstandarddirs.h>
//kopete
#include <kopetemessagemanager.h> // { Just for getting the contact
#include <kopeteaccount.h> // {
#include <kopetetransfermanager.h>
MSNP2P::MSNP2P( QObject *parent , const char * name )
: QObject( parent , name )
{
m_file=0l;
m_Sfile=0L;
m_Rfile=0L;
m_msgIdentifier=0;
m_sessionId=0;
m_totalDataSize=0;
m_offset=0;
m_kopeteTransfer=0L;
}
MSNP2P::~MSNP2P()
{
if(m_file)
delete m_file;
else
delete m_Rfile;
delete m_Sfile;
}
void MSNP2P::slotReadMessage( const QByteArray &msg )
{
QString messageHeader=QCString(msg.data() , (msg.find('\0')==-1) ? msg.size() : msg.find('\0') );
QRegExp rx("Content-Type: ([A-Za-z0-9$!*/\\-]*)");
rx.search( messageHeader );
QString type=rx.cap(1);
if( type== "application/x-msnmsgrp2p" )
{
//Get the starting position of the 48-bytes bunary header
unsigned int startBinHeader=0;
bool justCR=false;
while(startBinHeader < msg.size()-2)
{
if( msg.data()[startBinHeader]=='\r')
startBinHeader++;
if( msg.data()[startBinHeader]=='\n' )
{
if(justCR) break;
else justCR=true;
}
else justCR=false;
startBinHeader++;
}
startBinHeader++;
if(!justCR || startBinHeader+48 > msg.size())
{ //no binary header, or not long enough
if(m_kopeteTransfer)
{
m_kopeteTransfer->slotError( KIO::ERR_INTERNAL , i18n("Malformed packet received") );
m_kopeteTransfer=0L;
}
abortCurrentTransfer();
return;
}
//Read some interesting field from the binary header
unsigned int dataMessageSize=(int)(unsigned char)(msg.data()[startBinHeader+24]) + (int)((unsigned char)msg.data()[startBinHeader+25])*256;
unsigned int totalSize=(int)(unsigned char)(msg.data()[startBinHeader+16]) + (int)((unsigned char)msg.data()[startBinHeader+17])*256 + (int)((unsigned char)msg.data()[startBinHeader+18])*256*256 + (int)((unsigned char)msg.data()[startBinHeader+19])*256*256*256;
unsigned int dataOffset=(int)(unsigned char)(msg.data()[startBinHeader+8]) + (int)((unsigned char)msg.data()[startBinHeader+9])*256 + (int)((unsigned char)msg.data()[startBinHeader+10])*256*256 + (int)((unsigned char)msg.data()[startBinHeader+11])*256*256*256;
if(dataMessageSize==0)
{
kdDebug(14140) << "MSNP2P::slotReadMessage: I do not care, it's a ACK - flag= " << (int)(unsigned char)(msg.data()[startBinHeader+28]) << endl;
return;
}
if(msg.size() < startBinHeader+48+dataMessageSize)
{
//the message's size is shorter than the announced size
if(m_kopeteTransfer)
{
m_kopeteTransfer->slotError( KIO::ERR_INTERNAL , i18n("Malformed packet received") );
m_kopeteTransfer=0L;
}
abortCurrentTransfer();
return;
}
QString dataMessage=QCString((msg.data()+startBinHeader+48) , dataMessageSize);
if(m_msgHandle.isEmpty())
{ //if these addresses were not previously set, get it, they should be provided in the first message at last.
QRegExp rx("To: <msnmsgr:([^>]*)>");
if( rx.search( dataMessage ) != -1 )
m_myHandle=rx.cap(1);
rx=QRegExp("From: <msnmsgr:([^>]*)>");
if( rx.search( dataMessage ) != -1 )
m_msgHandle=rx.cap(1);
}
//Send the ack if needed
if(dataOffset+dataMessageSize>=totalSize)
sendP2PAck( (msg.data()+startBinHeader) );
if(m_Rfile) //we are already downloading something to this file
{
//m_file->file()->writeBlock( (msg.data()+startBinHeader+48) , dataMessageSize );
m_Rfile->writeBlock( (msg.data()+startBinHeader+48) , dataMessageSize );
if(m_kopeteTransfer)
m_kopeteTransfer->slotProcessed( dataOffset+dataMessageSize );
if(dataOffset+dataMessageSize >= totalSize) //the file is complete
{
if(m_file)
{
m_file->close();
emit fileReceived(m_file , m_obj);
m_file=0;
m_Rfile=0L;
}
else
{
if(m_kopeteTransfer) m_kopeteTransfer->slotComplete();
m_Rfile->close();
delete m_Rfile;
m_Rfile=0L;
}
/*
delete m_file;*/
//send the bye message
makeMSNSLPMessage(BYE, QString::null);
//deleteLater();
}
}
else
{
kdDebug(14141) << "MSNP2P::slotReadMessage: dataMessage: " << dataMessage << endl;
if(msg.data()[startBinHeader+48] == '\0' )
{ //This can be only the data preparaion message. prepare to download
m_file=new KTempFile( locateLocal( "tmp", "msnpicture-" ), ".png" );
m_file->setAutoDelete(true);
m_Rfile=m_file->file();
}
else if (dataMessage.contains("INVITE"))
{
//Parse the message to get some info for replying
QRegExp rx(";branch=\\{([0-9A-F\\-]*)\\}\r\n");
rx.search( dataMessage );
m_branch=rx.cap(1);
rx=QRegExp("Call-ID: *)\}rn]\{([0-9A-F\-]*)\}rn");
rx.search( dataMessage );
m_CallID=rx.cap(1);
if(!m_kopeteTransfer) // it's the first INVITE message
{
rx=QRegExp("SessionID: ([0-9]*)\r\n");
rx.search( dataMessage );
m_sessionId=rx.cap(1).toUInt();
rx=QRegExp("AppID: ([0-9]*)\r\n");
rx.search( dataMessage );
unsigned long int AppID=rx.cap(1).toUInt();
if(AppID==1) //the peer ask for a msn picture, or emoticon download.
{ // currently, we always send the display picture
//Send the OK message.
QString content="SessionID: " + QString::number( m_sessionId ) + "\r\n\r\n" ;
makeMSNSLPMessage( OK, content );
//prepare to send the file
m_Sfile = new QFile( locateLocal( "appdata", "msnpicture-"+ m_myHandle.lower().replace(QRegExp("[./~]"),"-") +".png" ) );
if(!m_Sfile->open(IO_ReadOnly)) {/* TODO: error?*/}
//send the data preparation message
QByteArray initM(4);
initM.fill('\0');
sendP2PMessage(initM);
m_totalDataSize= m_Sfile->size();
QTimer::singleShot( 10, this, SLOT(slotSendData()) ); //Go for upload
}
else if(AppID==2) //the peer want to transfer a file.
{
//extract the context from the invitation contents
rx=QRegExp("Context: ([0-9a-zA-Z+/=]*)");
rx.search( dataMessage );
QString context=rx.cap(1);
//Context is a base64 encoded dump of the internal memory of MSN messanger.
// the filesize is contained in the bytes 8..11
// the filename is from the byte 19
// I don't know what other fields are.
QByteArray binaryContext;
KCodecs::base64Decode( context.utf8() , binaryContext );
if(binaryContext.size() < 21 ) //security, (don't crash)
return; //TODO: handle error
//the filename is conteined in the context from the 19st char to the end. (in utf-16)
QTextCodec *codec = QTextCodec::codecForName("ISO-10646-UCS-2");
if(!codec)
return; //abort();
QString filename = codec->toUnicode(binaryContext.data()+19 , binaryContext.size()-19-16) ;
filename=filename.left(filename.find(QChar('\0')));
//the size is placed in the context in the bytes 8..12 (source: the amsn code)
unsigned long int filesize= (unsigned char)(binaryContext[8]) + (unsigned char)(binaryContext[9]) *256 + (unsigned char)(binaryContext[10]) *65536 + (unsigned char)(binaryContext[11]) *16777216 ;
//ugly hack to get the KopeteContact.
KopeteContact *c=0L;
if(parent())
{
KopeteMessageManager *kmm=dynamic_cast<KopeteMessageManager*>(parent()->parent());
if(kmm)
c=kmm->account()->contacts()[m_msgHandle];
}
disconnect(KopeteTransferManager::transferManager(), 0L , this, 0L);
connect(KopeteTransferManager::transferManager() , SIGNAL(accepted(KopeteTransfer*, const QString& )) ,
this, SLOT(slotTransferAccepted(KopeteTransfer*, const QString& )));
connect(KopeteTransferManager::transferManager() , SIGNAL(refused( const KopeteFileTransferInfo & ) ),
this, SLOT( slotFileTransferRefused( const KopeteFileTransferInfo & ) ) );
//show a dialog to ask the transfer.
KopeteTransferManager::transferManager()->askIncomingTransfer(c , filename , filesize, QString::null, QString::number(m_sessionId)+":"+m_branch+":"+m_CallID);
}
else //unknwon AppID
{
makeMSNSLPMessage( ERROR, QString::null );
}
} // end of if(m_kopeteTranfer)
else // we are nogitiating a complex invitaiton ( a file transfer)
{ // it's the second INVITE message
//dirrect connection is not yet implemented, use the connection via MSNP2P
QString content="Bridge: TCPv1\r\n"
"Listening: false\r\n"
"Nonce: {00000000-0000-0000-0000-000000000000}\r\n\r\n";
makeMSNSLPMessage(OK, content);
m_Rfile=new QFile( m_kopeteTransfer->destinationURL().path() );
if(!m_Rfile->open(IO_WriteOnly))
{
if(m_kopeteTransfer)
{
//TODO: handle the QFILE error
m_kopeteTransfer->slotError( KIO::ERR_CANNOT_OPEN_FOR_WRITING , i18n("Cannot open file for writing") );
m_kopeteTransfer=0L;
return;
}
abortCurrentTransfer();
}
}
}
else if (dataMessage.contains("BYE"))
{
//deleteLater();
}
}
}
else
{
kdDebug(14140) << "MSNSwitchBoardSocket::slotReadMessage: Unknown type '" << type << endl;
}
}
void MSNP2P::requestDisplayPicture( const QString &myHandle, const QString &msgHandle, QString msnObject)
{
//reset some field
/* m_file=0l;
m_Sfile=0L;
m_msgIdentifier=0;
m_sessionId=0;
m_totalDataSize=0;
m_offset=0;*/
m_sessionId=0;
m_myHandle=myHandle;
m_msgHandle=msgHandle;
m_obj=msnObject;
msnObject=QString::fromUtf8(KCodecs::base64Encode( msnObject.utf8() ));
msnObject.replace("=" , QString::null ) ;
unsigned long int sessID=rand()%0xFFFFFF00+4;
m_branch= QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number(rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16)+QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16)+QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16);
m_CallID= QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16) + "-" + QString::number(rand()%0xAAFF+0x1111, 16) + "-" + QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16)+QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16)+QString::number((unsigned long int)rand()%0xAAFF+0x1111, 16); ;
QString content="EUF-GUID: {A4268EEC-FEC5-49E5-95C3-F126696BDBF6}\r\n"
"SessionID: "+ QString::number(sessID)+"\r\n"
"AppID: 1\r\n"
"Context: " + msnObject +"\r\n\r\n";
makeMSNSLPMessage( INVITE , content );
}
void MSNP2P::makeMSNSLPMessage( MessageType type, QString content )
{
QString contentType= QString( "application/x-msnmsgr-sessionreqbody" );
QString method;
QString CSeq;
switch(type)
{
case INVITE:
method="INVITE MSNMSGR:"+ m_msgHandle + " MSNSLP/1.0";
CSeq="0";
break;
case DECLINE:
method="MSNSLP/1.0 603 DECLINE";
CSeq="1";
break;
case ERROR:
contentType="null";
method="MSNSLP/1.0 500 Internal Error";
CSeq="1";
break;
case OK:
if(m_kopeteTransfer)
contentType="application/x-msnmsgr-transreqbody";
method="MSNSLP/1.0 200 OK";
CSeq="1";
break;
case BYE:
contentType="application/x-msnmsgr-sessionclosebody";
method="BYE MSNMSGR:"+m_msgHandle+" MSNSLP/1.0";
CSeq="0";
break;
}
QCString dataMessage= QString(
method + "\r\n"
"To: <msnmsgr:"+m_msgHandle+">\r\n"
"From: <msnmsgr:"+m_myHandle+">\r\n"
"Via: MSNSLP/1.0/TLP ;branch={"+m_branch.upper()+"}\r\n"
"CSeq: "+ CSeq +"\r\n"
"Call-ID: {"+m_CallID.upper()+"}\r\n"
"Max-Forwards: 0\r\n"
"Content-Type: "+ contentType +"\r\n"
"Content-Length: "+ QString::number(content.length()+1)+"\r\n"
"\r\n" + content ).utf8(); //\0
//the data message must be end by \0, bye chance, QCString automaticaly appends \0 at the end of the QByteArray
kdDebug(14141) << k_funcinfo << dataMessage << endl;
sendP2PMessage(dataMessage);
}
void MSNP2P::sendP2PMessage(const QByteArray &dataMessage)
{
bool transferStarted=( m_Rfile || m_Sfile ); //we are stransfering binary is any of the file exists
QCString messageHeader=QString(
"MIME-Version: 1.0\r\n"
"Content-Type: application/x-msnmsgrp2p\r\n"
"P2P-Dest: "+ m_msgHandle +"\r\n\r\n").utf8();
QByteArray binHeader(48);
binHeader.fill('\0'); //fill with 0 for starting
if(m_msgIdentifier==0)
m_msgIdentifier=rand()%0x0FFFFFF0+4;
else if(m_offset==0)
m_msgIdentifier++;
//SessionID
unsigned long int sessionID=transferStarted ? m_sessionId : 0;
binHeader[0]=(char)(sessionID%256);
binHeader[1]=(char)((unsigned long int)(sessionID/256)%256);
binHeader[2]=(char)((unsigned long int)(sessionID/(256*256))%256);
binHeader[3]=(char)((unsigned long int)(sessionID/(256*256*256))%256);
//MessageID
binHeader[4]=(char)(m_msgIdentifier%256);
binHeader[5]=(char)((unsigned long int)(m_msgIdentifier/256)%256);
binHeader[6]=(char)((unsigned long int)(m_msgIdentifier/(256*256))%256);
binHeader[7]=(char)((unsigned long int)(m_msgIdentifier/(256*256*256))%256);
//offset
binHeader[8]=(char)(m_offset%256);
binHeader[9]=(char)((unsigned long int)(m_offset/256)%256);
binHeader[10]=(char)((unsigned long int)(m_offset/(256*256))%256);
binHeader[11]=(char)((unsigned long int)(m_offset/(256*256*256))%256);
unsigned int size=dataMessage.size();
if(m_totalDataSize) //it's a splitted message
{
binHeader[16]=(char)(m_totalDataSize%256);
binHeader[17]=(char)((unsigned long int)(m_totalDataSize/256)%256);
binHeader[18]=(char)((unsigned long int)(m_totalDataSize/(256*256))%256);
binHeader[19]=(char)((unsigned long int)(m_totalDataSize/(256*256*256))%256);
//update offset
m_offset+=size;
if(m_offset>=m_totalDataSize)
{ //message completely sent, reset values
m_offset=0;
m_totalDataSize=0;
}
}
else //not a splitted message, the total size is the current size
{
binHeader[16]=(char)size%256;
binHeader[17]=(int)size/256;
}
//message size
binHeader[24]=(char)size%256;
binHeader[25]=(int)size/256;
//Ack sessionID
binHeader[32]=(char)(rand()%256);
binHeader[33]=(char)(rand()%256);
binHeader[34]=(char)(rand()%256);
binHeader[35]=(char)(rand()%256);
/*binHeader[32]=0xDE;
binHeader[33]=0xC7;
binHeader[34]=0x07;
binHeader[35]=0x14;*/
//merge all in a unique message
QByteArray data( messageHeader.length() + binHeader.size() + dataMessage.size() + 4 );
for(unsigned int f=0; f< messageHeader.length() ; f++)
data[f]=messageHeader[f];
for(unsigned int f=0; f< binHeader.size() ; f++)
data[messageHeader.length()+f]=binHeader[f];
for(unsigned int f=0; f< dataMessage.size() ; f++)
data[messageHeader.length()+binHeader.size()+f]=dataMessage[f];
for(unsigned int f=0; f< 4 ; f++) //footer
data[messageHeader.length()+binHeader.size()+dataMessage.size()+f]='\0';
if(transferStarted)
{ //then, the footer ends with \1 (only for display pictures)
data[messageHeader.length()+binHeader.size() + dataMessage.size() +3 ]='\1';
}
//send the message
emit sendCommand("MSG", "D" , true , data , true );
}
void MSNP2P::sendP2PAck( const char* originalHeader )
{
QCString messageHeader=QString(
"MIME-Version: 1.0\r\n"
"Content-Type: application/x-msnmsgrp2p\r\n"
"P2P-Dest: "+ m_msgHandle +"\r\n\r\n").utf8();
QByteArray binHeader(48);
binHeader.fill('\0'); //fill with 0 for starting
//sessionID
binHeader[0]=originalHeader[0];
binHeader[1]=originalHeader[1];
binHeader[2]=originalHeader[2];
binHeader[3]=originalHeader[3];
//MessageID
bool a=false;
if(m_msgIdentifier==0)
{
m_msgIdentifier=rand()%0xFFFFFE00+10;
a=true;
}
else
m_msgIdentifier++;
binHeader[4]=(char)(m_msgIdentifier%256);
binHeader[5]=(char)((unsigned long int)(m_msgIdentifier/256)%256);
binHeader[6]=(char)((unsigned long int)(m_msgIdentifier/(256*256))%256);
binHeader[7]=(char)((unsigned long int)(m_msgIdentifier/(256*256*256))%256);
if(a)
m_msgIdentifier-=4;
//total size
binHeader[16]=originalHeader[16];
binHeader[17]=originalHeader[17];
binHeader[18]=originalHeader[18];
binHeader[19]=originalHeader[19];
binHeader[20]=originalHeader[20];
binHeader[21]=originalHeader[21];
binHeader[22]=originalHeader[22];
binHeader[23]=originalHeader[23];
//flag
binHeader[28]=(char)0x02;
//ack sessionID
binHeader[32]=originalHeader[4];
binHeader[33]=originalHeader[5];
binHeader[34]=originalHeader[6];
binHeader[35]=originalHeader[7];
//ack unique id
binHeader[36]=originalHeader[32];
binHeader[37]=originalHeader[33];
binHeader[38]=originalHeader[34];
binHeader[39]=originalHeader[35];
//ack data size
binHeader[40]=originalHeader[16];
binHeader[41]=originalHeader[17];
binHeader[42]=originalHeader[18];
binHeader[43]=originalHeader[19];
binHeader[44]=originalHeader[20];
binHeader[45]=originalHeader[21];
binHeader[46]=originalHeader[22];
binHeader[47]=originalHeader[23];
QByteArray data( messageHeader.length() + binHeader.size() + 4 );
for(unsigned int f=0; f< messageHeader.length() ; f++)
data[f]=messageHeader[f];
for(unsigned int f=0; f< binHeader.size() ; f++) //if binHeader is a QCString, it ends with \0 , which is ok
data[messageHeader.length()+f]=binHeader[f];
for(unsigned int f=0; f< 4 ; f++)
data[messageHeader.length()+binHeader.size() +f ]='\0';
emit sendCommand("MSG", "D" , true , data , true );
}
void MSNP2P::slotSendData()
{
if(!m_Sfile)
return;
char data[1200];
int bytesRead = m_Sfile->readBlock( data,1200 );
QByteArray dataBA(bytesRead);
for ( int f = 0; f < bytesRead; f++ )
dataBA[f] = data[f];
// kdDebug(14140) << "MSNP2P::slotSendData: offset=" << m_offset << " size=" << bytesRead << " totalSize=" << m_totalDataSize << " sent=" << m_offset+bytesRead << endl;
sendP2PMessage(dataBA);
if( m_totalDataSize == 0 ) //this has been reseted bacause the file is completely send
{
// kd(14140) << "MSNP2P::slotSendData: FINISHED! wait for the BYE message" << endl;
delete m_Sfile;
m_Sfile=0L;
m_sessionId=0;
}
else
QTimer::singleShot( 10, this, SLOT(slotSendData()) );
}
void MSNP2P::slotTransferAccepted(KopeteTransfer* transfer, const QString& /*filename*/ )
{
QStringList internalId=QStringList::split(":" , transfer->info().internalId() );
if(internalId[0].toUInt() == m_sessionId )
{
QObject::connect(transfer , SIGNAL(transferCanceled()), this, SLOT(abortCurrentTransfer()));
QObject::connect(transfer, SIGNAL(destroyed()) , this , SLOT(slotKopeteTransferDestroyed()));
m_branch=internalId[1];
QString callid=internalId[2];
QString content="SessionID: " + QString::number( m_sessionId ) +"\r\n\r\n";
makeMSNSLPMessage( OK, content);
m_kopeteTransfer=transfer;
}
}
void MSNP2P::slotFileTransferRefused( const KopeteFileTransferInfo &info )
{
QStringList internalId=QStringList::split(":" , info.internalId() );
kdDebug(14140) << k_funcinfo << internalId << endl;
if(internalId[0].toUInt() == m_sessionId )
{
m_branch=internalId[1];
m_CallID=internalId[2];
QString content="SessionID: " + QString::number( m_sessionId ) +"\r\n\r\n";
makeMSNSLPMessage( DECLINE , content );
}
}
void MSNP2P::abortCurrentTransfer()
{
if(m_kopeteTransfer)
{
delete m_Rfile;
m_Rfile=0L;
//this need to be reseted before sending the BYE message.
m_totalDataSize=0;
m_offset=0;
//FIXME: i'm not sure it's like that i should abort the transfer.
makeMSNSLPMessage(BYE, "\r\n\r\n" );
m_sessionId=0;
m_msgIdentifier=0;
}
}
void MSNP2P::slotKopeteTransferDestroyed()
{
m_kopeteTransfer=0L;
kdDebug(14140) << k_funcinfo << endl;
}
#include "msnp2p.moc"