Logo Search packages:      
Sourcecode: kmess version File versions

webcamtransferp2p.cpp

/***************************************************************************
                          webcamtransferp2p.cpp -  description
                             -------------------
    begin                : Sun Aug 7 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 "webcamtransferp2p.h"

#include "../../kmessdebug.h"
#include "../../utils/kmessshared.h"
#include "../../utils/xmlfunctions.h"
#include "../extra/directconnectionpool.h"
#include "../extra/msnwebcamconnection.h"
#include "../mimemessage.h"

#include <KLocale>
#include <stdlib.h>   // rand()

//solid specific includes
#include <solid/devicenotifier.h>
#include <solid/device.h>
#include <solid/deviceinterface.h>
#include <solid/processor.h>

#include <QDomDocument>


//
// I'd like to give a special thanks to the Kopete developers here,
// without them writing this code would have been a lot harder.
// Especially since all webcam online docs seam to be gone.
//




/**
 * Constructor
 *
 * @param  applicationList  The shared sources for the contact.
 */
00053 WebcamTransferP2P::WebcamTransferP2P(ApplicationList *applicationList)
: P2PApplication(applicationList)
, isUserSender_(false)
, webcamConnection_(0)
, webcamConnectionPool_(0)
{
  setApplicationType( ChatMessage::TYPE_APPLICATION_WEBCAM );
  buffer_.open( QBuffer::ReadWrite );

  // Initialize webcam connection pool, configure it to wait for a connection to authorize first.
  // This is needed because there are connection attempts at port 80. If both clients are at
  // the same network, you'll likely connect to the home router administration page too.
  webcamConnectionPool_ = new DirectConnectionPool( true );

  connect(webcamConnectionPool_, SIGNAL(      activeConnectionAuthorized()  ),    // The connection was authorized
          this,                    SLOT(  slotActiveConnectionAuthorized()  ));
  connect(webcamConnectionPool_, SIGNAL(          activeConnectionClosed()  ),    // The active connection was closed
          this,                    SLOT(      slotActiveConnectionClosed()  ));
  connect(webcamConnectionPool_, SIGNAL(           connectionEstablished()  ),    // The connection was established
          this,                    SLOT(       slotConnectionEstablished()  ));
  connect(webcamConnectionPool_, SIGNAL(            allConnectionsFailed()  ),    // All connection attempts failed.
          this,                    SLOT(        slotAllConnectionsFailed()  ));
}



/**
 * Destructor
 */
00082 WebcamTransferP2P::~WebcamTransferP2P()
{
  delete webcamConnectionPool_;
  webcamConnection_ = 0;  // will be deleted by the direct connection pool.
}



/**
 * Step one of a contact-started chat: the contact invites the user
 *
 * @param  message  The invitation message
 */
00095 void WebcamTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage & message)
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif

#if ENABLE_WEBCAM

  // Read the values from the message
  uint    appID  = message.getValue("AppID").toUInt();
  QString eufGuid( message.getValue("EUF-GUID") );
  QString context( message.getValue("Context") );

  if(appID != 4)
  {
    kWarning() << "Received unexpected AppID: " << appID << ".";

    // Wouldn't know what to do if the AppID is not 2, so send an 500 Internal Error back.
    showEventMessage( i18n("The webcam invitation was cancelled. Bad data was received."), ChatMessage::CONTENT_APP_CANCELED, true );
    sendCancelMessage( CANCEL_ABORT );
    return;
  }

  // Contact can request a webcam from us, or share it's own webcam.
  isUserSender_ = ( eufGuid == getPullAppId() );

  // Parse the context, it contains another GUID
  QByteArray decodedContext = QByteArray::fromBase64( context.toLatin1() );
  webcamGuid_ = P2PMessage::extractUtf16String( decodedContext.data(), 0, decodedContext.size() );

  // Display the accept message.
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug() << "Webcam invitation received, GUID=" << webcamGuid_ << endl;
  kDebug() << "waiting for user to accept..." << endl;
#endif

  if( isUserSender_ )
  {
    // FIXME: better message.
    offerAcceptOrReject( i18n("You are asked to show your webcam.") );
  }
  else
  {
    offerAcceptOrReject( i18n("You are invited to view this person's webcam.") );
  }

#else
  Q_UNUSED( message );

  // KMess does not support the webapplication invitations yet,
  // this class is currently a STUB to produce a proper error message
  showSystemMessage( i18n( "The contact is inviting you for '%1', but this is not implemented yet.",
                           i18n("webcam") ),
                     ChatMessage::CONTENT_SYSTEM_NOTICE, true );

  // Tell the contact we don't support this.
  sendCancelMessage(CANCEL_NOT_INSTALLED);
#endif
}



/**
 * Step two of a contact-started chat: the user accepts
 */
00160 void WebcamTransferP2P::contactStarted2_UserAccepts()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug() << "sending accept message";
#endif

  // Create the message
  MimeMessage message;
  message.addField( "SessionID", QString::number( getInvitationSessionID() ) );

  // Send the message
  sendSlpOkMessage(message);
  setWaitingState( P2P_WAIT_FOR_WEBCAM_DATA, 120000 );  // 120 seconds
}




/**
 * Step three of a contact-started chat: the contact confirms the accept
 */
00181 void WebcamTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage& /*message*/)
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif

  if( isUserSender_ )
  {
    // First syn message to send.
    sendSipMessage( "syn", 0x06, 0x91, 0x7c, P2P_MSG_WEBCAM_SYN );
  }
  else
  {

  }

  // The invitation continues with the gotData() call.
}



/**
 * Create the producer or viewer XML tag.
 */
00205 QString WebcamTransferP2P::createSipXml( uint recipientId, uint sessionId )
{
  // Get the network info
  const QString &externalIp = getExternalIp();
  const QString &localIp    = getLocalIp();

  // Get a reserved server port.
  MsnWebcamConnection *webcamConnection = new MsnWebcamConnection( getContactHandle(), recipientId, sessionId );
  int tcpPort = webcamConnectionPool_->addServerConnection( webcamConnection );

  // Get the CPU speed using Solid.
  int cpuSpeed = 0;
  QList<Solid::Device> cpuList = Solid::Device::listFromType( Solid::DeviceInterface::Processor, QString() );

  // Take the first processor
  if( ! cpuList.isEmpty() )
  {
    Solid::Device device = cpuList[0];
    Solid::Processor *processor = device.as<Solid::Processor>();
    if( processor != 0 )
    {
      cpuSpeed = processor->maxSpeed();

#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
      kDebug() << "Detected CPU speed is " << cpuSpeed;
#endif
    }
  }

  if( cpuSpeed == 0 )
  {
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
    kDebug() << "CPU speed not detected";
#endif
    cpuSpeed = 1000;
  }


  // Build XMl tags for the IP addresses
  QStringList ipAddresses;
  ipAddresses << externalIp;
  ipAddresses << localIp;

  QString tcpIpTags;

  int i = 1;
  foreach( const QString &ipAddress, ipAddresses )
  {
    tcpIpTags += "<tcpipaddress" + QString::number( i ) + ">" + ipAddress + "</tcpipaddress" + QString::number( i ) + ">";
    i++;
  }


  // Determine the root node.
  QString rootNode( ( isUserSender_ ? "producer" : "viewer" ) );


  // Build full XML
  return "<" + rootNode + ">"
           "<version>2.0</version>"
           "<rid>"     + QString::number( recipientId )     + "</rid>"
//           "<udprid>"  + QString::number( recipientId + 1 ) + "</udprid>"
           "<session>" + QString::number( sessionId )       + "</session>"
           "<ctypes>0</ctypes>"
           "<cpu>" + QString::number( cpuSpeed ) + "</cpu>"
           "<tcp>"
             "<tcpport>"         + QString::number( tcpPort ) + "</tcpport>"       // "\t\t\t\t\t\t\t\t  "
             "<tcplocalport>"    + QString::number( tcpPort ) + "</tcplocalport>"  // "\t\t\t\t\t\t\t\t  "
             "<tcpexternalport>" + QString::number( tcpPort ) + "</tcpexternalport>" + tcpIpTags +
           "</tcp>"
//           "<udp>"
//             "<udplocalport>7786</udplocalport>"
//             "<udpexternalport>31863</udpexternalport>"
//             "<udpexternalip>" + externalIp + "</udpexternalip>"
//             "<a1_port>31859</a1_port>"
//             "<b1_port>31860</b1_port>"
//             "<b2_port>31861</b2_port>"
//             "<b3_port>31862</b3_port>"
//             "<symmetricallocation>1</symmetricallocation>"
//             "<symmetricallocationincrement>1</symmetricallocationincrement>"
//             "<udpversion>1</udpversion>"
//             "<udpinternalipaddress1>" + internalIp + "</udpinternalipaddress1>"
//           "</udp>"
           "<codec></codec>"
           "<channelmode>1</channelmode>"
         "</" + rootNode + ">\r\n\r\n";
}



/**
 * Return the application's GUID for "video conference" invitations.
 */
00298 QString WebcamTransferP2P::getConferenceAppId()
{
  return "{4BD96FC0-AB17-4425-A14A-439185962DC8}";   // FIXME: identical to getPushAppId()  ??
}



/**
 * Return the application's GUID for "user push" invitations.
 */
00308 QString WebcamTransferP2P::getPushAppId()
{
  return "{4BD96FC0-AB17-4425-A14A-439185962DC8}";
}



/**
 * Return the application's GUID for "contact pull" invitations.
 */
00318 QString WebcamTransferP2P::getPullAppId()
{
  return "{1C9AA97E-9C05-4583-A3BD-908A196F1E92}";
}



/**
 * Called when SIP data is received.
 * This is the second stage of the webcam invitation.
 *
 * The webcam setup message can something like:
 * - "syn"
 * - "ack"
 * - "<producer>...."
 * - "<viewer>...."
 *
 * @param  message  P2P message with the data.
 */
00337 void WebcamTransferP2P::gotData(const P2PMessage &message)
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug() << "Buffering received webcam data message...";
#endif

  // Keep tracking messages. Most are just one packet,
  // but the <producer> is split over multiple packets.
  writeP2PDataToFile( message, &buffer_ );
}



/**
 * Called when all data is received.
 *
 * This method overwrites the behavior of the P2PApplication class.
 * After all data is received, a BYE won't be sent.
 * Instead, another data message is sent back.
 */
00357 void WebcamTransferP2P::gotDataComplete(const P2PMessage &lastMessage)
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug() << "Received complete webcam data message.";
#endif
  Q_UNUSED( lastMessage );

  // NOTE: The ack is already sent, no need to handle that.

  // Get the complete message
  buffer_.reset();
  QByteArray message = buffer_.readAll();
  QString payload( P2PMessage::extractUtf16String( message.data(), 10, message.size() - 10 ) );

  //bool contactStarted = ( ! isUserStartedApp() );

#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  // FIXME: display hex data, extract part from formatRawData() to kmessdebug.cpp
  kDebug() << "received setup data:" << payload;
#endif

  // Find out what to do.
  if( payload == "syn" )
  {
    if( isUserSender_ )
    {
      // Wait for last ack.
      //sendSipMessage( "syn", 0x17, 0x2a, 0x01, P2P_MSG_WEBCAM_SYN );
    }
    else
    {
      // Received first syn, send syn back.
      sendSipMessage( "syn", 0x06, 0x91, 0x7c, P2P_MSG_WEBCAM_SYN );
      sendSipMessage( "ack", 0xfe, 0x13, 0x07, P2P_MSG_WEBCAM_ACK );
      // ...and wait for syn to be accepted, then send ack as well.

      //sendSipMessage( "ack", 0xea, 0x00, 0x00, P2P_MSG_WEBCAM_ACK );
    }
  }
  else if( payload == "ack" )
  {
    if( isUserSender_ )
    {
      // Got final ack, send the <producer> message
      uint sessionId   = rand() % 1000 + 5000;
      uint recipientId = rand() % 100 + 50;
      QString producerXml( createSipXml( recipientId, sessionId ) );
      sendSipMessage( producerXml, 0, 0, 0, P2P_MSG_WEBCAM_SETUP );
    }
    else
    {
      // Ready to receive the <producer> xml tag!
    }
  }
  else if( payload.startsWith("<") )
  {
    QDomDocument xml;
    if( ! xml.setContent(payload) )
    {
      kWarning() << "Could not parse webcam negotiation status message (invalid XML, contact=" << getContactHandle() << ")!";
      kWarning() << "The invalid XML was:" << payload;
    }
    
    // XML.
    QDomElement rootNode = xml.documentElement();
    QString rootTagName( rootNode.tagName() );
    if( rootTagName == "producer" || rootTagName == "viewer" )
    {
      // <producer> tag
      // Example:
      //
      // <producer>
      //   <version>2.0</version>
      //   <rid>101</rid>
      //   <udprid>102</udprid>
      //   <session>7971</session>
      //   <ctypes>0</ctypes>
      //   <cpu>1830</cpu>
      //   <tcp>
      //     <tcpport>80</tcpport>
      //     <tcplocalport>80</tcplocalport>
      //     <tcpexternalport>0</tcpexternalport>
      //     <tcpipaddress1>192.168.1.82</tcpipaddress1>
      //     <tcpipaddress2>210.222.333.444</tcpipaddress2>
      //   </tcp>
      //   <udp>
      //     <udplocalport>34799</udplocalport>
      //     <udpexternalport>51294</udpexternalport>
      //     <udpexternalip>210.222.333.444</udpexternalip>
      //     <a1_port>51290</a1_port>
      //     <b1_port>51291</b1_port>
      //     <b2_port>51292</b2_port>
      //     <b3_port>51293</b3_port>
      //     <symmetricallocation>1</symmetricallocation>
      //     <symmetricallocationincrement>1</symmetricallocationincrement>
      //     <udpversion>1</udpversion>
      //     <udpinternalipaddress1>192.168.1.82</udpinternalipaddress1>
      //   </udp>
      //   <codec></codec>
      //   <channelmode>1</channelmode>
      // </producer>

      // Get the authentication data for the connection
      uint recipientId  = XmlFunctions::getNodeValue( rootNode, "rid" ).toUInt();
      uint sessionId    = XmlFunctions::getNodeValue( rootNode, "session" ).toUInt();

      // Get all ports we can attempt connect to.
      QDomNode tcpNode = rootNode.namedItem("tcp");
      QList<uint> tcpPorts;
      tcpPorts.append( XmlFunctions::getNodeValue( tcpNode, "tcpport" ).toUInt() );
      tcpPorts.append( XmlFunctions::getNodeValue( tcpNode, "tcplocalport" ).toUInt() );
      tcpPorts.append( XmlFunctions::getNodeValue( tcpNode, "tcpexternalport" ).toUInt() );
      
      // Remove all invalid or duplicate ports.
      tcpPorts.removeAll(0);
      qSort( tcpPorts );
      for( int i = tcpPorts.size() - 1; i >= 1; i-- )
      {
        if( tcpPorts[ i ] == tcpPorts[ i - 1 ] )
        {
          tcpPorts.removeAt( i );
        }
      }


      // Get all possible IP addresses to connect to.
      QStringList tcpIpAddresses;
      for( int i = 1; i < 10; i++ )
      {
        QDomNode tcpIpAddress = tcpNode.namedItem( "tcpipaddress" + QString::number( i ) );
        if( tcpIpAddress.isNull() )
        {
          break;
        }
      
        tcpIpAddresses << tcpIpAddress.toElement().text();
      }


#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
      kDebug() << "Received the" << rootTagName << "tag, tcp ips=" << tcpIpAddresses << " tcp ports=" << tcpPorts;
#endif

      
      // Create a connection attempt to the given IPs/ports
      foreach( const QString &tcpIpAddress, tcpIpAddresses )
      {
        foreach( uint tcpPort, tcpPorts )
        {
          MsnWebcamConnection *webcamConnection = new MsnWebcamConnection( getContactHandle(), recipientId, sessionId );
          webcamConnectionPool_->addConnection( webcamConnection, tcpIpAddress, (quint16) tcpPort );
        }
      }


      // Send the <viewer> tag back.
      if( rootTagName == "producer" )
      {
        QString viewerXml( createSipXml( recipientId, sessionId ) );
        sendSipMessage( viewerXml, 0, 0, 0, P2P_MSG_WEBCAM_SETUP );
      }
    }
    else if( rootTagName == "viewer" )
    {
      // <viewer> tag
    }
    else
    {
      sendP2PAbort();
    }
  }
  else if( payload.startsWith("ReflData:") )
  {
    // no idea..
  }
  else if( payload == "receivedViewerData" )
  {
    // nothing to do
  }
  else
  {
    // FIXME: ack is already sent, can't revert that. (see what WLM does with invalid XML).
    sendP2PAbort();
  }
}



/**
 * Called when an ACK message to an unknown message is received.
 * This class uses this feature to respond to ACK messages of the 'syn' and 'ack' webcam negotiation messages.
 */
00549 bool WebcamTransferP2P::gotUnhandledAck( const P2PMessage &ackMessage, P2PMessageType ackMessageType )
{
  Q_UNUSED( ackMessage );

  switch( ackMessageType )
  {
    case P2P_MSG_WEBCAM_SYN:
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
      kDebug() << "Received P2P ACK for the webcam 'syn' message.";
#endif

      // Our syn was received.
      if( isUserSender_ )
      {
        // Wait for syn / ack.
      }
      else
      {
        // Finally send ack back.
//        sendSipMessage( "ack", 0xfe, 0x13, 0x07, P2P_MSG_WEBCAM_ACK );
//        sendSipMessage( "ack", 0xfe, 0xc8, 0x05, P2P_MSG_WEBCAM_ACK );
      }

      return true;

    case P2P_MSG_WEBCAM_ACK:
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
      kDebug() << "Received P2P ACK for the webcam 'ack' message.";
#endif

      // Our ack was received.
      if( isUserSender_ )
      {
      }
      else
      {
      }

      return true;

    default:
      return false;
  }
}



/**
 * Send a webcam negotiation message
 */
00599 void WebcamTransferP2P::sendSipMessage( const QString &contents, quint8 flag1, quint8 flag2, quint8 flag3, P2PMessageType messageType )
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  // FIXME: display hex data, add function + hexmap to kmessdebug.cpp
  kDebug() << "sending setup data: " << contents;
#endif


  QByteArray sipMessage( 10 + contents.length() * 2 + 2, '\0' );

  // Observed:
  // WLM syn:
  //   80 06 91 7c  08 00 08 00  00 00    // binary flags
  //   73 00 79 00  6e 00 00 00           // s.y.n...
  //
  // WLM ack:
  //   80 fe b2 04  08 00 08 00  00 00    // binary flags
  //   61 00 63 00  6b 00 00 00           // a.c.k...
  //
  // WLM producer:
  //   80 06 91 7c  08 00 44 06  00 00
  //
  // WLM viewer:
  //   80 06 91 7c  08 00 12 06  00 00
  //
  // 


  sipMessage[0] = '\x80';
  sipMessage[1] = flag1;
  sipMessage[2] = flag2;
  sipMessage[3] = flag3;
  sipMessage[4] = '\x08';
  sipMessage[6] = '\x08';

  P2PMessage::insertUtf16String( sipMessage, contents, 10 );

  // Send message.
  sendP2PMessage( sipMessage, 0, P2P_TYPE_WEBCAM, messageType );
}



/**
 * The active webcam connection is authorized.
 */
00645 void WebcamTransferP2P::slotActiveConnectionAuthorized()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif
}



/**
 * The active webcam connection is closed
 */
00657 void WebcamTransferP2P::slotActiveConnectionClosed()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif

}



/**
 * All connection attempts failed.
 */
00670 void WebcamTransferP2P::slotAllConnectionsFailed()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif

}



/**
 * The active webcam connection is closed
 */
00683 void WebcamTransferP2P::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug();
#endif

}



/**
 * Step one of a user-started chat: the user invites the contact
 */
00696 void WebcamTransferP2P::userStarted1_UserInvitesContact()
{
#ifdef KMESSDEBUG_WEBCAMTRANSFER_P2P
  kDebug() << "starting webcam session";
#endif

  // Generate context field
  webcamGuid_ = "{11CF1BE9-3186-93A5-B38D-914DB1163BF7}";

  // Write the string
  int contextLength = webcamGuid_.size() * 2 + 2;
  QByteArray context( contextLength, '\0' );
  P2PMessage::insertUtf16String( context, webcamGuid_, 0 );

  // Encode to base64
  QString encodedContext( context.toBase64().replace( "AAAA", "AA==" ) );  // FIXME: find out why the null padding is wrong.

  // Test 1
  encodedContext = "ewAwADQAMgA1AEUANwA5ADcALQA0ADkARgAxAC0ANABEADMANwAtADkAMAA5AEEALQAwADMAMQAxADEANgAxADEAOQBEADkAQgB9AA==";

  // Test 2: Kopete sending webcam to WLM
  encodedContext = "ewBCADgAQgBFADcAMABEAEUALQBFADIAQwBBAC0ANAA0ADAAMAAtAEEARQAwADMALQA4ADgARgBGADgANQBCADkARgA0AEUAOAB9AA==";

  // Send the invitation
  sendSlpSessionInvitation( KMessShared::generateID(), getPushAppId(), 4, encodedContext );
}



/**
 * Step two of a user-started chat: the contact accepts
 */
00728 void WebcamTransferP2P::userStarted2_ContactAccepts(const MimeMessage &message)
{
  Q_UNUSED( message );
  // FIXME: start DC connection

  if( isUserSender_ )
  {
    // First syn message to send.
    sendSipMessage( "syn", 0x06, 0x91, 0x7c, P2P_MSG_WEBCAM_SYN );  // WLM
    //sendSipMessage( "syn", 0x17, 0x2a, 0x01 );  // Kopete
  }
}



/**
 * Step three of a user-started chat: the user prepares for the session
 */
00746 void WebcamTransferP2P::userStarted3_UserPrepares()
{


}



#include "webcamtransferp2p.moc"

Generated by  Doxygen 1.6.0   Back to index